diff --git a/CHANGES b/CHANGES
--- a/CHANGES
+++ b/CHANGES
@@ -1,9 +1,151 @@
 User-visible changes in hledger and hledger-lib.
 
 
+0.26 (2015/7/12)
+
+Account aliases:
+
+- Account aliases are once again non-regular-expression-based, by default. (#252)
+    
+    The regex account aliases added in 0.24 trip up people switching between
+    hledger and Ledger. (Also they are currently slow).
+    
+    This change makes the old non-regex aliases the default; they are
+    unsurprising, useful, and pretty close in functionality to Ledger's.
+    
+    The new regex aliases are still available; they must be enclosed
+    in forward slashes. (Ledger effectively ignores these.)
+    
+Journal format:
+
+- We now parse, and also print, journal entries with no postings, as
+  proposed on the mail lists.  These are not well-formed General
+  Journal entries/transactions, but here is my rationale:
+    
+    - Ledger and beancount parse them
+    - if they are parsed, they should be printed
+    - they provide a convenient way to record (and report) non-transaction events
+    - they permit more gradual introduction and learning of the concepts.
+      So eg a beginner can keep a simple journal before learning about accounts and postings.
+
+- Trailing whitespace after a `comment` directive is now ignored.
+
+Command-line interface:
+
+- The -f/file option may now be used multiple times. 
+  This is equivalent to concatenating the input files before running hledger.
+  The add command adds entries to the first file specified.
+
+Queries:
+
+- real: (no argument) is now a synonym for real:1
+
+- tag: now matches tag names with a regular expression, like most other queries
+
+- empty: is no longer supported, as it overlaps a bit confusingly with
+  amt:0. The --empty flag is still available.
+
+- You can now match on pending status (#250)
+    
+    A transaction/posting status of ! (pending) was effectively equivalent
+    to * (cleared). Now it's a separate state, not matched by --cleared.
+    The new Ledger-compatible --pending flag matches it, and so does
+    --uncleared.
+
+    The relevant search query terms are now status:*, status:! and
+    status: (the old status:1 and status:0 spellings are deprecated).
+    
+    Since we interpret --uncleared and status: as "any state except cleared",
+    it's not currently possible to match things which are neither cleared
+    nor pending.
+
+activity:
+- activity no longer excludes 0-amount postings by default.
+
+add:
+- Don't show quotes around the journal file path in the "Creating..."
+  message, for consistency with the subsequent "Adding..." message.
+
+balancesheet:
+- Accounts beginning with "debt" or now also recognised as liabilities.
+
+print:
+- We now limit the display precision of inferred prices. (#262)
+    
+    When a transaction posts to two commodities without specifying the
+    conversion price, we generate a price which makes it balance (cf
+    http://hledger.org/manual.html#prices). The print command showed
+    this with full precision (so that manual calculations with the
+    displayed numbers would look right), but this sometimes meant we
+    showed 255 digits (when there are multiple postings in the
+    commodity being priced, and the averaged unit price is an
+    irrational number). In this case we now set the price's display
+    precision to the sum of the (max) display precisions of the
+    commodities involved. An example:
+    
+    hledgerdev -f- print
+    <<<
+    1/1
+        c    C 10.00
+        c    C 11.00
+        d  D -320.00
+    >>>
+    2015/01/01
+        c  C 10.00 @ D 15.2381
+        c  C 11.00 @ D 15.2381
+        d     D -320.00
+    
+    >>>=0
+    
+    There might still be cases where this will show more price decimal
+    places than necessary. 
+
+- We now show inferred unit prices with at least 2 decimal places.
+    
+    When inferring prices, if the commodities involved have low
+    display precisions, we don't do a good job of rendering
+    accurate-looking unit prices. Eg if the journal doesn't use any
+    decimal places, any inferred unit prices are also displayed with
+    no decimal places, which makes them look wrong to the user.  Now,
+    we always give inferred unit prices a minimum display precision of
+    2, which helps a bit.
+
+register:
+- Postings with no amounts could give a runtime error in some obscure case, now fixed.
+
+stats: 
+- stats now supports -o/--outputfile, like register/balance/print.
+- An O(n^2) performance slowdown has been fixed, it's now much faster on large journals.
+
+      +--------------------------------------++--------+--------+
+      |                                      ||   0.25 |   0.26 |
+      +======================================++========+========+
+      | -f data/100x100x10.journal     stats ||   0.10 |   0.16 |
+      | -f data/1000x1000x10.journal   stats ||   0.45 |   0.21 |
+      | -f data/10000x1000x10.journal  stats ||  58.92 |   2.16 |
+      +--------------------------------------++--------+--------+
+
+Miscellaneous:
+
+- The June 30 day span was not being rendered correctly; fixed. (#272)
+
+- The bench script invoked by "cabal bench" or "stack bench" now runs
+  some simple benchmarks.
+    
+    You can get more accurate benchmark times by running with --criterion.
+    This will usually give much the same numbers and takes much longer.
+    
+    Or with --simplebench, it benchmarks whatever commands are
+    configured in bench/default.bench. This mode uses the first
+    "hledger" executable in $PATH.
+
+- The deprecated shakespeare-text dependency has been removed more thoroughly.
+
+
 0.25.1 (2015/4/29)
 
 - timelog: support the description field (#247)
+
 
 0.25 (2015/4/7)
 
diff --git a/Hledger/Cli.hs b/Hledger/Cli.hs
--- a/Hledger/Cli.hs
+++ b/Hledger/Cli.hs
@@ -346,7 +346,7 @@
              tsourcepos=nullsourcepos,
              tdate=parsedate "2007/01/01",
              tdate2=Nothing,
-             tstatus=False,
+             tstatus=Uncleared,
              tcode="*",
              tdescription="opening balance",
              tcomment="",
@@ -362,7 +362,7 @@
              tsourcepos=nullsourcepos,
              tdate=parsedate "2007/02/01",
              tdate2=Nothing,
-             tstatus=False,
+             tstatus=Uncleared,
              tcode="*",
              tdescription="ayres suites",
              tcomment="",
@@ -378,7 +378,7 @@
              tsourcepos=nullsourcepos,
              tdate=parsedate "2007/01/02",
              tdate2=Nothing,
-             tstatus=False,
+             tstatus=Uncleared,
              tcode="*",
              tdescription="auto transfer to savings",
              tcomment="",
@@ -394,7 +394,7 @@
              tsourcepos=nullsourcepos,
              tdate=parsedate "2007/01/03",
              tdate2=Nothing,
-             tstatus=False,
+             tstatus=Uncleared,
              tcode="*",
              tdescription="poquito mas",
              tcomment="",
@@ -410,7 +410,7 @@
              tsourcepos=nullsourcepos,
              tdate=parsedate "2007/01/03",
              tdate2=Nothing,
-             tstatus=False,
+             tstatus=Uncleared,
              tcode="*",
              tdescription="verizon",
              tcomment="",
@@ -426,7 +426,7 @@
              tsourcepos=nullsourcepos,
              tdate=parsedate "2007/01/03",
              tdate2=Nothing,
-             tstatus=False,
+             tstatus=Uncleared,
              tcode="*",
              tdescription="discover",
              tcomment="",
diff --git a/Hledger/Cli/Accounts.hs b/Hledger/Cli/Accounts.hs
--- a/Hledger/Cli/Accounts.hs
+++ b/Hledger/Cli/Accounts.hs
@@ -49,10 +49,10 @@
 accounts CliOpts{reportopts_=ropts} j = do
   d <- getCurrentDay
   let q = queryFromOpts d ropts
-      nodepthq = dbg "nodepthq" $ filterQuery (not . queryIsDepth) q
-      depth    = dbg "depth" $ queryDepth $ filterQuery queryIsDepth q
-      ps = dbg "ps" $ journalPostings $ filterJournalPostings nodepthq j
-      as = dbg "as" $ nub $ filter (not . null) $ map (clipAccountName depth) $ sort $ map paccount ps
+      nodepthq = dbg1 "nodepthq" $ filterQuery (not . queryIsDepth) q
+      depth    = dbg1 "depth" $ queryDepth $ filterQuery queryIsDepth q
+      ps = dbg1 "ps" $ journalPostings $ filterJournalPostings nodepthq j
+      as = dbg1 "as" $ nub $ filter (not . null) $ map (clipAccountName depth) $ sort $ map paccount ps
       as' | tree_ ropts = expandAccountNames as
           | otherwise   = as
       render a | tree_ ropts = replicate (2 * (accountNameLevel a - 1)) ' ' ++ accountLeafName a
diff --git a/Hledger/Cli/Add.hs b/Hledger/Cli/Add.hs
--- a/Hledger/Cli/Add.hs
+++ b/Hledger/Cli/Add.hs
@@ -147,7 +147,7 @@
       balancedPostingsWizard = do
         ps <- postingsWizard es2{esPostings=[]}
         let t = nulltransaction{tdate=date
-                               ,tstatus=False
+                               ,tstatus=Uncleared
                                ,tcode=code
                                ,tdescription=desc
                                ,tcomment=comment
@@ -241,11 +241,11 @@
    line $ green $ printf "Account %d%s%s: " pnum (endmsg::String) (showDefault def)
     where
       canfinish = not (null esPostings) && postingsBalanced esPostings
-      parseAccountOrDotOrNull _  _ "."       = dbg $ Just "." -- . always signals end of txn
-      parseAccountOrDotOrNull "" True ""     = dbg $ Just ""  -- when there's no default and txn is balanced, "" also signals end of txn
-      parseAccountOrDotOrNull def@(_:_) _ "" = dbg $ Just def -- when there's a default, "" means use that
-      parseAccountOrDotOrNull _ _ s          = dbg $ either (const Nothing) validateAccount $ runParser (accountnamep <* eof) (jContext esJournal) "" s -- otherwise, try to parse the input as an accountname
-      dbg = id -- strace
+      parseAccountOrDotOrNull _  _ "."       = dbg1 $ Just "." -- . always signals end of txn
+      parseAccountOrDotOrNull "" True ""     = dbg1 $ Just ""  -- when there's no default and txn is balanced, "" also signals end of txn
+      parseAccountOrDotOrNull def@(_:_) _ "" = dbg1 $ Just def -- when there's a default, "" means use that
+      parseAccountOrDotOrNull _ _ s          = dbg1 $ either (const Nothing) validateAccount $ runParser (accountnamep <* eof) (jContext esJournal) "" s -- otherwise, try to parse the input as an accountname
+      dbg1 = id -- strace
       validateAccount s | no_new_accounts_ esOpts && not (s `elem` journalAccountNames esJournal) = Nothing
                         | otherwise = Just s
 
diff --git a/Hledger/Cli/Balance.hs b/Hledger/Cli/Balance.hs
--- a/Hledger/Cli/Balance.hs
+++ b/Hledger/Cli/Balance.hs
@@ -434,7 +434,7 @@
                   ++ (if row_total_ opts then ["  Total"] else [])
                   ++ (if average_ opts then ["Average"] else [])
     items' | empty_ opts = items
-           | otherwise   = items -- dbg "2" $ filter (any (not . isZeroMixedAmount) . snd) $ dbg "1" items
+           | otherwise   = items -- dbg1 "2" $ filter (any (not . isZeroMixedAmount) . snd) $ dbg1 "1" items
     accts = map renderacct items'
     renderacct ((a,a',i),_,_,_)
       | tree_ opts = replicate ((i-1)*2) ' ' ++ a'
diff --git a/Hledger/Cli/Histogram.hs b/Hledger/Cli/Histogram.hs
--- a/Hledger/Cli/Histogram.hs
+++ b/Hledger/Cli/Histogram.hs
@@ -13,10 +13,8 @@
 import System.Console.CmdArgs.Explicit
 import Text.Printf
 
+import Hledger
 import Hledger.Cli.Options
-import Hledger.Data
-import Hledger.Reports
-import Hledger.Query
 import Prelude hiding (putStr)
 import Hledger.Utils.UTF8IOCompat (putStr)
 
@@ -54,10 +52,7 @@
       -- same as Register
       -- should count transactions, not postings ?
       -- ps = sortBy (comparing postingDate) $ filterempties $ filter matchapats $ filterdepth $ journalPostings j
-      ps = sortBy (comparing postingDate) $ filterempties $ filter (q `matchesPosting`) $ journalPostings j
-      filterempties
-          | queryEmpty q = id
-          | otherwise = filter (not . isZeroMixedAmount . pamount)
+      ps = sortBy (comparing postingDate) $ filter (q `matchesPosting`) $ journalPostings j
 
 printDayWith f (DateSpan b _, ps) = printf "%s %s\n" (show $ fromJust b) (f ps)
 
diff --git a/Hledger/Cli/Main.hs b/Hledger/Cli/Main.hs
--- a/Hledger/Cli/Main.hs
+++ b/Hledger/Cli/Main.hs
@@ -199,15 +199,15 @@
     isNullCommand        = null rawcmd
     (argsbeforecmd, argsaftercmd') = break (==rawcmd) args
     argsaftercmd         = drop 1 argsaftercmd'
-    dbgM :: Show a => String -> a -> IO ()
-    dbgM = dbgAtM 2
+    dbgIO :: Show a => String -> a -> IO ()
+    dbgIO = tracePrettyAtIO 2
 
-  dbgM "running" prognameandversion
-  dbgM "raw args" args
-  dbgM "raw args rearranged for cmdargs" args'
-  dbgM "raw command is probably" rawcmd
-  dbgM "raw args before command" argsbeforecmd
-  dbgM "raw args after command" argsaftercmd
+  dbgIO "running" prognameandversion
+  dbgIO "raw args" args
+  dbgIO "raw args rearranged for cmdargs" args'
+  dbgIO "raw command is probably" rawcmd
+  dbgIO "raw args before command" argsbeforecmd
+  dbgIO "raw args after command" argsaftercmd
 
   -- Search PATH for add-ons, excluding any that match built-in names.
   -- The precise addon names (including file extension) are used for command
@@ -231,32 +231,32 @@
     generalHelp          = putStr $ showModeHelp $ mainmode addonDisplayNames
     badCommandError      = error' ("command "++rawcmd++" is not recognized, run with no command to see a list") >> exitFailure
     f `orShowHelp` mode  = if hasHelp args then putStr (showModeHelp mode) else f
-  dbgM "processed opts" opts
-  dbgM "command matched" cmd
-  dbgM "isNullCommand" isNullCommand
-  dbgM "isInternalCommand" isInternalCommand
-  dbgM "isExternalCommand" isExternalCommand
-  dbgM "isBadCommand" isBadCommand
+  dbgIO "processed opts" opts
+  dbgIO "command matched" cmd
+  dbgIO "isNullCommand" isNullCommand
+  dbgIO "isInternalCommand" isInternalCommand
+  dbgIO "isExternalCommand" isExternalCommand
+  dbgIO "isBadCommand" isBadCommand
   d <- getCurrentDay
-  dbgM "date span from opts" (dateSpanFromOpts d $ reportopts_ opts)
-  dbgM "interval from opts" (intervalFromOpts $ reportopts_ opts)
-  dbgM "query from opts & args" (queryFromOpts d $ reportopts_ opts)
+  dbgIO "date span from opts" (dateSpanFromOpts d $ reportopts_ opts)
+  dbgIO "interval from opts" (intervalFromOpts $ reportopts_ opts)
+  dbgIO "query from opts & args" (queryFromOpts d $ reportopts_ opts)
   let
     runHledgerCommand
       -- high priority flags and situations. --help should be highest priority.
-      | hasHelp argsbeforecmd    = dbgM "" "--help before command, showing general help" >> generalHelp
+      | hasHelp argsbeforecmd    = dbgIO "" "--help before command, showing general help" >> generalHelp
       | not (hasHelp argsaftercmd) && (hasVersion argsbeforecmd || (hasVersion argsaftercmd && isInternalCommand))
                                  = putStrLn prognameandversion
       | not (hasHelp argsaftercmd) && (hasDetailedVersion argsbeforecmd || (hasDetailedVersion argsaftercmd && isInternalCommand))
                                  = putStrLn prognameanddetailedversion
       -- \| (null externalcmd) && "binary-filename" `inRawOpts` rawopts = putStrLn $ binaryfilename progname
       -- \| "--browse-args" `elem` args     = System.Console.CmdArgs.Helper.execute "cmdargs-browser" mainmode' args >>= (putStr . show)
-      | isNullCommand            = dbgM "" "no command, showing general help" >> generalHelp
+      | isNullCommand            = dbgIO "" "no command, showing general help" >> generalHelp
       | isBadCommand             = badCommandError
 
       -- internal commands
       | cmd == "activity"        = withJournalDo opts histogram       `orShowHelp` activitymode
-      | cmd == "add"             = (journalFilePathFromOpts opts >>= ensureJournalFileExists >> withJournalDo opts add) `orShowHelp` addmode
+      | cmd == "add"             = (journalFilePathFromOpts opts >>= (ensureJournalFileExists . head) >> withJournalDo opts add) `orShowHelp` addmode
       | cmd == "accounts"        = withJournalDo opts accounts        `orShowHelp` accountsmode
       | cmd == "balance"         = withJournalDo opts balance         `orShowHelp` balancemode
       | cmd == "balancesheet"    = withJournalDo opts balancesheet    `orShowHelp` balancesheetmode
@@ -271,9 +271,9 @@
       | isExternalCommand = do
           let externalargs = argsbeforecmd ++ filter (not.(=="--")) argsaftercmd
           let shellcmd = printf "%s-%s %s" progname cmd (unwords' externalargs) :: String
-          dbgM "external command selected" cmd
-          dbgM "external command arguments" (map quoteIfNeeded externalargs)
-          dbgM "running shell command" shellcmd
+          dbgIO "external command selected" cmd
+          dbgIO "external command arguments" (map quoteIfNeeded externalargs)
+          dbgIO "running shell command" shellcmd
           system shellcmd >>= exitWith
 
       -- deprecated commands
diff --git a/Hledger/Cli/Options.hs b/Hledger/Cli/Options.hs
--- a/Hledger/Cli/Options.hs
+++ b/Hledger/Cli/Options.hs
@@ -64,6 +64,7 @@
 import qualified Control.Exception as C
 import Control.Monad (when)
 import Data.List.Compat
+import Data.List.Split (splitOneOf)
 import Data.Maybe
 import Safe
 import System.Console.CmdArgs
@@ -121,8 +122,9 @@
  ,flagReq  ["period","p"]    (\s opts -> Right $ setopt "period" s opts) "PERIODEXP" "set start date, end date, and/or reporting interval all at once (overrides the flags above)"
  ,flagNone ["date2","aux-date"] (setboolopt "date2") "use postings/txns' secondary dates instead"
 
- ,flagNone ["cleared","C"]   (setboolopt "cleared") "include only pending/cleared postings/txns"
- ,flagNone ["uncleared","U"] (setboolopt "uncleared") "include only uncleared postings/txns"
+ ,flagNone ["cleared","C"]   (setboolopt "cleared") "include only cleared postings/txns"
+ ,flagNone ["pending"]       (setboolopt "pending") "include only pending postings/txns"
+ ,flagNone ["uncleared","U"] (setboolopt "uncleared") "include only uncleared (and pending) postings/txns"
  ,flagNone ["real","R"]      (setboolopt "real") "include only non-virtual postings"
  ,flagReq  ["depth"]         (\s opts -> Right $ setopt "depth" s opts) "N" "hide accounts/postings deeper than N"
  ,flagNone ["empty","E"]     (setboolopt "empty") "show empty/zero things which are normally omitted"
@@ -248,7 +250,7 @@
 data CliOpts = CliOpts {
      rawopts_         :: RawOpts
     ,command_         :: String
-    ,file_            :: Maybe FilePath
+    ,file_            :: [FilePath]
     ,rules_file_      :: Maybe FilePath
     ,output_file_     :: Maybe FilePath
     ,output_format_   :: Maybe String
@@ -309,7 +311,7 @@
   return defcliopts {
               rawopts_         = rawopts
              ,command_         = stringopt "command" rawopts
-             ,file_            = maybestringopt "file" rawopts
+             ,file_            = map stripquotes $ listofstringopt "file" rawopts
              ,rules_file_      = maybestringopt "rules-file" rawopts
              ,output_file_     = maybestringopt "output-file" rawopts
              ,output_format_   = maybestringopt "output-format" rawopts
@@ -360,26 +362,22 @@
 -- CliOpts accessors
 
 -- | Get the account name aliases from options, if any.
-aliasesFromOpts :: CliOpts -> [(AccountName,AccountName)]
-aliasesFromOpts = map parseAlias . alias_
-    where
-      -- similar to ledgerAlias
-      parseAlias :: String -> (AccountName,AccountName)
-      parseAlias s = (accountNameWithoutPostingType $ strip orig
-                     ,accountNameWithoutPostingType $ strip alias')
-          where
-            (orig, alias) = break (=='=') s
-            alias' = case alias of ('=':rest) -> rest
-                                   _ -> orig
+aliasesFromOpts :: CliOpts -> [AccountAlias]
+aliasesFromOpts = map (\a -> fromparse $ runParser accountaliasp () ("--alias "++quoteIfNeeded a) a)
+                  . alias_
 
 -- | Get the (tilde-expanded, absolute) journal file path from
 -- 1. options, 2. an environment variable, or 3. the default.
-journalFilePathFromOpts :: CliOpts -> IO String
+-- Actually, returns one or more file paths. There will be more
+-- than one if multiple -f options were provided.
+journalFilePathFromOpts :: CliOpts -> IO [String]
 journalFilePathFromOpts opts = do
   f <- defaultJournalPath
   d <- getCurrentDirectory
-  expandPath d $ fromMaybe f $ file_ opts
-
+  mapM (expandPath d) $ ifEmpty (file_ opts) [f]
+ where
+    ifEmpty [] d = d
+    ifEmpty l _ = l
 
 -- | Get the expanded, absolute output file path from options,
 -- or the default (-, meaning stdout).
@@ -521,7 +519,7 @@
 -- directory) or whether it has execute permission.
 hledgerExecutablesInPath :: IO [String]
 hledgerExecutablesInPath = do
-  pathdirs <- regexSplit "[:;]" `fmap` getEnvSafe "PATH"
+  pathdirs <- splitOneOf "[:;]" `fmap` getEnvSafe "PATH"
   pathfiles <- concat `fmap` mapM getDirectoryContentsSafe pathdirs
   return $ nub $ sort $ filter isHledgerExeName pathfiles
   -- XXX should exclude directories and files without execute permission.
diff --git a/Hledger/Cli/Print.hs b/Hledger/Cli/Print.hs
--- a/Hledger/Cli/Print.hs
+++ b/Hledger/Cli/Print.hs
@@ -100,7 +100,7 @@
     description = tdescription t
     date = showDate (tdate t)
     date2 = maybe "" showDate (tdate2 t)
-    status = if tstatus t then "*" else ""
+    status = show $ tstatus t
     code = tcode t
     comment = chomp $ strip $ tcomment t
 
@@ -116,7 +116,7 @@
    amounts
   where
     Mixed amounts = pamount p
-    status = if pstatus p then "*" else ""
+    status = show $ pstatus p
     account = showAccountName Nothing (ptype p) (paccount p)
     comment = chomp $ strip $ pcomment p
 
diff --git a/Hledger/Cli/Register.hs b/Hledger/Cli/Register.hs
--- a/Hledger/Cli/Register.hs
+++ b/Hledger/Cli/Register.hs
@@ -149,9 +149,12 @@
                                _                      -> (id,acctwidth)
       amt = showMixedAmountWithoutPrice $ pamount p
       bal = showMixedAmountWithoutPrice b
+      -- alternate behaviour, show null amounts as 0 instead of blank
+      -- amt = if null amt' then "0" else amt'
+      -- bal = if null bal' then "0" else bal'
       (amtlines, ballines) = (lines amt, lines bal)
       (amtlen, ballen) = (length amtlines, length ballines)
-      numlines = max amtlen ballen
+      numlines = max 1 (max amtlen ballen)
       (amtfirstline:amtrest) = take numlines $ amtlines ++ repeat "" -- posting amount is top-aligned
       (balfirstline:balrest) = take numlines $ replicate (numlines - ballen) "" ++ ballines -- balance amount is bottom-aligned
       spacer = replicate (totalwidth - (amtwidth + 2 + balwidth)) ' '
diff --git a/Hledger/Cli/Stats.hs b/Hledger/Cli/Stats.hs
--- a/Hledger/Cli/Stats.hs
+++ b/Hledger/Cli/Stats.hs
@@ -13,6 +13,8 @@
 import Data.List
 import Data.Maybe
 import Data.Ord
+import Data.HashSet (size, fromList)
+import Data.Text (pack)
 import Data.Time.Calendar
 import System.Console.CmdArgs.Explicit
 import Text.Printf
@@ -21,13 +23,15 @@
 import Hledger
 import Hledger.Cli.Options
 import Prelude hiding (putStr)
-import Hledger.Utils.UTF8IOCompat (putStr)
+import Hledger.Cli.Utils (writeOutput)
 
 
 statsmode = (defCommandMode $ ["stats"] ++ aliases) {
   modeHelp = "show some journal statistics" `withAliases` aliases
  ,modeGroupFlags = Group {
-     groupUnnamed = []
+     groupUnnamed = [
+        flagReq  ["output-file","o"]   (\s opts -> Right $ setopt "output-file" s opts) "FILE[.FMT]" "write output to FILE instead of stdout. A recognised FMT suffix influences the format."
+        ]
     ,groupHidden = []
     ,groupNamed = [generalflagsgroup1]
     }
@@ -37,7 +41,7 @@
 -- like Register.summarisePostings
 -- | Print various statistics for the journal.
 stats :: CliOpts -> Journal -> IO ()
-stats CliOpts{reportopts_=reportopts_} j = do
+stats opts@CliOpts{reportopts_=reportopts_} j = do
   d <- getCurrentDay
   let q = queryFromOpts d reportopts_
       l = ledgerFromJournal q j
@@ -45,7 +49,7 @@
       intervalspans = splitSpan (intervalFromOpts reportopts_) reportspan
       showstats = showLedgerStats l d
       s = intercalate "\n" $ map showstats intervalspans
-  putStr s
+  writeOutput opts s
 
 showLedgerStats :: Ledger -> Day -> DateSpan -> String
 showLedgerStats l today span =
@@ -63,7 +67,7 @@
         ,("Transactions", printf "%d (%0.1f per day)" tnum txnrate)
         ,("Transactions last 30 days", printf "%d (%0.1f per day)" tnum30 txnrate30)
         ,("Transactions last 7 days", printf "%d (%0.1f per day)" tnum7 txnrate7)
-        ,("Payees/descriptions", show $ length $ nub $ map tdescription ts)
+        ,("Payees/descriptions", show $ size $ fromList $ map (pack . tdescription) ts)
         ,("Accounts", printf "%d (depth %d)" acctnum acctdepth)
         ,("Commodities", printf "%s (%s)" (show $ length cs) (intercalate ", " cs))
       -- Transactions this month     : %(monthtxns)s (last month in the same period: %(lastmonthtxns)s)
diff --git a/Hledger/Cli/Utils.hs b/Hledger/Cli/Utils.hs
--- a/Hledger/Cli/Utils.hs
+++ b/Hledger/Cli/Utils.hs
@@ -68,7 +68,7 @@
   -- to let the add command work.
   rulespath <- rulesFilePathFromOpts opts
   journalpath <- journalFilePathFromOpts opts
-  ej <- readJournalFile Nothing rulespath (not $ ignore_assertions_ opts) journalpath
+  ej <- readJournalFiles Nothing rulespath (not $ ignore_assertions_ opts) journalpath
   either error' (cmd opts . journalApplyAliases (aliasesFromOpts opts)) ej
 
 -- | Write some output to stdout or to a file selected by --output-file.
diff --git a/bench/bench.hs b/bench/bench.hs
new file mode 100644
--- /dev/null
+++ b/bench/bench.hs
@@ -0,0 +1,70 @@
+-- bench
+-- By default, show approximate times for some standard hledger operations on a sample journal.
+-- With --criterion, show accurate times (slow).
+-- With --simplebench, show approximate times for the commands in default.bench, using the first hledger executable on $PATH.
+
+import Criterion.Main     (defaultMainWith, defaultConfig, bench, nfIO)
+import SimpleBench        (defaultMain)
+import System.Directory   (getCurrentDirectory)
+import System.Environment (getArgs, withArgs)
+import System.Info        (os)
+import System.Process     (readProcess)
+import System.TimeIt      (timeItT)
+import Text.Printf
+import Hledger.Cli
+
+-- sample journal file to use for benchmarks
+inputfile = "bench/10000x1000x10.journal"
+
+outputfile = "/dev/null" -- hide output of benchmarked commands (XXX unixism)
+-- outputfile = "-" -- show output of benchmarked commands
+
+main = do
+ -- withArgs ["--simplebench"] $ do
+ -- withArgs ["--criterion"] $ do
+  args <- getArgs
+  if "--criterion" `elem` args
+    then withArgs [] benchWithCriterion
+    else if "--simplebench" `elem` args
+         then benchWithSimplebench
+         else benchWithTimeit
+
+benchWithTimeit = do
+  getCurrentDirectory >>= printf "Benchmarking hledger in %s with timeit\n"
+  let opts = defcliopts{output_file_=Just outputfile}
+  (t0,j) <- timeit ("read "++inputfile) $ either error id <$> readJournalFile Nothing Nothing True inputfile
+  (t1,_) <- timeit ("print") $ print' opts j
+  (t2,_) <- timeit ("register") $ register opts j
+  (t3,_) <- timeit ("balance") $ balance  opts j
+  (t4,_) <- timeit ("stats") $ stats opts j
+  printf "Total: %0.2fs\n" (sum [t0,t1,t2,t3,t4])
+
+timeit :: String -> IO a -> IO (Double, a)
+timeit name action = do
+  printf "%s%s" name (replicate (40 - length name) ' ')
+  (t,a) <- timeItT action
+  printf "[%.2fs]\n" t
+  return (t,a)
+
+benchWithCriterion = do
+  getCurrentDirectory >>= printf "Benchmarking hledger in %s with criterion\n"
+  let opts = defcliopts{output_file_=Just "/dev/null"}
+  j <- either error id <$> readJournalFile Nothing Nothing True inputfile
+  Criterion.Main.defaultMainWith defaultConfig $ [
+    bench ("read "++inputfile) $ nfIO $ (either error const <$> readJournalFile Nothing Nothing True inputfile),
+    bench ("print")            $ nfIO $ print'   opts j,
+    bench ("register")         $ nfIO $ register opts j,
+    bench ("balance")          $ nfIO $ balance  opts j,
+    bench ("stats")            $ nfIO $ stats    opts j
+    ]
+
+benchWithSimplebench = do
+  let whichcmd = if os == "mingw32" then "where" else "which"
+  exe <- init <$> readProcess whichcmd ["hledger"] ""
+  pwd <- getCurrentDirectory
+  printf "Benchmarking %s in %s with simplebench and shell\n" exe pwd
+  flip withArgs SimpleBench.defaultMain [
+     "-fbench/default.bench"
+    ,"-v"
+    ,"hledger"
+    ] 
diff --git a/hledger.cabal b/hledger.cabal
--- a/hledger.cabal
+++ b/hledger.cabal
@@ -1,5 +1,5 @@
 name:           hledger
-version: 0.25.1
+version: 0.26
 stability:      stable
 category:       Finance, Console
 synopsis:       The main command-line interface for the hledger accounting tool.
@@ -17,22 +17,16 @@
 maintainer:     Simon Michael <simon@joyful.com>
 homepage:       http://hledger.org
 bug-reports:    http://hledger.org/bugs
-tested-with:    GHC==7.8.2, GHC==7.10.1
+tested-with:    GHC==7.8.4, GHC==7.10.1
 cabal-version:  >= 1.10
 build-type:     Simple
 -- data-dir:       data
 -- data-files:
 extra-tmp-files:
 extra-source-files: 
-                    tests/suite.hs
+                    test/test.hs
                     CHANGES
 
--- Cabal-Version:  >= 1.9.2
--- Test-Suite test-hledger
---     type:       exitcode-stdio-1.0
---     main-is:    test-hledger.hs
---     build-depends: base
-
 source-repository head
   type:     git
   location: https://github.com/simonmichael/hledger
@@ -53,7 +47,7 @@
 
 
 library
-  cpp-options: -DVERSION="0.25.1"
+  cpp-options: -DVERSION="0.26"
   ghc-options: -Wall -fno-warn-unused-do-bind -fno-warn-name-shadowing -fno-warn-missing-signatures
   ghc-options: -fno-warn-type-defaults -fno-warn-orphans
   default-language: Haskell2010
@@ -75,11 +69,12 @@
                   Hledger.Cli.Register
                   Hledger.Cli.Stats
   build-depends:
-                  hledger-lib == 0.25.1
+                  hledger-lib == 0.26
                  ,base >= 4.3 && < 5
                  ,base-compat >= 0.8.1
                  -- ,cabal-file-th
                  ,containers
+                 ,unordered-containers
                  ,cmdargs >= 0.10 && < 0.11
                  ,csv
                  -- ,data-pprint >= 0.2.1 && < 0.3
@@ -93,7 +88,6 @@
                  ,parsec >= 3
                  ,process
                  ,regex-tdfa
-                 ,regexpr >= 0.5.1
                  ,safe >= 0.2
                  ,split >= 0.1 && < 0.3
                  ,text >= 0.11
@@ -104,7 +98,7 @@
     -- ghc 7.10 requires shakespeare 2.0.2.2+
     build-depends: shakespeare      >= 2.0.2.2 && < 2.1
   else
-    -- for older ghcs, allow shakespeare 2.x or 1.x (which also requires shakespeare-text)
+    -- for older ghcs, allow shakespeare 1.x (which also requires shakespeare-text)
     -- http://www.yesodweb.com/blog/2014/04/consolidation-progress
     build-depends:
                   shakespeare      >= 1.0 && < 2.1
@@ -123,18 +117,19 @@
   main-is:        hledger-cli.hs
   hs-source-dirs: app
   default-language: Haskell2010
-  cpp-options: -DVERSION="0.25.1"
+  cpp-options: -DVERSION="0.26"
   ghc-options: -Wall -fno-warn-unused-do-bind -fno-warn-name-shadowing -fno-warn-missing-signatures
   ghc-options: -fno-warn-type-defaults -fno-warn-orphans
   if flag(threaded)
        ghc-options:   -threaded
   -- same as above:
   build-depends:
-                  hledger-lib == 0.25.1
-                 ,hledger == 0.25.1
+                  hledger-lib == 0.26
+                 ,hledger == 0.26
                  ,base >= 4.3 && < 5
                  ,base-compat >= 0.8.1
                  ,containers
+                 ,unordered-containers
                  ,cmdargs >= 0.10 && < 0.11
                  ,csv
                  -- ,data-pprint >= 0.2.1 && < 0.3
@@ -148,15 +143,19 @@
                  ,parsec >= 3
                  ,process
                  ,regex-tdfa
-                 ,regexpr >= 0.5.1
                  ,safe >= 0.2
-                 ,shakespeare-text >= 1.0 && < 1.2
-                 ,shakespeare      >= 1.0 && < 2.1
                  ,split >= 0.1 && < 0.3
                  ,tabular >= 0.2 && < 0.3
                  ,text >= 0.11
                  ,utf8-string >= 0.3.5 && < 1.1
                  ,wizards == 1.0.*
+  -- as above
+  if impl(ghc >= 7.10)
+    build-depends: shakespeare      >= 2.0.2.2 && < 2.1
+  else
+    build-depends:
+                  shakespeare      >= 1.0 && < 2.1
+                 ,shakespeare-text >= 1.0 && < 1.2
   if flag(old-locale)
     build-depends: time < 1.5, old-locale
   else
@@ -165,10 +164,10 @@
     build-depends: pretty-show >= 1.6.4
 
 
-test-suite tests
+test-suite test
   type:     exitcode-stdio-1.0
-  main-is:  suite.hs
-  hs-source-dirs: tests
+  main-is:  test.hs
+  hs-source-dirs: test
   default-language: Haskell2010
   ghc-options: -Wall -fno-warn-unused-do-bind -fno-warn-name-shadowing -fno-warn-missing-signatures
   ghc-options: -fno-warn-type-defaults -fno-warn-orphans
@@ -191,10 +190,7 @@
                , parsec >= 3
                , process
                , regex-tdfa
-               , regexpr
                , safe
-               , shakespeare-text >= 1.0 && < 1.2
-               , shakespeare      >= 1.0 && < 2.1
                , split
                ,tabular >= 0.2 && < 0.3
                , test-framework
@@ -202,6 +198,13 @@
                , text
                , transformers
                , wizards == 1.0.*
+  -- as above
+  if impl(ghc >= 7.10)
+    build-depends: shakespeare      >= 2.0.2.2 && < 2.1
+  else
+    build-depends:
+                  shakespeare      >= 1.0 && < 2.1
+                 ,shakespeare-text >= 1.0 && < 1.2
   if impl(ghc >= 7.4)
     build-depends: pretty-show >= 1.6.4
   if flag(old-locale)
@@ -210,11 +213,10 @@
     build-depends: time >= 1.5
 
 
--- not a standard cabal bench test, I think
 benchmark bench
   type:             exitcode-stdio-1.0
-  -- hs-source-dirs:   
-  main-is:          ../tools/simplebench.hs
+  hs-source-dirs:   bench
+  main-is:          bench.hs
   ghc-options: -Wall -fno-warn-unused-do-bind -fno-warn-name-shadowing -fno-warn-missing-signatures
   ghc-options: -fno-warn-type-defaults -fno-warn-orphans
   default-language: Haskell2010
@@ -222,8 +224,10 @@
                     hledger,
                     base >= 4.3 && < 5,
                     base-compat >= 0.8.1,
+                    criterion,
                     html,
                     tabular >= 0.2 && < 0.3,
+                    timeit,
                     process,
                     filepath,
                     directory
diff --git a/test/test.hs b/test/test.hs
new file mode 100644
--- /dev/null
+++ b/test/test.hs
@@ -0,0 +1,6 @@
+import Hledger.Cli (tests_Hledger_Cli)
+import Test.Framework.Providers.HUnit (hUnitTestToTests)
+import Test.Framework.Runners.Console (defaultMain)
+
+main :: IO ()
+main = defaultMain $ hUnitTestToTests tests_Hledger_Cli
diff --git a/tests/suite.hs b/tests/suite.hs
deleted file mode 100644
--- a/tests/suite.hs
+++ /dev/null
@@ -1,6 +0,0 @@
-import Hledger.Cli (tests_Hledger_Cli)
-import Test.Framework.Providers.HUnit (hUnitTestToTests)
-import Test.Framework.Runners.Console (defaultMain)
-
-main :: IO ()
-main = defaultMain $ hUnitTestToTests tests_Hledger_Cli
