packages feed

hledger 0.22.2 → 0.23

raw patch · 16 files changed

+1149/−925 lines, 16 filesdep +shakespearedep +wizardsdep ~hledger-libdep ~shakespeare-textPVP ok

version bump matches the API change (PVP)

Dependencies added: shakespeare, wizards

Dependency ranges changed: hledger-lib, shakespeare-text

API changes (from Hackage documentation)

- Hledger.Cli.Add: PostingsState :: Journal -> (AccountName -> Bool) -> Bool -> Maybe [Posting] -> PostingsState
- Hledger.Cli.Add: RestartEntryException :: RestartEntryException
- Hledger.Cli.Add: accountCompletion :: CompletionCache -> CompletionFunc IO
- Hledger.Cli.Add: askFor :: String -> Maybe String -> Maybe (String -> Bool) -> InputT IO String
- Hledger.Cli.Add: completionCache :: Journal -> CompletionCache
- Hledger.Cli.Add: data PostingsState
- Hledger.Cli.Add: data RestartEntryException
- Hledger.Cli.Add: getAndAddTransactionsLoop :: Journal -> CliOpts -> String -> [String] -> IO ()
- Hledger.Cli.Add: getPostingsForTransactionWithHistory :: Journal -> CliOpts -> String -> String -> String -> String -> [String] -> IO (Maybe Transaction)
- Hledger.Cli.Add: getPostingsLoop :: PostingsState -> [Posting] -> [String] -> IO [Posting]
- Hledger.Cli.Add: getTransaction :: Journal -> CliOpts -> String -> [String] -> IO Transaction
- Hledger.Cli.Add: getTransactionOrRestart :: Journal -> CliOpts -> String -> [String] -> IO (Maybe Transaction)
- Hledger.Cli.Add: headTailDef :: a -> [a] -> (a, [a])
- Hledger.Cli.Add: instance Exception RestartEntryException
- Hledger.Cli.Add: instance Show RestartEntryException
- Hledger.Cli.Add: instance Typeable RestartEntryException
- Hledger.Cli.Add: psAccept :: PostingsState -> AccountName -> Bool
- Hledger.Cli.Add: psHistory :: PostingsState -> Maybe [Posting]
- Hledger.Cli.Add: psJournal :: PostingsState -> Journal
- Hledger.Cli.Add: psSuggestHistoricalAmount :: PostingsState -> Bool
- Hledger.Cli.Add: runInteraction :: Journal -> InputT IO a -> IO a
- Hledger.Cli.Add: runInteractionDefault :: InputT IO a -> IO a
- Hledger.Cli.Add: type CompletionCache = [AccountName]
- Hledger.Cli.Options: activitymode :: Mode RawOpts
- Hledger.Cli.Options: addmode :: Mode RawOpts
- Hledger.Cli.Options: argsToCliOpts :: [String] -> [String] -> IO CliOpts
- Hledger.Cli.Options: balancemode :: Mode RawOpts
- Hledger.Cli.Options: balancesheetmode :: Mode RawOpts
- Hledger.Cli.Options: boolopt :: String -> RawOpts -> Bool
- Hledger.Cli.Options: cashflowmode :: Mode RawOpts
- Hledger.Cli.Options: convertmode :: Mode RawOpts
- Hledger.Cli.Options: debugArgs :: [String] -> CliOpts -> IO ()
- Hledger.Cli.Options: getHledgerAddonCommands :: IO [String]
- Hledger.Cli.Options: inRawOpts :: String -> RawOpts -> Bool
- Hledger.Cli.Options: incomestatementmode :: Mode RawOpts
- Hledger.Cli.Options: intopt :: String -> RawOpts -> Int
- Hledger.Cli.Options: listofstringopt :: Eq a => a -> [(a, t)] -> [t]
- Hledger.Cli.Options: mainmode :: [Name] -> Mode RawOpts
- Hledger.Cli.Options: maybeintopt :: String -> RawOpts -> Maybe Int
- Hledger.Cli.Options: maybestringopt :: Eq a => a -> [(a, String)] -> Maybe String
- Hledger.Cli.Options: moveFlagsAfterCommand :: [String] -> [String]
- Hledger.Cli.Options: optserror :: [Char] -> c
- Hledger.Cli.Options: printmode :: Mode RawOpts
- Hledger.Cli.Options: registermode :: Mode RawOpts
- Hledger.Cli.Options: setboolopt :: t -> [(t, [Char])] -> [(t, [Char])]
- Hledger.Cli.Options: setopt :: t -> [Char] -> [(t, [Char])] -> [(t, [Char])]
- Hledger.Cli.Options: statsmode :: Mode RawOpts
- Hledger.Cli.Options: stringopt :: Eq a => a -> [(a, String)] -> [Char]
- Hledger.Cli.Options: testmode :: Mode RawOpts
- Hledger.Cli.Options: type RawOpts = [(String, String)]
- Hledger.Cli.Print: showTransactions :: ReportOpts -> Query -> Journal -> String
- Hledger.Cli.Stats: showLedgerStats :: Ledger -> Day -> DateSpan -> String
- Hledger.Cli.Tests: flatTests :: CliOpts -> Test
- Hledger.Cli.Tests: hierarchicalTests :: CliOpts -> Test
- Hledger.Cli.Tests: runTests :: CliOpts -> IO Counts
- Hledger.Cli.Tests: runTestsTillFailure :: CliOpts -> IO (Maybe String)
+ Hledger.Cli.Accounts: accounts :: CliOpts -> Journal -> IO ()
+ Hledger.Cli.Accounts: accountsmode :: Mode RawOpts
+ Hledger.Cli.Accounts: tests_Hledger_Cli_Accounts :: Test
+ Hledger.Cli.Add: EntryState :: CliOpts -> [String] -> Day -> Day -> Journal -> Maybe Transaction -> [Posting] -> EntryState
+ Hledger.Cli.Add: RestartTransactionException :: RestartTransactionException
+ Hledger.Cli.Add: accountCompleter :: Journal -> String -> CompletionFunc IO
+ Hledger.Cli.Add: accountWizard :: EntryState -> Wizard Haskeline [Char]
+ Hledger.Cli.Add: addmode :: Mode RawOpts
+ Hledger.Cli.Add: amountAndCommentWizard :: EntryState -> Wizard Haskeline (Amount, [Char])
+ Hledger.Cli.Add: amountCompleter :: String -> CompletionFunc IO
+ Hledger.Cli.Add: completer :: [String] -> String -> CompletionFunc IO
+ Hledger.Cli.Add: confirmedTransactionWizard :: EntryState -> Wizard Haskeline Transaction
+ Hledger.Cli.Add: data EntryState
+ Hledger.Cli.Add: data RestartTransactionException
+ Hledger.Cli.Add: dateAndCodeWizard :: EntryState -> Wizard Haskeline (Day, [Char])
+ Hledger.Cli.Add: dateCompleter :: String -> CompletionFunc IO
+ Hledger.Cli.Add: defEntryState :: EntryState
+ Hledger.Cli.Add: defaultTo' :: b -> Wizard Haskeline b -> Wizard Haskeline b
+ Hledger.Cli.Add: descriptionAndCommentWizard :: EntryState -> Wizard Haskeline (String, String)
+ Hledger.Cli.Add: descriptionCompleter :: Journal -> String -> CompletionFunc IO
+ Hledger.Cli.Add: esArgs :: EntryState -> [String]
+ Hledger.Cli.Add: esDefDate :: EntryState -> Day
+ Hledger.Cli.Add: esJournal :: EntryState -> Journal
+ Hledger.Cli.Add: esOpts :: EntryState -> CliOpts
+ Hledger.Cli.Add: esPostings :: EntryState -> [Posting]
+ Hledger.Cli.Add: esSimilarTransaction :: EntryState -> Maybe Transaction
+ Hledger.Cli.Add: esToday :: EntryState -> Day
+ Hledger.Cli.Add: getAndAddTransactions :: EntryState -> IO ()
+ Hledger.Cli.Add: green :: [Char] -> [Char]
+ Hledger.Cli.Add: instance Exception RestartTransactionException
+ Hledger.Cli.Add: instance Show EntryState
+ Hledger.Cli.Add: instance Show RestartTransactionException
+ Hledger.Cli.Add: instance Typeable EntryState
+ Hledger.Cli.Add: instance Typeable RestartTransactionException
+ Hledger.Cli.Add: maybeExit :: Wizard Haskeline [Char] -> Wizard Haskeline [Char]
+ Hledger.Cli.Add: maybeRestartTransaction :: Wizard Haskeline [Char] -> Wizard Haskeline [Char]
+ Hledger.Cli.Add: postingWizard :: EntryState -> Wizard Haskeline (Maybe Posting)
+ Hledger.Cli.Add: postingsBalanced :: [Posting] -> Bool
+ Hledger.Cli.Add: postingsWizard :: EntryState -> Wizard Haskeline [Posting]
+ Hledger.Cli.Add: showDefault :: [Char] -> [Char]
+ Hledger.Cli.Add: showHelp :: IO ()
+ Hledger.Cli.Add: similarTransaction :: EntryState -> String -> Maybe Transaction
+ Hledger.Cli.Add: transactionWizard :: EntryState -> Wizard Haskeline Transaction
+ Hledger.Cli.Add: withCompletion :: :<: WithSettings b => CompletionFunc IO -> Wizard b a -> Wizard b a
+ Hledger.Cli.Balance: balancemode :: Mode RawOpts
+ Hledger.Cli.Balancesheet: balancesheetmode :: Mode RawOpts
+ Hledger.Cli.Cashflow: cashflowmode :: Mode RawOpts
+ Hledger.Cli.Histogram: activitymode :: Mode RawOpts
+ Hledger.Cli.Incomestatement: incomestatementmode :: Mode RawOpts
+ Hledger.Cli.Main: argsToCliOpts :: [String] -> [String] -> IO CliOpts
+ Hledger.Cli.Main: builtinCommandNames :: [String]
+ Hledger.Cli.Main: builtinCommands :: [Mode RawOpts]
+ Hledger.Cli.Main: mainmode :: [Name] -> Mode RawOpts
+ Hledger.Cli.Main: moveFlagsAfterCommand :: [String] -> [String]
+ Hledger.Cli.Main: oldconvertmode :: Mode RawOpts
+ Hledger.Cli.Options: defAddonCommandMode :: Name -> Mode RawOpts
+ Hledger.Cli.Options: defMode :: Mode RawOpts
+ Hledger.Cli.Options: hledgerAddons :: IO ([String], [String])
+ Hledger.Cli.Options: withAliases :: String -> [String] -> String
+ Hledger.Cli.Print: printmode :: Mode RawOpts
+ Hledger.Cli.Register: registermode :: Mode RawOpts
+ Hledger.Cli.Stats: statsmode :: Mode RawOpts
+ Hledger.Cli.Tests: testmode :: Mode RawOpts
- Hledger.Cli.Options: argsFlag :: FlagHelp -> Arg [([Char], [Char])]
+ Hledger.Cli.Options: argsFlag :: FlagHelp -> Arg RawOpts
- Hledger.Cli.Options: decodeRawOpts :: [(t, SystemString)] -> [(t, String)]
+ Hledger.Cli.Options: decodeRawOpts :: RawOpts -> RawOpts
- Hledger.Cli.Options: formatFromOpts :: ReportOpts -> Either String [FormatString]
+ Hledger.Cli.Options: formatFromOpts :: ReportOpts -> Either String [OutputFormat]
- Hledger.Cli.Options: generalflagsgroup1 :: ([Char], [Flag [([Char], [Char])]])
+ Hledger.Cli.Options: generalflagsgroup1 :: (String, [Flag RawOpts])
- Hledger.Cli.Options: generalflagsgroup2 :: ([Char], [Flag [([Char], [Char])]])
+ Hledger.Cli.Options: generalflagsgroup2 :: (String, [Flag RawOpts])
- Hledger.Cli.Options: generalflagsgroup3 :: ([Char], [Flag [([Char], [Char])]])
+ Hledger.Cli.Options: generalflagsgroup3 :: (String, [Flag RawOpts])
- Hledger.Cli.Options: helpflags :: [Flag [([Char], [Char])]]
+ Hledger.Cli.Options: helpflags :: [Flag RawOpts]
- Hledger.Cli.Options: inputflags :: [Flag [([Char], [Char])]]
+ Hledger.Cli.Options: inputflags :: [Flag RawOpts]
- Hledger.Cli.Options: reportflags :: [Flag [([Char], [Char])]]
+ Hledger.Cli.Options: reportflags :: [Flag RawOpts]

Files

CHANGES view
@@ -1,7 +1,83 @@+0.23 (2014/5/1)++Journal format:+- A # (hash) in column 0 is now also supported for starting a top-level journal comment, like Ledger.+- The "too many missing amounts" error now reminds about the 2-space rule.+- Fix: . (period) is no longer parsed as a valid amount.+- Fix: default commodity directives no longer limit the maximum display precision (#169).+- Fix: + before an amount is no longer parsed as part of the commodity (#181).++CLI:+- Command-line help cleanups, layout improvements.+- Descriptions are shown for known add-ons in the command list.+- Command aliases have been simplified.+- Add-ons can now have any of these file extensions:+  none, hs, lhs, pl, py, rb, rkt, sh, bat, com, exe.+- Add-ons are displayed without their file extensions when possible.+- Add-ons with the same name as a built-in command or alias are ignored.+- Fix: add-on detection and invocation now works on windows.+- Fix: add-ons with digits in the name are now found.+- Fix: add-on arguments containing a single quote now work.+- Fix: when -- is used to hide add-on options from the main program,+  it is no longer passed through as an add-on argument.++Queries:+- The currency/commodity query prefix (sym:) has been renamed to cur:.+- Currency/commodity queries are applied more strongly in register and+  balance reports, filtering out unwanted currencies entirely. Eg+  hledger balance cur:'\$' now reports only the dollar amounts even if+  there are multi-currency transactions or postings.+- Amount queries like amt:N, amt:<N and amt:>N, where N is not 0, now do an unsigned+  comparison of the amount and N. That is, they compare the absolute magnitude.+  To do a signed comparison instead, write N with its sign (eg amt:+N, amt:<+N, amt:>-N).+- Fix: amount queries no longer give false positives on multi-commodity amounts.++accounts:+- An accounts command has been added, similar to Ledger's, for listing account names+  in flat or hierarchical mode.++add:+- Tab completion now works at all prompts, and will insert the default if the input area is empty.+- Account and amount defaults are more robust and useful.+- Transactions may also be completed by the enter key, when there are no more default postings.+- Input prompts are displayed in a different colour when supported.++balance:+- Balance reports in flat mode now always show exclusive (subaccount-excluding) balances.+- Balance reports in flat mode with --depth now aggregate deeper accounts at the depth limit instead of excluding them.+- Multicolumn reports in flat mode now support --drop.+- Multicolumn balance reports can now show the account hierarchy with --tree.+- Multicolumn report start/end dates are adjusted to encompass the displayed+  report periods, so the first and last periods are "full" and comparable to the others.+- Fix: zero-balance leaf accounts below a non-zero-balance parent are no longer always shown (#170).+- Fix: multicolumn reports now support --date2 (cf #174).++balancesheet, cashflow, incomestatement:+- These commands now support --flat and --drop.++print:+- Tag queries (tag:) will now match a transaction if any of its postings match.++register:+- The --display option has been dropped. To see an accurate running total which+  includes the prior starting balance, use --historical/-H (like balance). +- With a report interval, report start/end dates are adjusted to encompass the displayed+  periods, so the first and last periods are "full" and comparable to the others.+- Fix: --date2 now works with report intervals (fixes #174).++Miscellaneous:+- Default report dates now derive from the secondary dates when --date2 is in effect.+- Default report dates now notice any posting dates outside the transaction dates' span.+- Debug output improvements.+- New add-on example: extra/hledger-rewrite.hs, adds postings to matched entries.+- Compatible with GHC 7.2 (#155) - GHC 7.8, shakespeare 2++ 0.22.2 (2014/4/16)  - display years before 1000 with four digits, not three - avoid pretty-show to build with GHC < 7.4 - allow text 1.1, drop data-pprint to build with GHC 7.8.x+  0.22.1 (2014/1/6) and older: see http://hledger.org/release-notes or doc/CHANGES.md.
Hledger/Cli.hs view
@@ -8,6 +8,7 @@ -}  module Hledger.Cli (+                     module Hledger.Cli.Accounts,                      module Hledger.Cli.Add,                      module Hledger.Cli.Balance,                      module Hledger.Cli.Balancesheet,@@ -20,13 +21,17 @@                      module Hledger.Cli.Options,                      module Hledger.Cli.Utils,                      module Hledger.Cli.Version,-                     tests_Hledger_Cli+                     tests_Hledger_Cli,+                     module Hledger,+                     module System.Console.CmdArgs.Explicit               ) where import Data.Time.Calendar+import System.Console.CmdArgs.Explicit import Test.HUnit  import Hledger+import Hledger.Cli.Accounts import Hledger.Cli.Add import Hledger.Cli.Balance import Hledger.Cli.Balancesheet
+ Hledger/Cli/Accounts.hs view
@@ -0,0 +1,62 @@+{-|++The @accounts@ command lists account names:++- in flat mode (default), it lists the full names of accounts posted to by matched postings,+  clipped to the specified depth, possibly with leading components dropped.++- in tree mode, it shows the indented short names of accounts posted to by matched postings,+  and their parents, to the specified depth.++-}++module Hledger.Cli.Accounts (+  accountsmode+ ,accounts+ ,tests_Hledger_Cli_Accounts+) where++import Data.List+import System.Console.CmdArgs.Explicit as C+import Test.HUnit++import Hledger+import Prelude hiding (putStrLn)+import Hledger.Utils.UTF8IOCompat (putStrLn)+import Hledger.Cli.Options+++-- | Command line options for this command.+accountsmode = (defCommandMode $ ["accounts"] ++ aliases) {+  modeHelp = "show account names" `withAliases` aliases+ ,modeHelpSuffix = [+    "This command lists the accounts referenced by matched postings (and in tree mode, their parents as well). The accounts can be depth-clipped (--depth N) or have their leading parts trimmed (--drop N)."+   ]+ ,modeGroupFlags = C.Group {+     groupUnnamed = [+      flagNone ["tree"] (\opts -> setboolopt "tree" opts) "show short account names, as a tree"+     ,flagNone ["flat"] (\opts -> setboolopt "flat" opts) "show full account names, as a list (default)"+     ,flagReq  ["drop"] (\s opts -> Right $ setopt "drop" s opts) "N" "flat mode: omit N leading account name parts"+     ]+    ,groupHidden = []+    ,groupNamed = [generalflagsgroup1]+    }+ }+  where aliases = []++-- | The accounts command.+accounts :: CliOpts -> Journal -> IO ()+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+      as' | tree_ ropts = expandAccountNames as+          | otherwise   = as+      render a | tree_ ropts = replicate (2 * (accountNameLevel a - 1)) ' ' ++ accountLeafName a+               | otherwise   = accountNameDrop (drop_ ropts) a+  mapM_ (putStrLn . render) as'++tests_Hledger_Cli_Accounts = TestList []
Hledger/Cli/Add.hs view
@@ -1,265 +1,348 @@-{-# LANGUAGE ScopedTypeVariables, DeriveDataTypeable #-}-{-| -+{-| A history-aware add command to help with data entry.--Note: this might not be sensible, but add has some aspirations of being-both user-friendly and pipeable/scriptable and for this reason-informational messages are mostly written to stderr rather than stdout.+|-} --}+{-# OPTIONS_GHC -fno-warn-missing-signatures -fno-warn-unused-do-bind #-}+{-# LANGUAGE ScopedTypeVariables, DeriveDataTypeable, RecordWildCards, TypeOperators, FlexibleContexts #-}  module Hledger.Cli.Add where+ import Control.Exception as E import Control.Monad import Control.Monad.Trans (liftIO) import Data.Char (toUpper, toLower) import Data.List import Data.Maybe+import Data.Time.Calendar (Day) import Data.Typeable (Typeable)-import Safe (headDef, tailDef, headMay)-import System.Console.Haskeline (InputT, runInputT, defaultSettings, setComplete, getInputLine)+import Safe (headDef, headMay)+import System.Console.CmdArgs.Explicit+import System.Console.Haskeline (runInputT, defaultSettings, setComplete) import System.Console.Haskeline.Completion+import System.Console.Wizard  +import System.Console.Wizard.Haskeline import System.IO ( stderr, hPutStr, hPutStrLn )-import System.IO.Error-import Text.ParserCombinators.Parsec+import Text.ParserCombinators.Parsec hiding (Line) import Text.Printf-import qualified Data.Set as Set  import Hledger-import Prelude hiding (putStr, putStrLn, appendFile)-import Hledger.Utils.UTF8IOCompat (putStr, putStrLn, appendFile) import Hledger.Cli.Options import Hledger.Cli.Register (postingsReportAsText)  +addmode = (defCommandMode ["add"]) {+  modeHelp = "prompt for transactions and add them to the journal"+ ,modeHelpSuffix = ["Defaults come from previous similar transactions; use query patterns to restrict these."]+ ,modeGroupFlags = Group {+     groupUnnamed = [+      flagNone ["no-new-accounts"]  (\opts -> setboolopt "no-new-accounts" opts) "don't allow creating new accounts"+     ]+    ,groupHidden = []+    ,groupNamed = [generalflagsgroup2]+    }+ }++-- | State used while entering transactions.+data EntryState = EntryState {+   esOpts               :: CliOpts           -- ^ command line options+  ,esArgs               :: [String]          -- ^ command line arguments remaining to be used as defaults+  ,esToday              :: Day               -- ^ today's date+  ,esDefDate            :: Day               -- ^ the default date for next transaction+  ,esJournal            :: Journal           -- ^ the journal we are adding to+  ,esSimilarTransaction :: Maybe Transaction -- ^ the most similar historical txn+  ,esPostings           :: [Posting]         -- ^ postings entered so far in the current txn+  } deriving (Show,Typeable)++defEntryState = EntryState {+   esOpts               = defcliopts+  ,esArgs               = []+  ,esToday              = nulldate+  ,esDefDate            = nulldate+  ,esJournal            = nulljournal+  ,esSimilarTransaction = Nothing+  ,esPostings           = []+}++data RestartTransactionException = RestartTransactionException deriving (Typeable,Show)+instance Exception RestartTransactionException++-- data ShowHelpException = ShowHelpException deriving (Typeable,Show)+-- instance Exception ShowHelpException+ -- | Read multiple transactions from the console, prompting for each -- field, and append them to the journal file.  If the journal came -- from stdin, this command has no effect. add :: CliOpts -> Journal -> IO () add opts j-    | f == "-" = return ()+    | journalFilePath j == "-" = return ()     | otherwise = do-  hPutStr stderr $ unlines [-     "Adding transactions to journal file "++f-    ,"Provide field values at the prompts, or press enter to accept defaults."-    ,"Use readline keys to edit, use tab key to complete account names."-    ,"A code (in parentheses) may be entered following transaction dates."-    ,"A comment may be entered following descriptions or amounts."+        hPrintf stderr "Adding transactions to journal file %s\n" (journalFilePath j)+        showHelp+        today <- getCurrentDay+        let es = defEntryState{esOpts=opts+                              ,esArgs=map stripquotes $ listofstringopt "args" $ rawopts_ opts+                              ,esToday=today+                              ,esDefDate=today+                              ,esJournal=j+                              }+        getAndAddTransactions es `E.catch` (\(_::UnexpectedEOF) -> putStr "")++showHelp = hPutStr stderr $ unlines [+     "Any command line arguments will be used as defaults."+    ,"Use tab key to complete, readline keys to edit, enter to accept defaults."+    ,"An optional (CODE) may follow transaction dates."+    ,"An optional ; COMMENT may follow descriptions or amounts."     ,"If you make a mistake, enter < at any prompt to restart the transaction."-    ,"To complete a transaction, enter . when prompted."-    ,"To quit, press control-d or control-c."+    ,"To end a transaction, enter . when prompted."+    ,"To quit, enter . at a date prompt or press control-d or control-c."     ]-  today <- showDate `fmap` getCurrentDay-  let args                = words' $ query_ $ reportopts_ opts-      (defdate, moredefs) = headTailDef today args-  getAndAddTransactionsLoop j opts defdate moredefs-        `E.catch` (\e -> unless (isEOFError e) $ ioError e)-      where f = journalFilePath j --- | Loop reading transactions from the console, prompting for,--- validating, displaying and appending each one to the journal file,--- until end of input or ctrl-c (then raise an EOF exception).--- If provided, command-line arguments are used as defaults for the--- first transaction; otherwise defaults come from the most similar--- recent transaction in the journal.-getAndAddTransactionsLoop :: Journal -> CliOpts -> String -> [String] -> IO ()-getAndAddTransactionsLoop j opts defdate moredefs = do-  hPrintf stderr "\nStarting a new transaction.\n"-  t <- getTransaction j opts defdate moredefs-  j' <- journalAddTransaction j opts t-  hPrintf stderr "Added to the journal.\n"-  let defdate' = showDate $ tdate t-  getAndAddTransactionsLoop j' opts defdate' []---- | Read a single transaction from the console, with history-aware prompting,--- allowing the user to restart and confirm at the end.--- A default date, and zero or more defaults for subsequent fields, are provided.-getTransaction :: Journal -> CliOpts -> String -> [String] -> IO Transaction-getTransaction j opts defdate moredefs = do-  mt <- getTransactionOrRestart j opts defdate moredefs-  let restart = do-        hPrintf stderr "\nRestarting this transaction.\n"-        getTransaction j opts defdate moredefs+-- | Loop reading transactions from the console, prompting, validating+-- and appending each one to the journal file, until end of input or+-- ctrl-c (then raise an EOF exception).  If provided, command-line+-- arguments are used as defaults; otherwise defaults come from the+-- most similar recent transaction in the journal.+getAndAddTransactions :: EntryState -> IO ()+getAndAddTransactions es@EntryState{..} = (do+  mt <- runInputT (setComplete noCompletion defaultSettings) (run $ haskeline $ confirmedTransactionWizard es)   case mt of-    Nothing -> restart-    Just t  -> do-      hPrintf stderr "\nTransaction entered:\n%s" (show t)-      yn <- runInteractionDefault $ askFor "Accept this transaction" (Just "y") (Just $ \s -> map toLower s `elem` ["<","y","yes","n","no"])-      case headMay $ map toLower yn of-        Just 'y' -> return t-        _        -> restart---- | Read a single transaction from the console, with history-aware prompting,--- or return nothing indicating that the user wants to restart entering this transaction.--- A default date, and zero or more defaults for subsequent fields, are provided.-getTransactionOrRestart :: Journal -> CliOpts -> String -> [String] -> IO (Maybe Transaction)-getTransactionOrRestart j opts defdate moredefs = do-  let dateandcodep = do {d <- smartdate; c <- optionMaybe code; many spacenonewline; eof; return (d, fromMaybe "" c)}-  datecodestr <- runInteractionDefault $ askFor "date"-            (Just defdate)-            (Just $ \s -> null s-                         || s == "."-                         || isRight (parseWithCtx nullctx dateandcodep $ lowercase s))-  when (datecodestr == ".") $ ioError $ mkIOError eofErrorType "" Nothing Nothing-  today <- getCurrentDay-  let (sdate,code) = fromparse $ parseWithCtx nullctx dateandcodep datecodestr-      defday = fixSmartDate today $ fromparse $ (parse smartdate "" . lowercase) defdate-      datestr = showDate $ fixSmartDate defday sdate--  let (defdesc, moredefs') = headTailDef "" moredefs-  desc <- runInteractionDefault $ askFor "description" (Just defdesc) Nothing-  if desc == "<"-   then return Nothing-   else do-    let (description,comment) = (strip a, strip $ dropWhile (==';') b) where (a,b) = break (==';') desc-    getPostingsForTransactionWithHistory j opts datestr code description comment moredefs'--data RestartEntryException = RestartEntryException deriving (Typeable,Show)-instance Exception RestartEntryException+    Nothing -> fail "urk ?"+    Just t -> do+      j <- if debug_ esOpts > 0+           then do hPrintf stderr "Skipping journal add due to debug mode.\n"+                   return esJournal+           else do j' <- journalAddTransaction esJournal esOpts t+                   hPrintf stderr "Saved.\n"+                   return j'+      hPrintf stderr "Starting the next transaction (. or ctrl-D/ctrl-C to quit)\n"+      getAndAddTransactions es{esJournal=j, esDefDate=tdate t}+  )+  `E.catch` (\(_::RestartTransactionException) ->+                 hPrintf stderr "Restarting this transaction.\n" >> getAndAddTransactions es) --- | Information used as the basis for suggested account names, amounts etc. in add prompt.-data PostingsState = PostingsState {-   psJournal                 :: Journal-  ,psAccept                  :: AccountName -> Bool-  ,psSuggestHistoricalAmount :: Bool-  ,psHistory                 :: Maybe [Posting]-  }+-- confirmedTransactionWizard :: (ArbitraryIO :<: b, OutputLn :<: b, Line :<: b) => EntryState -> Wizard b Transaction+-- confirmedTransactionWizard :: EntryState -> Wizard Haskeline Transaction+confirmedTransactionWizard es@EntryState{..} = do+  t <- transactionWizard es+  -- liftIO $ hPrintf stderr {- "Transaction entered:\n%s" -} (show t)+  output $ show t+  y <- let def = "y" in+       retryMsg "Please enter y or n." $ +        parser ((fmap ('y' ==)) . headMay . map toLower . strip) $ +        defaultTo' def $ nonEmpty $ +        maybeRestartTransaction $+        line $ green $ printf "Save this transaction to the journal ?%s: " (showDefault def)+  if y then return t else throw RestartTransactionException --- | Loop reading postings from the console, until a valid balanced--- set of postings has been entered, then return the final transaction,--- or nothing indicating that the user wants to restart entering this transaction.-getPostingsForTransactionWithHistory :: Journal -> CliOpts -> String -> String -> String -> String -> [String] -> IO (Maybe Transaction)-getPostingsForTransactionWithHistory j opts datestr code description comment defargs = do-  today <- getCurrentDay-  let historymatches = transactionsSimilarTo j (queryFromOpts today $ reportopts_ opts) description-      bestmatch | not (null defargs) || null historymatches = Nothing-                | otherwise = Just $ snd $ head historymatches-      bestmatchpostings = maybe Nothing (Just . tpostings) bestmatch-      date = fixSmartDate today $ fromparse $ (parse smartdate "" . lowercase) datestr-      accept x = x == "." || (not . null) x &&-        if no_new_accounts_ opts-            then x `elem` existingaccts-            else True-      existingaccts = journalAccountNames j-      getvalidpostings = do-        ps <- getPostingsLoop (PostingsState j accept True bestmatchpostings) [] defargs+transactionWizard es@EntryState{..} = do+  (date,code)    <- dateAndCodeWizard es+  let es1@EntryState{esArgs=args1} = es{esArgs=drop 1 esArgs, esDefDate=date}+  (desc,comment) <- descriptionAndCommentWizard es1+  let mbaset = similarTransaction es1 desc+  when (isJust mbaset) $ liftIO $ hPrintf stderr "Using this similar transaction for defaults:\n%s" (show $ fromJust mbaset)+  let es2 = es1{esArgs=drop 1 args1, esSimilarTransaction=mbaset}+      balancedPostingsWizard = do+        ps <- postingsWizard es2{esPostings=[]}         let t = nulltransaction{tdate=date                                ,tstatus=False                                ,tcode=code-                               ,tdescription=description+                               ,tdescription=desc                                ,tcomment=comment                                ,tpostings=ps                                }-        either retry (return . Just) $ balanceTransaction Nothing t -- imprecise balancing+        case balanceTransaction Nothing t of -- imprecise balancing (?)+          Right t' -> return t'+          Left err -> liftIO (hPutStrLn stderr $ "\n" ++ (capitalize err) ++ "please re-enter.") >> balancedPostingsWizard+  balancedPostingsWizard++-- Identify the closest recent match for this description in past transactions.+similarTransaction :: EntryState -> String -> Maybe Transaction+similarTransaction EntryState{..} desc =+  let q = queryFromOptsOnly esToday $ reportopts_ esOpts+      historymatches = transactionsSimilarTo esJournal q desc+      bestmatch | null historymatches = Nothing+                | otherwise           = Just $ snd $ head historymatches+  in bestmatch++dateAndCodeWizard EntryState{..} = do+  let def = headDef (showDate esDefDate) esArgs+  retryMsg "A valid hledger smart date is required. Eg: 2014/2/14, 14, yesterday." $ +   parser (parseSmartDateAndCode esToday) $ +   withCompletion (dateCompleter def) $+   defaultTo' def $ nonEmpty $ +   maybeExit $+   maybeRestartTransaction $+   -- maybeShowHelp $+   line $ green $ printf "Date%s: " (showDefault def)+    where+      parseSmartDateAndCode refdate s = either (const Nothing) (\(d,c) -> return (fixSmartDate refdate d, c)) edc           where-            retry msg = liftIO (hPutStrLn stderr $ "\n" ++ (capitalize msg) ++ "please re-enter.") >> getvalidpostings+            edc = parseWithCtx nullctx dateandcodep $ lowercase s+            dateandcodep = do+                d <- smartdate+                c <- optionMaybe codep+                many spacenonewline+                eof+                return (d, fromMaybe "" c)+      -- defday = fixSmartDate today $ fromparse $ (parse smartdate "" . lowercase) defdate+      -- datestr = showDate $ fixSmartDate defday smtdate -  when (isJust bestmatch) $ liftIO $ hPrintf stderr "\nUsing this existing transaction for defaults:\n%s" (show $ fromJust bestmatch)-  getvalidpostings `E.catch` \(_::RestartEntryException) -> return Nothing+descriptionAndCommentWizard EntryState{..} = do+  let def = headDef "" esArgs+  s <- withCompletion (descriptionCompleter esJournal def) $+       defaultTo' def $ nonEmpty $ +       maybeRestartTransaction $+       line $ green $ printf "Description%s: " (showDefault def)+  let (desc,comment) = (strip a, strip $ dropWhile (==';') b) where (a,b) = break (==';') s+  return (desc,comment) --- | Read postings from the command line until . is entered, generating--- useful defaults based on historical context and postings entered so far.-getPostingsLoop :: PostingsState -> [Posting] -> [String] -> IO [Posting]-getPostingsLoop st enteredps defargs = do-  let bestmatch | isNothing historicalps = Nothing-                | n <= length ps = Just $ ps !! (n-1)-                | otherwise = Nothing-                where Just ps = historicalps-      bestmatchacct = maybe Nothing (Just . showacctname) bestmatch-      defacct  = maybe bestmatchacct Just $ headMay defargs-      defargs' = tailDef [] defargs-      ordot | null enteredps || length enteredrealps == 1 = "" :: String-            | otherwise = " (or . to complete this transaction)"-  account <- runInteraction j $ askFor (printf "account %d%s" n ordot) defacct (Just accept)-  when (account=="<") $ throwIO RestartEntryException-  if account=="."-    then-     if null enteredps-      then do hPutStrLn stderr $ "\nPlease enter some postings first."-              getPostingsLoop st enteredps defargs-      else return enteredps-    else do-      let defacctused = Just account == defacct-          historicalps' = if defacctused then historicalps else Nothing-          bestmatch' | isNothing historicalps' = Nothing-                     | n <= length ps = Just $ ps !! (n-1)-                     | otherwise = Nothing-                     where Just ps = historicalps'-          defamountstr | isJust commandlineamt                  = commandlineamt-                       | isJust bestmatch' && suggesthistorical = Just historicalamountstr-                       | n > 1                                  = Just balancingamountstr-                       | otherwise                              = Nothing-              where-                commandlineamt      = headMay defargs'-                historicalamountstr = showMixedAmountWithPrecision p $ pamount $ fromJust bestmatch'-                balancingamountstr  = showMixedAmountWithPrecision p $ negate $ sum $ map pamount enteredrealps-                -- what should this be ?-                -- 1 maxprecision (show all decimal places or none) ?-                -- 2 maxprecisionwithpoint (show all decimal places or .0 - avoids some but not all confusion with thousands separators) ?-                -- 3 canonical precision for this commodity in the journal ?-                -- 4 maximum precision entered so far in this transaction ?-                -- 5 3 or 4, whichever would show the most decimal places ?-                -- I think 1 or 4, whichever would show the most decimal places-                p = maxprecisionwithpoint-          defargs'' = tailDef [] defargs'-      amt <- runInteractionDefault $ askFor (printf "amount  %d" n) defamountstr validateamount-      when (amt=="<") $ throwIO RestartEntryException-      let (amountstr,comment) = (strip a, strip $ dropWhile (==';') b) where (a,b) = break (==';') amt-      let a  = fromparse $ runParser (amountp <|> return missingamt) ctx     "" amountstr-          a' = fromparse $ runParser (amountp <|> return missingamt) nullctx "" amountstr-          wasdefamtused = Just (showAmount a) == defamountstr-          defcommodityadded | acommodity a == acommodity a' = Nothing-                                | otherwise                     = Just $ acommodity a-          p = nullposting{paccount=stripbrackets account-                         ,pamount=mixed a-                         ,pcomment=comment-                         ,ptype=postingtype account-                         }-          st' = if wasdefamtused-                 then st-                 else st{psHistory=historicalps', psSuggestHistoricalAmount=False}-      when (isJust defcommodityadded) $-           liftIO $ hPutStrLn stderr $ printf "using default commodity (%s)" (fromJust defcommodityadded)-      getPostingsLoop st' (enteredps ++ [p]) defargs''+postingsWizard es@EntryState{..} = do+  mp <- postingWizard es+  case mp of Nothing -> return esPostings+             Just p  -> postingsWizard es{esArgs=drop 2 esArgs, esPostings=esPostings++[p]}++postingWizard es@EntryState{..} = do+  acct <- accountWizard es+  if acct `elem` [".",""]+  then case (esPostings, postingsBalanced esPostings) of+         ([],_)    -> liftIO (hPutStrLn stderr "Please enter some postings first.") >> postingWizard es+         (_,False) -> liftIO (hPutStrLn stderr "Please enter more postings to balance the transaction.") >> postingWizard es+         (_,True)  -> return Nothing -- no more postings, end of transaction+  else do+    let es1 = es{esArgs=drop 1 esArgs}+    (amt,comment)  <- amountAndCommentWizard es1+    return $ Just nullposting{paccount=stripbrackets acct+                             ,pamount=mixed amt+                             ,pcomment=comment+                             ,ptype=accountNamePostingType acct+                             }++postingsBalanced :: [Posting] -> Bool+postingsBalanced ps = isRight $ balanceTransaction Nothing nulltransaction{tpostings=ps}++accountWizard EntryState{..} = do+  let pnum = length esPostings + 1+      historicalp = maybe Nothing (Just . (!! (pnum-1)) . (++ (repeat nullposting)) . tpostings) esSimilarTransaction+      historicalacct = case historicalp of Just p  -> showAccountName Nothing (ptype p) (paccount p)+                                           Nothing -> ""+      def = headDef historicalacct esArgs+      endmsg | canfinish && null def = " (or . or enter to finish this transaction)"+             | canfinish             = " (or . to finish this transaction)"+             | otherwise             = ""+  retryMsg "A valid hledger account name is required. Eg: assets:cash, expenses:food:eating out." $+   parser (parseAccountOrDotOrNull def canfinish) $+   withCompletion (accountCompleter esJournal def) $+   defaultTo' def $ -- nonEmpty $ +   maybeRestartTransaction $+   line $ green $ printf "Account %d%s%s: " pnum (endmsg::String) (showDefault def)     where-      j = psJournal st-      historicalps = psHistory st-      ctx = jContext j-      accept = psAccept st-      suggesthistorical = psSuggestHistoricalAmount st-      n = length enteredps + 1-      enteredrealps = filter isReal enteredps-      showacctname p = showAccountName Nothing (ptype p) $ paccount p-      postingtype ('[':_) = BalancedVirtualPosting-      postingtype ('(':_) = VirtualPosting-      postingtype _ = RegularPosting-      validateamount = Just $ \s -> (null s && not (null enteredrealps))-                                    || s == "<"-                                    || (isRight (runParser (amountp >> many spacenonewline >> optional (char ';' >> many anyChar) >> eof) ctx "" s)-                                        && s /= ".")+      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 $ parseWithCtx (jContext esJournal) accountnamep s -- otherwise, try to parse the input as an accountname+      dbg = id -- strace+      validateAccount s | no_new_accounts_ esOpts && not (s `elem` journalAccountNames esJournal) = Nothing+                        | otherwise = Just s --- | Prompt for and read a string value, optionally with a default value--- and a validator. A validator causes the prompt to repeat until the--- input is valid. May also raise an EOF exception if control-d or control-c is pressed.-askFor :: String -> Maybe String -> Maybe (String -> Bool) -> InputT IO String-askFor prompt def validator = do-  l <- fmap (maybe eofErr id)-            $ getInputLine $ prompt ++ " ? " ++ maybe "" showdef def ++ ": "-  let input = if null l then fromMaybe l def else l-  case validator of-    Just valid -> if valid input-                   then return input-                   else askFor prompt def validator-    Nothing -> return input+amountAndCommentWizard EntryState{..} = do+  let pnum = length esPostings + 1+      (mhistoricalp,followedhistoricalsofar) =+          case esSimilarTransaction of+            Nothing                        -> (Nothing,False)+            Just Transaction{tpostings=ps} -> (if length ps >= pnum then Just (ps !! (pnum-1)) else Nothing+                                              ,all (\(a,b) -> pamount a == pamount b) $ zip esPostings ps)+      def = case (esArgs, mhistoricalp, followedhistoricalsofar) of+              (d:_,_,_)                                             -> d+              (_,Just hp,True)                                      -> showamt $ pamount hp+              _  | pnum > 1 && not (isZeroMixedAmount balancingamt) -> showamt balancingamt+              _                                                     -> ""+  retryMsg "A valid hledger amount is required. Eg: 1, $2, 3 EUR, \"4 red apples\"." $+   parser parseAmountAndComment $ +   withCompletion (amountCompleter def) $+   defaultTo' def $ nonEmpty $ +   maybeRestartTransaction $+   line $ green $ printf "Amount  %d%s: " pnum (showDefault def)+    where  +      parseAmountAndComment = either (const Nothing) Just . parseWithCtx (jContext esJournal) amountandcommentp+      amountandcommentp = do+        a <- amountp+        many spacenonewline+        c <- fromMaybe "" `fmap` optionMaybe (char ';' >> many anyChar)+        -- eof+        return (a,c)+      balancingamt = negate $ sum $ map pamount realps where realps = filter isReal esPostings+      showamt = showMixedAmountWithPrecision+                  -- what should this be ?+                  -- 1 maxprecision (show all decimal places or none) ?+                  -- 2 maxprecisionwithpoint (show all decimal places or .0 - avoids some but not all confusion with thousands separators) ?+                  -- 3 canonical precision for this commodity in the journal ?+                  -- 4 maximum precision entered so far in this transaction ?+                  -- 5 3 or 4, whichever would show the most decimal places ?+                  -- I think 1 or 4, whichever would show the most decimal places+                  maxprecisionwithpoint+  --+  -- let -- (amt,comment) = (strip a, strip $ dropWhile (==';') b) where (a,b) = break (==';') amtcmt+      -- a           = fromparse $ runParser (amountp <|> return missingamt) (jContext esJournal) "" amt+  --     awithoutctx = fromparse $ runParser (amountp <|> return missingamt) nullctx              "" amt+  --     defamtaccepted = Just (showAmount a) == mdefamt+  --     es2 = if defamtaccepted then es1 else es1{esHistoricalPostings=Nothing}+  --     mdefaultcommodityapplied = if acommodity a == acommodity awithoutctx then Nothing else Just $ acommodity a+  -- when (isJust mdefaultcommodityapplied) $+  --      liftIO $ hPutStrLn stderr $ printf "using default commodity (%s)" (fromJust mdefaultcommodityapplied)++maybeExit = parser (\s -> if s=="." then throw UnexpectedEOF else Just s)++maybeRestartTransaction = parser (\s -> if s=="<" then throw RestartTransactionException else Just s)++-- maybeShowHelp :: Wizard Haskeline String -> Wizard Haskeline String+-- maybeShowHelp wizard = maybe (liftIO showHelp >> wizard) return $ +--                        parser (\s -> if s=="?" then Nothing else Just s) wizard++-- Completion helpers++dateCompleter :: String -> CompletionFunc IO+dateCompleter = completer ["today","tomorrow","yesterday"]++descriptionCompleter :: Journal -> String -> CompletionFunc IO+descriptionCompleter j = completer (journalDescriptions j)++accountCompleter :: Journal -> String -> CompletionFunc IO+accountCompleter j = completer (journalAccountNamesUsed j)++amountCompleter :: String -> CompletionFunc IO+amountCompleter = completer []++-- | Generate a haskeline completion function from the given+-- completions and default, that case insensitively completes with+-- prefix matches, or infix matches above a minimum length, or+-- completes the null string with the default.+completer :: [String] -> String -> CompletionFunc IO+completer completions def = completeWord Nothing "" completionsFor     where-        showdef "" = ""-        showdef s = "[" ++ s ++ "]"-        eofErr = E.throw $ mkIOError eofErrorType "end of input" Nothing Nothing+      simpleCompletion' s = (simpleCompletion s){isFinished=False}+      completionsFor "" = return [simpleCompletion' def]+      completionsFor i  = return (map simpleCompletion' ciprefixmatches)+          where+            ciprefixmatches = [c | c <- completions, i `isPrefixOf` c]+            -- mixed-case completions require haskeline > 0.7.1.2+            -- ciprefixmatches = [c | c <- completions, lowercase i `isPrefixOf` lowercase c] --- | Append this transaction to the journal's file, and to the journal's--- transaction list.+--------------------------------------------------------------------------------++-- utilities++defaultTo' = flip defaultTo++withCompletion f = withSettings (setComplete f defaultSettings)++green s = "\ESC[1;32m\STX"++s++"\ESC[0m\STX"++showDefault "" = ""+showDefault s = " [" ++ s ++ "]"++-- | Append this transaction to the journal's file and transaction list. journalAddTransaction :: Journal -> CliOpts -> Transaction -> IO Journal journalAddTransaction j@Journal{jtxns=ts} opts t = do   let f = journalFilePath j@@ -292,6 +375,28 @@         ropts = defreportopts{empty_=True}         opts = defcliopts{reportopts_=ropts} +capitalize :: String -> String+capitalize "" = ""+capitalize (c:cs) = toUpper c : cs++-- Find the most similar and recent transactions matching the given transaction description and report query.+-- Transactions are listed with their "relevancy" score, most relevant first.+transactionsSimilarTo :: Journal -> Query -> String -> [(Double,Transaction)]+transactionsSimilarTo j q desc =+    sortBy compareRelevanceAndRecency+               $ filter ((> threshold).fst)+               [(compareDescriptions desc $ tdescription t, t) | t <- ts]+    where+      compareRelevanceAndRecency (n1,t1) (n2,t2) = compare (n2,tdate t2) (n1,tdate t1)+      ts = filter (q `matchesTransaction`) $ jtxns j+      threshold = 0++compareDescriptions :: [Char] -> [Char] -> Double+compareDescriptions s t = compareStrings s' t'+    where s' = simplify s+          t' = simplify t+          simplify = filter (not . (`elem` "0123456789"))+ -- | Return a similarity measure, from 0 to 1, for two strings. -- This is Simon White's letter pairs algorithm from -- http://www.catalysoft.com/articles/StrikeAMatch.html@@ -307,54 +412,8 @@       u = length pairs1 + length pairs2       pairs1 = wordLetterPairs $ uppercase s1       pairs2 = wordLetterPairs $ uppercase s2+ wordLetterPairs = concatMap letterPairs . words+ letterPairs (a:b:rest) = [a,b] : letterPairs (b:rest) letterPairs _ = []--compareDescriptions :: [Char] -> [Char] -> Double-compareDescriptions s t = compareStrings s' t'-    where s' = simplify s-          t' = simplify t-          simplify = filter (not . (`elem` "0123456789"))--transactionsSimilarTo :: Journal -> Query -> String -> [(Double,Transaction)]-transactionsSimilarTo j q s =-    sortBy compareRelevanceAndRecency-               $ filter ((> threshold).fst)-               [(compareDescriptions s $ tdescription t, t) | t <- ts]-    where-      compareRelevanceAndRecency (n1,t1) (n2,t2) = compare (n2,tdate t2) (n1,tdate t1)-      ts = filter (q `matchesTransaction`) $ jtxns j-      threshold = 0--runInteraction :: Journal -> InputT IO a -> IO a-runInteraction j m = do-    let cc = completionCache j-    runInputT (setComplete (accountCompletion cc) defaultSettings) m--runInteractionDefault :: InputT IO a -> IO a-runInteractionDefault m = do-    runInputT (setComplete noCompletion defaultSettings) m---- A precomputed list of all accounts previously entered into the journal.-type CompletionCache = [AccountName]--completionCache :: Journal -> CompletionCache-completionCache j = -- Only keep unique account names.-                    Set.toList $ Set.fromList-                        [paccount p | t <- jtxns j, p <- tpostings t]--accountCompletion :: CompletionCache -> CompletionFunc IO-accountCompletion cc = completeWord Nothing-                        "" -- don't break words on whitespace, since account names-                           -- can contain spaces.-                        $ \s -> return $ map simpleCompletion-                                        $ filter (s `isPrefixOf`) cc--capitalize :: String -> String-capitalize "" = ""-capitalize (c:cs) = toUpper c : cs--headTailDef :: a -> [a] -> (a,[a])-headTailDef defhead as = (headDef defhead as, tailDef [] as)-
Hledger/Cli/Balance.hs view
@@ -233,7 +233,8 @@ -}  module Hledger.Cli.Balance (-  balance+  balancemode+ ,balance  ,balanceReportAsText  ,periodBalanceReportAsText  ,cumulativeBalanceReportAsText@@ -243,18 +244,41 @@  import Data.List import Data.Maybe+-- import System.Console.CmdArgs+import System.Console.CmdArgs.Explicit as C+-- import System.Console.CmdArgs.Text import Test.HUnit-import Text.Tabular+import Text.Tabular as T import Text.Tabular.AsciiArt  import Hledger import Prelude hiding (putStr) import Hledger.Utils.UTF8IOCompat (putStr)-import Hledger.Data.FormatStrings+import Hledger.Data.OutputFormat import Hledger.Cli.Options  --- | Print a balance report.+-- | Command line options for this command.+balancemode = (defCommandMode $ ["balance"] ++ aliases) { -- also accept but don't show the common bal alias+  modeHelp = "show accounts and balances" `withAliases` aliases+ ,modeGroupFlags = C.Group {+     groupUnnamed = [+      flagNone ["tree"] (\opts -> setboolopt "tree" opts) "show accounts as a tree (default in simple reports)"+     ,flagNone ["flat"] (\opts -> setboolopt "flat" opts) "show accounts as a list (default in multicolumn)"+     ,flagReq  ["drop"] (\s opts -> Right $ setopt "drop" s opts) "N" "flat mode: omit N leading account name parts"+     ,flagReq  ["format"] (\s opts -> Right $ setopt "format" s opts) "FORMATSTR" "tree mode: use this custom line format"+     ,flagNone ["no-elide"] (\opts -> setboolopt "no-elide" opts) "tree mode: don't squash boring parent accounts"+     ,flagNone ["no-total"] (\opts -> setboolopt "no-total" opts) "don't show the final total"+     ,flagNone ["cumulative"] (\opts -> setboolopt "cumulative" opts) "multicolumn mode: show accumulated ending balances"+     ,flagNone ["historical","H"] (\opts -> setboolopt "historical" opts) "multicolumn mode: show historical ending balances"+     ]+    ,groupHidden = []+    ,groupNamed = [generalflagsgroup1]+    }+ }+  where aliases = ["bal"]++-- | The balance command, prints a balance report. balance :: CliOpts -> Journal -> IO () balance CliOpts{reportopts_=ropts} j = do   d <- getCurrentDay@@ -263,11 +287,10 @@          Left err -> [err]          Right _ ->           case (intervalFromOpts ropts, balancetype_ ropts) of-            (NoInterval,_)        -> balanceReportAsText           ropts $ balanceReport ropts (queryFromOpts d ropts) j-            (_,PeriodBalance)     -> periodBalanceReportAsText     ropts $ periodBalanceReport                 ropts (queryFromOpts d ropts) j-            (_,CumulativeBalance) -> cumulativeBalanceReportAsText ropts $ cumulativeOrHistoricalBalanceReport ropts (queryFromOpts d ropts) j-            (_,HistoricalBalance) -> historicalBalanceReportAsText ropts $ cumulativeOrHistoricalBalanceReport ropts (queryFromOpts d ropts) j-+            (NoInterval,_)        -> balanceReportAsText           ropts  $ balanceReport ropts (queryFromOpts d ropts) j+            (_,PeriodBalance)     -> periodBalanceReportAsText     ropts $ multiBalanceReport ropts (queryFromOpts d ropts) j+            (_,CumulativeBalance) -> cumulativeBalanceReportAsText ropts $ multiBalanceReport ropts (queryFromOpts d ropts) j+            (_,HistoricalBalance) -> historicalBalanceReportAsText ropts $ multiBalanceReport ropts (queryFromOpts d ropts) j   putStr $ unlines output  -- | Render an old-style single-column balance report as plain text.@@ -309,8 +332,8 @@     b         USD -1  ; Account 'b' has two amounts. The account name is printed on the last line. -} -- | Render one balance report line item as plain text suitable for console output.-balanceReportItemAsText :: ReportOpts -> [FormatString] -> BalanceReportItem -> [String]-balanceReportItemAsText opts format (_, accountName, depth, Mixed amounts) =+balanceReportItemAsText :: ReportOpts -> [OutputFormat] -> BalanceReportItem -> [String]+balanceReportItemAsText opts format ((_, accountName, depth), Mixed amounts) =     -- 'amounts' could contain several quantities of the same commodity with different price.     -- In order to combine them into single value (which is expected) we take the first price and     -- use it for the whole mixed amount. This could be suboptimal. XXX@@ -325,7 +348,7 @@       multiline [a]    = [formatBalanceReportItem opts (Just accountName) depth a format]       multiline (a:as) = (formatBalanceReportItem opts Nothing depth a format) : multiline as -formatBalanceReportItem :: ReportOpts -> Maybe AccountName -> Int -> Amount -> [FormatString] -> String+formatBalanceReportItem :: ReportOpts -> Maybe AccountName -> Int -> Amount -> [OutputFormat] -> String formatBalanceReportItem _ _ _ _ [] = "" formatBalanceReportItem opts accountName depth amount (fmt:fmts) =   s ++ (formatBalanceReportItem opts accountName depth amount fmts)@@ -353,8 +376,8 @@     ((" "++) . showDateSpan)     showMixedAmountWithoutPrice     $ Table-      (Group NoLine $ map (Header . padright acctswidth) accts)-      (Group NoLine $ map Header colspans)+      (T.Group NoLine $ map (Header . padright acctswidth) accts)+      (T.Group NoLine $ map Header colspans)       (map snd items')     +----+     totalrow@@ -363,9 +386,9 @@     items' | empty_ opts = items            | otherwise   = items -- dbg "2" $ filter (any (not . isZeroMixedAmount) . snd) $ dbg "1" items     accts = map renderacct items'-    renderacct ((a,a',_i),_)-      | flat_ opts = a-      | otherwise  = a' -- replicate i ' ' ++ +    renderacct ((a,a',i),_)+      | tree_ opts = replicate ((i-1)*2) ' ' ++ a'+      | otherwise  = accountNameDrop (drop_ opts) a     acctswidth = maximum $ map length $ accts     totalrow | no_total_ opts = row "" []              | otherwise      = row "" coltotals@@ -378,15 +401,15 @@    render id ((" "++) . maybe "" (showDate . prevday) . spanEnd) showMixedAmountWithoutPrice $     addtotalrow $       Table-       (Group NoLine $ map (Header . padright acctswidth) accts)-       (Group NoLine $ map Header colspans)+       (T.Group NoLine $ map (Header . padright acctswidth) accts)+       (T.Group NoLine $ map Header colspans)        (map snd items)   where     trimborder = ("":) . (++[""]) . drop 1 . init . map (drop 1 . init)     accts = map renderacct items-    renderacct ((a,a',_),_)-      | flat_ opts = a-      | otherwise  = a' -- replicate i ' ' ++ +    renderacct ((a,a',i),_)+      | tree_ opts = replicate ((i-1)*2) ' ' ++ a'+      | otherwise  = accountNameDrop (drop_ opts) a     acctswidth = maximum $ map length $ accts     addtotalrow | no_total_ opts = id                 | otherwise      = (+----+ row "" coltotals)@@ -399,15 +422,15 @@    render id ((" "++) . maybe "" (showDate . prevday) . spanEnd) showMixedAmountWithoutPrice $     addtotalrow $       Table-       (Group NoLine $ map (Header . padright acctswidth) accts)-       (Group NoLine $ map Header colspans)+       (T.Group NoLine $ map (Header . padright acctswidth) accts)+       (T.Group NoLine $ map Header colspans)        (map snd items)   where     trimborder = ("":) . (++[""]) . drop 1 . init . map (drop 1 . init)     accts = map renderacct items-    renderacct ((a,a',_),_)-      | flat_ opts = a-      | otherwise  = a' -- replicate i ' ' ++ +    renderacct ((a,a',i),_)+      | tree_ opts = replicate ((i-1)*2) ' ' ++ a'+      | otherwise  = accountNameDrop (drop_ opts) a     acctswidth = maximum $ map length $ accts     addtotalrow | no_total_ opts = id                 | otherwise      = (+----+ row "" coltotals)
Hledger/Cli/Balancesheet.hs view
@@ -6,11 +6,13 @@ -}  module Hledger.Cli.Balancesheet (-  balancesheet+  balancesheetmode+ ,balancesheet  ,tests_Hledger_Cli_Balancesheet ) where  import qualified Data.Text.Lazy.IO as LT+import System.Console.CmdArgs.Explicit import Test.HUnit import Text.Shakespeare.Text @@ -19,6 +21,20 @@ import Hledger.Cli.Balance  +balancesheetmode :: Mode RawOpts+balancesheetmode = (defCommandMode $ ["balancesheet"]++aliases) {+  modeHelp = "show a balance sheet" `withAliases` aliases+ ,modeGroupFlags = Group {+     groupUnnamed = [+      flagNone ["flat"] (\opts -> setboolopt "flat" opts) "show accounts as a list"+     ,flagReq  ["drop"] (\s opts -> Right $ setopt "drop" s opts) "N" "flat mode: omit N leading account name parts"+     ]+    ,groupHidden = []+    ,groupNamed = [generalflagsgroup1]+    }+ }+  where aliases = ["bs"]+ -- | Print a simple balance sheet. balancesheet :: CliOpts -> Journal -> IO () balancesheet CliOpts{reportopts_=ropts} j = do@@ -34,7 +50,6 @@ #{unlines $ balanceReportAsText ropts assetreport} Liabilities: #{unlines $ balanceReportAsText ropts liabilityreport}- Total: -------------------- #{padleft 20 $ showMixedAmountWithoutPrice total}@@ -45,6 +60,7 @@   where p = case period_ of Nothing -> Nothing                             Just (i, DateSpan _ e) -> Just (i, DateSpan Nothing e) +tests_Hledger_Cli_Balancesheet :: Test tests_Hledger_Cli_Balancesheet = TestList  [  ]
Hledger/Cli/Cashflow.hs view
@@ -9,11 +9,13 @@ -}  module Hledger.Cli.Cashflow (-  cashflow+  cashflowmode+ ,cashflow  ,tests_Hledger_Cli_Cashflow ) where  import qualified Data.Text.Lazy.IO as LT+import System.Console.CmdArgs.Explicit import Test.HUnit import Text.Shakespeare.Text @@ -22,6 +24,19 @@ import Hledger.Cli.Balance  +cashflowmode :: Mode RawOpts+cashflowmode = (defCommandMode ["cashflow","cf"]) {+  modeHelp = "show a cashflow statement" `withAliases` ["cf"]+ ,modeGroupFlags = Group {+     groupUnnamed = [+      flagNone ["flat"] (\opts -> setboolopt "flat" opts) "show accounts as a list"+     ,flagReq  ["drop"] (\s opts -> Right $ setopt "drop" s opts) "N" "flat mode: omit N leading account name parts"+     ]+    ,groupHidden = []+    ,groupNamed = [generalflagsgroup1]+    }+ }+ -- | Print a simple cashflow statement. cashflow :: CliOpts -> Journal -> IO () cashflow CliOpts{reportopts_=ropts} j = do@@ -37,12 +52,12 @@  Cash flows: #{unlines $ balanceReportAsText ropts cashreport}- Total: -------------------- #{padleft 20 $ showMixedAmountWithoutPrice total} |] +tests_Hledger_Cli_Cashflow :: Test tests_Hledger_Cli_Cashflow = TestList  [  ]
Hledger/Cli/Histogram.hs view
@@ -1,6 +1,6 @@ {-|  -Print a histogram report.+Print a histogram report. (The "activity" command).  -} @@ -9,6 +9,7 @@ import Data.List import Data.Maybe import Data.Ord+import System.Console.CmdArgs.Explicit import Text.Printf  import Hledger.Cli.Options@@ -19,6 +20,17 @@ import Hledger.Utils.UTF8IOCompat (putStr)  +activitymode = (defCommandMode $ ["activity"] ++ aliases) {+  modeHelp = "show an ascii barchart of posting counts per interval (default: daily)" `withAliases` aliases+ ,modeHelpSuffix = []+ ,modeGroupFlags = Group {+     groupUnnamed = []+    ,groupHidden = []+    ,groupNamed = [generalflagsgroup1]+    }+ }+  where aliases = []+ barchar = '*'  -- | Print a histogram of some statistic per reporting interval, such as@@ -34,7 +46,7 @@       i = intervalFromOpts opts       interval | i == NoInterval = Days 1                | otherwise = i-      span = queryDateSpan (date2_ opts) q `orDatesFrom` journalDateSpan j+      span = queryDateSpan (date2_ opts) q `spanDefaultsFrom` journalDateSpan (date2_ opts) j       spans = filter (DateSpan Nothing Nothing /=) $ splitSpan interval span       spanps = [(s, filter (isPostingInDateSpan s) ps) | s <- spans]       -- same as Register@@ -45,6 +57,6 @@           | queryEmpty q = id           | otherwise = filter (not . isZeroMixedAmount . pamount) -printDayWith f (DateSpan b _, ts) = printf "%s %s\n" (show $ fromJust b) (f ts)+printDayWith f (DateSpan b _, ps) = printf "%s %s\n" (show $ fromJust b) (f ps)  countBar ps = replicate (length ps) barchar
Hledger/Cli/Incomestatement.hs view
@@ -6,11 +6,13 @@ -}  module Hledger.Cli.Incomestatement (-  incomestatement+  incomestatementmode+ ,incomestatement  ,tests_Hledger_Cli_Incomestatement ) where  import qualified Data.Text.Lazy.IO as LT+import System.Console.CmdArgs.Explicit import Test.HUnit import Text.Shakespeare.Text @@ -18,6 +20,21 @@ import Hledger.Cli.Options import Hledger.Cli.Balance ++incomestatementmode :: Mode RawOpts+incomestatementmode = (defCommandMode $ ["incomestatement"]++aliases) {+  modeHelp = "show an income statement" `withAliases` aliases+ ,modeGroupFlags = Group {+     groupUnnamed = [+      flagNone ["flat"] (\opts -> setboolopt "flat" opts) "show accounts as a list"+     ,flagReq  ["drop"] (\s opts -> Right $ setopt "drop" s opts) "N" "flat mode: omit N leading account name parts"+     ]+    ,groupHidden = []+    ,groupNamed = [generalflagsgroup1]+    }+ }+  where aliases = ["is"]+ -- | Print a simple income statement. incomestatement :: CliOpts -> Journal -> IO () incomestatement CliOpts{reportopts_=ropts} j = do@@ -32,7 +49,6 @@ #{unlines $ balanceReportAsText ropts incomereport} Expenses: #{unlines $ balanceReportAsText ropts expensereport}- Total: -------------------- #{padleft 20 $ showMixedAmountWithoutPrice total}
Hledger/Cli/Main.hs view
@@ -38,18 +38,20 @@  module Hledger.Cli.Main where -import Control.Monad+-- import Control.Monad+import Data.Char (isDigit) import Data.List import Safe-import System.Console.CmdArgs.Explicit (modeHelp)--- import System.Console.CmdArgs.Helper+import System.Console.CmdArgs.Explicit as C import System.Environment import System.Exit+import System.FilePath import System.Process import Text.Printf  import Hledger (ensureJournalFileExists) import Hledger.Cli.Add+import Hledger.Cli.Accounts import Hledger.Cli.Balance import Hledger.Cli.Balancesheet import Hledger.Cli.Cashflow@@ -62,10 +64,119 @@ import Hledger.Cli.Tests import Hledger.Cli.Utils import Hledger.Cli.Version+import Hledger.Data.Dates (getCurrentDay)+import Hledger.Data.RawOptions (RawOpts, optserror)+import Hledger.Reports.ReportOptions (dateSpanFromOpts, intervalFromOpts, queryFromOpts) import Hledger.Utils-import Hledger.Reports-import Hledger.Data.Dates ++-- | The overall cmdargs mode describing command-line options for hledger.+mainmode addons = defMode {+  modeNames = [progname]+ ,modeHelp = unlines []+ ,modeHelpSuffix = [""]+ ,modeArgs = ([], Just $ argsFlag "[ARGS]")+ ,modeGroupModes = Group {+    -- modes (commands) in named groups:+    groupNamed = [+      ("Data entry commands", [+        addmode+       ])+     ,("\nReporting commands", [+        printmode+       ,accountsmode+       ,balancemode+       ,registermode+       ,incomestatementmode+       ,balancesheetmode+       ,cashflowmode+       ,activitymode+       ,statsmode+       ])+     ]+     ++ case addons of [] -> []+                       cs -> [("\nAdd-on commands", map defAddonCommandMode cs)]+    -- modes in the unnamed group, shown first without a heading:+   ,groupUnnamed = [+     ]+    -- modes handled but not shown+   ,groupHidden = [+        testmode+       ,oldconvertmode+     ]+   }+ ,modeGroupFlags = Group {+     -- flags in named groups:+     groupNamed = [generalflagsgroup3]+     -- flags in the unnamed group, shown last without a heading:+    ,groupUnnamed = []+     -- flags accepted but not shown in the help:+    ,groupHidden = inputflags -- included here so they'll not raise a confusing error if present with no COMMAND+    }+ }++oldconvertmode = (defCommandMode ["convert"]) {+  modeValue = [("command","convert")]+ ,modeHelp = "convert is no longer needed, just use -f FILE.csv"+ ,modeArgs = ([], Just $ argsFlag "[CSVFILE]")+ ,modeGroupFlags = Group {+     groupUnnamed = []+    ,groupHidden = helpflags+    ,groupNamed = []+    }+ }++builtinCommands :: [Mode RawOpts]+builtinCommands =+  let gs = modeGroupModes $ mainmode []+  in concatMap snd (groupNamed gs) ++ groupUnnamed gs ++ groupHidden gs++builtinCommandNames :: [String]+builtinCommandNames = concatMap modeNames builtinCommands++-- | Parse hledger CLI options from these command line arguments and+-- add-on command names, or raise any error.+argsToCliOpts :: [String] -> [String] -> IO CliOpts+argsToCliOpts args addons = do+  let+    args'        = moveFlagsAfterCommand args+    cmdargsopts  = processValue (mainmode addons) args'+    cmdargsopts' = decodeRawOpts cmdargsopts+  rawOptsToCliOpts cmdargsopts' >>= checkCliOpts++-- | A hacky workaround for cmdargs not accepting flags before the+-- subcommand name: try to detect and move such flags after the+-- command.  This allows the user to put them in either position.+-- The order of options is not preserved, but this should be ok.+--+-- Since we're not parsing flags as precisely as cmdargs here, this is+-- imperfect. We make a decent effort to:+-- - move all no-argument help and input flags+-- - move all required-argument help and input flags along with their values, space-separated or not+-- - not confuse things further or cause misleading errors.+moveFlagsAfterCommand :: [String] -> [String]+moveFlagsAfterCommand args = move args+  where+    move (f:a:as)           | isMovableNoArgFlag f           = (move $ a:as) ++ [f]+    move (f:v:a:as)         | isMovableReqArgFlag f          = (move $ a:as) ++ [f,v]+    move (fv:a:as)          | isMovableReqArgFlagAndValue fv = (move $ a:as) ++ [fv]+    move ("--debug":v:a:as) | not (null v) && all isDigit v  = (move $ a:as) ++ ["--debug",v]+    move ("--debug":a:as)                                    = (move $ a:as) ++ ["--debug"]+    move (fv@('-':'-':'d':'e':'b':'u':'g':'=':_):a:as)       = (move $ a:as) ++ [fv]+    move as = as++    isMovableNoArgFlag a  = "-" `isPrefixOf` a && dropWhile (=='-') a `elem` noargflagstomove+    isMovableReqArgFlag a = "-" `isPrefixOf` a && dropWhile (=='-') a `elem` reqargflagstomove+    isMovableReqArgFlagAndValue ('-':'-':a:as) = case break (== '=') (a:as) of (f:fs,_) -> (f:fs) `elem` reqargflagstomove+                                                                               _        -> False+    isMovableReqArgFlagAndValue ('-':f:_:_) = [f] `elem` reqargflagstomove+    isMovableReqArgFlagAndValue _ = False++    noargflagstomove  = concatMap flagNames $ filter ((==FlagNone).flagInfo) flagstomove+    reqargflagstomove = concatMap flagNames $ filter ((==FlagReq ).flagInfo) flagstomove+    flagstomove = inputflags ++ helpflags++-- | Let's go. main :: IO () main = do @@ -86,59 +197,63 @@     isNullCommand        = null rawcmd     (argsbeforecmd, argsaftercmd') = break (==rawcmd) args     argsaftercmd         = drop 1 argsaftercmd'-  when (debugLevel > 0) $ do-    printf "running: %s\n" prognameandversion-    printf "raw args: %s\n" (show args)-    printf "raw args rearranged for cmdargs: %s\n" (show args')-    printf "raw command might be: %s\n" (show rawcmd)-    printf "raw args before command: %s\n" (show argsbeforecmd)-    printf "raw args after command: %s\n" (show argsaftercmd)+    dbgM :: Show a => String -> a -> IO ()+    dbgM = dbgAtM 2 -  -- search PATH for add-ons-  addons <- getHledgerAddonCommands+  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 +  -- Search PATH for add-ons, excluding any that match built-in names.+  -- The precise addon names (including file extension) are used for command+  -- parsing, and the display names are used for displaying the commands list.+  (addonPreciseNames', addonDisplayNames') <- hledgerAddons+  let addonPreciseNames = filter (not . (`elem` builtinCommandNames) . dropExtension) addonPreciseNames'+  let addonDisplayNames = filter (not . (`elem` builtinCommandNames)) addonDisplayNames'+   -- parse arguments with cmdargs-  opts <- argsToCliOpts args addons+  opts <- argsToCliOpts args addonPreciseNames    -- select an action and run it.   let     cmd                  = command_ opts -- the full matched internal or external command name, if any-    isInternalCommand    = not (null cmd) && not (cmd `elem` addons) -- probably-    isExternalCommand    = not (null cmd) && cmd `elem` addons -- probably+    isInternalCommand    = cmd `elem` builtinCommandNames -- not (null cmd) && not (cmd `elem` addons)+    isExternalCommand    = not (null cmd) && cmd `elem` addonPreciseNames -- probably     isBadCommand         = not (null rawcmd) && null cmd     hasHelp args         = any (`elem` args) ["--help","-h","-?"]     hasVersion           = ("--version" `elem`)-    mainmode'            = mainmode addons-    generalHelp          = putStr $ showModeHelp mainmode'+    generalHelp          = putStr $ showModeHelp $ mainmode addonDisplayNames     version              = putStrLn prognameandversion     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-  when (debug_ opts > 0) $ do-    putStrLn $ "processed opts:\n" ++ ppShow opts-    putStrLn $ "command matched: " ++ show cmd-    putStrLn $ "isNullCommand: " ++ show isNullCommand-    putStrLn $ "isInternalCommand: " ++ show isInternalCommand-    putStrLn $ "isExternalCommand: " ++ show isExternalCommand-    putStrLn $ "isBadCommand: " ++ show isBadCommand-    d <- getCurrentDay-    putStrLn $ "date span from opts: " ++ (show $ dateSpanFromOpts d $ reportopts_ opts)-    putStrLn $ "interval from opts: " ++ (show $ intervalFromOpts $ reportopts_ opts)-    putStrLn $ "query from opts & args: " ++ (show $ queryFromOpts d $ reportopts_ opts)+  dbgM "processed opts" opts+  dbgM "command matched" cmd+  dbgM "isNullCommand" isNullCommand+  dbgM "isInternalCommand" isInternalCommand+  dbgM "isExternalCommand" isExternalCommand+  dbgM "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)   let-    dbg s = if debug_ opts > 0 then trace s else id     runHledgerCommand       -- high priority flags and situations. --help should be highest priority.-      | hasHelp argsbeforecmd    = dbg "--help before command, showing general help" generalHelp+      | hasHelp argsbeforecmd    = dbgM "" "--help before command, showing general help" >> generalHelp       | not (hasHelp argsaftercmd) && (hasVersion argsbeforecmd || (hasVersion argsaftercmd && isInternalCommand))                                  = version       -- \| (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            = dbg "no command, showing general help" generalHelp+      | isNullCommand            = dbgM "" "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 == "accounts"        = withJournalDo opts accounts        `orShowHelp` accountsmode       | cmd == "balance"         = withJournalDo opts balance         `orShowHelp` balancemode       | cmd == "balancesheet"    = withJournalDo opts balancesheet    `orShowHelp` balancesheetmode       | cmd == "cashflow"        = withJournalDo opts cashflow        `orShowHelp` cashflowmode@@ -150,15 +265,15 @@        -- an external command       | isExternalCommand = do-          let shellcmd = printf "%s-%s %s" progname cmd (unwords' argsaftercmd)-          when (debug_ opts > 0) $ do-            printf "external command selected: %s\n" cmd-            printf "external command arguments: %s\n" (show argsaftercmd)-            printf "running shell command: %s\n" (show shellcmd)+          let externalargs = 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           system shellcmd >>= exitWith        -- deprecated commands-      | cmd == "convert"         = error' (modeHelp convertmode) >> exitFailure+      | cmd == "convert"         = error' (modeHelp oldconvertmode) >> exitFailure        -- shouldn't reach here       | otherwise                = optserror ("could not understand the arguments "++show args) >> exitFailure
Hledger/Cli/Options.hs view
@@ -1,165 +1,131 @@-{-# LANGUAGE TemplateHaskell, ScopedTypeVariables, DeriveDataTypeable #-}+{-# LANGUAGE ScopedTypeVariables, DeriveDataTypeable #-} {-| -Command-line options for the hledger program, and related utilities.+Common cmdargs modes and flags, a command-line options type, and+related utilities used by hledger commands.  -}  module Hledger.Cli.Options ( -  -- * cmdargs modes & flags-  -- | These tell cmdargs how to parse the command line arguments.-  -- There's one mode for each internal subcommand, plus a main mode.-  mainmode,-  activitymode,-  addmode,-  balancemode,-  balancesheetmode,-  cashflowmode,-  incomestatementmode,-  printmode,-  registermode,-  statsmode,-  testmode,-  convertmode,-  defCommandMode,-  argsFlag,+  -- * cmdargs flags & modes   helpflags,   inputflags,   reportflags,   generalflagsgroup1,   generalflagsgroup2,   generalflagsgroup3,+  defMode,+  defCommandMode,+  defAddonCommandMode,+  argsFlag,+  showModeHelp,+  withAliases,   -  -- * raw options-  -- | To allow the cmdargs modes to be reused and extended by other-  -- packages (eg, add-ons which want to mimic the standard hledger-  -- options), we parse the command-line arguments to a simple-  -- association list, not a fixed ADT.-  RawOpts,-  inRawOpts,-  boolopt,-  intopt,-  maybeintopt,-  stringopt,-  maybestringopt,-  listofstringopt,-  setopt,-  setboolopt,-   -- * CLI options-  -- | Raw options are converted to a more convenient,-  -- package-specific options structure. This is the \"opts\" used-  -- throughout hledger CLI code.   CliOpts(..),   defcliopts,+  getCliOpts,+  decodeRawOpts,+  rawOptsToCliOpts,+  checkCliOpts, +  -- possibly these should move into argsToCliOpts   -- * CLI option accessors-  -- | Some options require more processing. Possibly these should be merged into argsToCliOpts.+  -- | These do the extra processing required for some options.   aliasesFromOpts,-  formatFromOpts,   journalFilePathFromOpts,   rulesFilePathFromOpts,+  -- | For register:   OutputWidth(..),   Width(..),   defaultWidth,   defaultWidthWithFlag,   widthFromOpts,+  -- | For balance:+  formatFromOpts, -  -- * utilities-  getHledgerAddonCommands,-  argsToCliOpts,-  moveFlagsAfterCommand,-  decodeRawOpts,-  checkCliOpts,-  rawOptsToCliOpts,-  optserror,-  showModeHelp,-  debugArgs,-  getCliOpts,+  -- * Other utils+  hledgerAddons, -  -- * tests+  -- * Tests   tests_Hledger_Cli_Options  )  where    import qualified Control.Exception as C--- import Control.Monad (filterM) import Control.Monad (when)-import Data.Char (isDigit) import Data.List-import Data.List.Split import Data.Maybe-import Data.Time.Calendar import Safe import System.Console.CmdArgs import System.Console.CmdArgs.Explicit import System.Console.CmdArgs.Text import System.Directory import System.Environment-import System.Exit+import System.Exit (exitSuccess)+import System.FilePath import Test.HUnit import Text.ParserCombinators.Parsec as P-import Text.Printf  import Hledger-import Hledger.Data.FormatStrings as Format+import Hledger.Data.OutputFormat as OutputFormat import Hledger.Cli.Version --- --- 1. cmdargs mode and flag (option) definitions for the hledger CLI,--- can be reused by other packages as well.--- --- | Our cmdargs modes parse arguments into an association list for better reuse.-type RawOpts = [(String,String)]---- common flags and flag groups+-- common cmdargs flags  -- | Common help flags: --help, --debug, --version...+helpflags :: [Flag RawOpts] helpflags = [-  flagNone ["help","h","?"] (setboolopt "help") "Display general help or (with --help after COMMAND) command help."+  flagNone ["help","h"] (setboolopt "help") "show general help or (after command) command help"  -- ,flagNone ["browse-args"] (setboolopt "browse-args") "use a web UI to select options and build up a command line"- ,flagOpt "1" ["debug"] (\s opts -> Right $ setopt "debug" s opts) "N" "Show debug output (optional argument sets debug level)"- ,flagNone ["version"] (setboolopt "version") "Print version information"+ ,flagOpt "1" ["debug"] (\s opts -> Right $ setopt "debug" s opts) "N" "show debug output (increase N for more)"+ ,flagNone ["version"] (setboolopt "version") "show version information"  ]  -- | Common input-related flags: --file, --rules-file, --alias...+inputflags :: [Flag RawOpts] inputflags = [-  flagReq ["file","f"]  (\s opts -> Right $ setopt "file" s opts) "FILE" "use a different journal file; - means stdin"- ,flagReq ["rules-file"]  (\s opts -> Right $ setopt "rules-file" s opts) "RULESFILE" "conversion rules for CSV (default: FILE.rules)"- ,flagReq ["alias"]  (\s opts -> Right $ setopt "alias" s opts)  "ACCT=ALIAS" "convert ACCT's name to ALIAS"+  flagReq ["file","f"]  (\s opts -> Right $ setopt "file" s opts) "FILE" "use a different input file. For stdin, use -"+ ,flagReq ["rules-file"]  (\s opts -> Right $ setopt "rules-file" s opts) "RFILE" "CSV conversion rules file (default: FILE.rules)"+ ,flagReq ["alias"]  (\s opts -> Right $ setopt "alias" s opts)  "OLD=NEW" "display accounts named OLD as NEW"  ] --- | Common report-related flags: --period, --cost, --display etc.+-- | Common report-related flags: --period, --cost, etc.+reportflags :: [Flag RawOpts] reportflags = [-  flagReq  ["begin","b"]     (\s opts -> Right $ setopt "begin" s opts) "DATE" "report on transactions on or after this date"- ,flagReq  ["end","e"]       (\s opts -> Right $ setopt "end" s opts) "DATE" "report on transactions before this date"- ,flagReq  ["period","p"]    (\s opts -> Right $ setopt "period" s opts) "PERIODEXP" "report on transactions during the specified period and/or with the specified reporting interval"- ,flagNone ["daily","D"]     (\opts -> setboolopt "daily" opts) "report by day"- ,flagNone ["weekly","W"]    (\opts -> setboolopt "weekly" opts) "report by week"- ,flagNone ["monthly","M"]   (\opts -> setboolopt "monthly" opts) "report by month"- ,flagNone ["quarterly","Q"] (\opts -> setboolopt "quarterly" opts) "report by quarter"- ,flagNone ["yearly","Y"]    (\opts -> setboolopt "yearly" opts) "report by year"- ,flagNone ["cleared","C"]   (\opts -> setboolopt "cleared" opts) "report only on cleared transactions"- ,flagNone ["uncleared","U"] (\opts -> setboolopt "uncleared" opts) "report only on uncleared transactions"- ,flagNone ["cost","B"]      (\opts -> setboolopt "cost" opts) "report cost of commodities"- ,flagReq  ["depth"]         (\s opts -> Right $ setopt "depth" s opts) "N" "hide accounts/transactions deeper than this"- ,flagReq  ["display","d"]   (\s opts -> Right $ setopt "display" s opts) "DISPLAYEXP" "show only transactions matching the expression, which is 'dOP[DATE]' where OP is <, <=, =, >=, >"- ,flagNone ["date2","aux-date","effective"]     (\opts -> setboolopt "date2" opts) "use transactions' secondary dates, if any"- ,flagNone ["empty","E"]     (\opts -> setboolopt "empty" opts) "show empty/zero things which are normally elided"- ,flagNone ["real","R"]      (\opts -> setboolopt "real" opts) "report only on real (non-virtual) transactions"+  flagReq  ["begin","b"]     (\s opts -> Right $ setopt "begin" s opts) "DATE" "include postings/txns on or after this date"+ ,flagReq  ["end","e"]       (\s opts -> Right $ setopt "end" s opts) "DATE" "include postings/txns before this date"+ ,flagNone ["daily","D"]     (\opts -> setboolopt "daily" opts) "multiperiod/multicolumn report by day"+ ,flagNone ["weekly","W"]    (\opts -> setboolopt "weekly" opts) "multiperiod/multicolumn report by week"+ ,flagNone ["monthly","M"]   (\opts -> setboolopt "monthly" opts) "multiperiod/multicolumn report by month"+ ,flagNone ["quarterly","Q"] (\opts -> setboolopt "quarterly" opts) "multiperiod/multicolumn report by quarter"+ ,flagNone ["yearly","Y"]    (\opts -> setboolopt "yearly" opts) "multiperiod/multicolumn report by year"+ ,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"] (\opts -> setboolopt "date2" opts) "use postings/txns' secondary dates instead"++ ,flagNone ["cleared","C"]   (\opts -> setboolopt "cleared" opts) "include only cleared postings/txns"+ ,flagNone ["uncleared","U"] (\opts -> setboolopt "uncleared" opts) "include only uncleared postings/txns"+ ,flagNone ["real","R"]      (\opts -> setboolopt "real" opts) "include only non-virtual postings"+ ,flagReq  ["depth"]         (\s opts -> Right $ setopt "depth" s opts) "N" "hide accounts/postings deeper than N"+ ,flagNone ["empty","E"]     (\opts -> setboolopt "empty" opts) "show empty/zero things which are normally omitted"+ ,flagNone ["cost","B"]      (\opts -> setboolopt "cost" opts) "show amounts in their cost price's commodity"  ] +argsFlag :: FlagHelp -> Arg RawOpts argsFlag desc = flagArg (\s opts -> Right $ setopt "args" s opts) desc +generalflagstitle :: String generalflagstitle = "\nGeneral flags"++generalflagsgroup1, generalflagsgroup2, generalflagsgroup3 :: (String, [Flag RawOpts]) generalflagsgroup1 = (generalflagstitle, inputflags ++ reportflags ++ helpflags) generalflagsgroup2 = (generalflagstitle, inputflags ++ helpflags) generalflagsgroup3 = (generalflagstitle, helpflags) --- cmdargs modes+-- cmdargs mode constructors  -- | A basic mode template. defMode :: Mode RawOpts@@ -174,7 +140,7 @@  ,modeGroupFlags = Group {      groupNamed = []     ,groupUnnamed = [-        flagNone ["help","h","?"] (setboolopt "help") "Display command help."+        flagNone ["help","h","?"] (setboolopt "help") "Show command help."         ]     ,groupHidden = []     }@@ -183,6 +149,7 @@  }  -- | A basic subcommand mode with the given command name(s).+defCommandMode :: [Name] -> Mode RawOpts defCommandMode names = defMode {    modeNames=names   ,modeValue=[("command", headDef "" names)]@@ -190,9 +157,10 @@   }  -- | A basic subcommand mode suitable for an add-on command.+defAddonCommandMode :: Name -> Mode RawOpts defAddonCommandMode addon = defMode {    modeNames = [addon]-  ,modeHelp = printf "run %s-%s" progname addon+  ,modeHelp = fromMaybe "" $ lookup (stripAddonExtension addon) standardAddonsHelp   ,modeValue=[("command",addon)]   ,modeGroupFlags = Group {       groupUnnamed = []@@ -202,64 +170,45 @@   ,modeArgs = ([], Just $ argsFlag "[ARGS]")   } +-- | Built-in descriptions for some of the known external addons,+-- since we don't currently have any way to ask them.+standardAddonsHelp :: [(String,String)]+standardAddonsHelp = [+   ("chart", "generate simple balance pie charts")+  ,("interest", "generate interest transaction entries")+  ,("irr", "calculate internal rate of return")+  ,("vty", "start the curses-style interface")+  ,("web", "start the web interface")+  ,("accounts", "list account names")+  ,("balance-csv", "output a balance report as CSV")+  ,("equity", "show a transaction entry zeroing all accounts")+  ,("print-unique", "print only transactions with unique descriptions")+  ,("register-csv", "output a register report as CSV")+  ,("rewrite", "add specified postings to matched transaction entries")+  ,("addon",  "dummy add-on command for testing")+  ,("addon2", "dummy add-on command for testing")+  ,("addon3", "dummy add-on command for testing")+  ,("addon4", "dummy add-on command for testing")+  ,("addon5", "dummy add-on command for testing")+  ,("addon6", "dummy add-on command for testing")+  ,("addon7", "dummy add-on command for testing")+  ,("addon8", "dummy add-on command for testing")+  ,("addon9", "dummy add-on command for testing")+  ]++-- | Get a mode's help message as a nicely wrapped string.+showModeHelp :: Mode a -> String+showModeHelp = (showText defaultWrap :: [Text] -> String) . +               (helpText [] HelpFormatDefault :: Mode a -> [Text])+ -- | Add command aliases to the command's help string. withAliases :: String -> [String] -> String s `withAliases` []     = s-s `withAliases` (a:[]) = s ++ " (alias: " ++ a ++ ")"-s `withAliases` as     = s ++ " (aliases: " ++ intercalate ", " as ++ ")"+s `withAliases` as = s ++ " (" ++ intercalate ", " as ++ ")"+-- s `withAliases` (a:[]) = s ++ " (alias: " ++ a ++ ")"+-- s `withAliases` as     = s ++ " (aliases: " ++ intercalate ", " as ++ ")"  --- | The top-level cmdargs mode for hledger.-mainmode addons = defMode {-  modeNames = [progname]- ,modeHelp = unlines [-      "run the specified hledger command. Commands:"-     ]- ,modeHelpSuffix = [""]- ,modeArgs = ([], Just $ argsFlag "[ARGS]")- ,modeGroupModes = Group {-    -- modes (commands) in named groups:-    groupNamed = [-      ("Adding data", [-        addmode-       ])-     ,("\nBasic reports", [-        printmode-       ,balancemode-       ,registermode-       -- ,transactionsmode-       ])-     ,("\nMore reports", [-        activitymode-       ,incomestatementmode-       ,balancesheetmode-       ,cashflowmode-       ,statsmode-       ])-     ,("\nMiscellaneous", [-        testmode-       ])-     ]-     ++ case addons of [] -> []-                       cs -> [("\nAdd-on commands found", map defAddonCommandMode cs)]-    -- modes in the unnamed group, shown first without a heading:-   ,groupUnnamed = [-     ]-    -- modes handled but not shown-   ,groupHidden = [-        convertmode-     ]-   }- ,modeGroupFlags = Group {-     -- flags in named groups:-     groupNamed = [generalflagsgroup3]-     -- flags in the unnamed group, shown last without a heading:-    ,groupUnnamed = []-     -- flags accepted but not shown in the help:-    ,groupHidden = inputflags -- included here so they'll not raise a confusing error if present with no COMMAND-    }- }- -- help_postscript = [ --   -- "DATES can be Y/M/D or smart dates like \"last month\"." --   -- ,"PATTERNS are regular"@@ -268,151 +217,13 @@ --   -- ,"When using both, not: comes last." --  ] --- visible subcommand modes -addmode = (defCommandMode ["add"]) {-  modeHelp = "prompt for new transaction entries and add them to the journal"- ,modeHelpSuffix = ["Defaults come from previous similar transactions; use query patterns to restrict these."]- ,modeGroupFlags = Group {-     groupUnnamed = [-      flagNone ["no-new-accounts"]  (\opts -> setboolopt "no-new-accounts" opts) "don't allow creating new accounts"-     ]-    ,groupHidden = []-    ,groupNamed = [generalflagsgroup2]-    }- }--balancemode = (defCommandMode $ ["balance"] ++ aliases) {-  modeHelp = "show matched accounts and their balances" `withAliases` aliases- ,modeGroupFlags = Group {-     groupUnnamed = [-      flagNone ["cumulative"] (\opts -> setboolopt "cumulative" opts) "with a reporting interval, show accumulated totals starting from 0"-     ,flagNone ["historical","H"] (\opts -> setboolopt "historical" opts) "with a reporting interval, show accurate historical ending balances"-     ,flagNone ["flat"] (\opts -> setboolopt "flat" opts) "show full account names, unindented"-     ,flagReq  ["drop"] (\s opts -> Right $ setopt "drop" s opts) "N" "with --flat, omit this many leading account name components"-     ,flagReq  ["format"] (\s opts -> Right $ setopt "format" s opts) "FORMATSTR" "use this custom line format"-     ,flagNone ["no-elide"] (\opts -> setboolopt "no-elide" opts) "no eliding at all, stronger than --empty"-     ,flagNone ["no-total"] (\opts -> setboolopt "no-total" opts) "don't show the final total"-     ]-    ,groupHidden = []-    ,groupNamed = [generalflagsgroup1]-    }- }-  where aliases = ["b","bal"]--printmode = (defCommandMode $ ["print"] ++ aliases) {-  modeHelp = "show matched journal entries" `withAliases` aliases- ,modeGroupFlags = Group {-     groupUnnamed = []-    ,groupHidden = []-    ,groupNamed = [generalflagsgroup1]-    }- }-  where aliases = ["p"]--registermode = (defCommandMode $ ["register"] ++ aliases) {-  modeHelp = "show matched postings and running total" `withAliases` aliases- ,modeGroupFlags = Group {-     groupUnnamed = [-      flagOpt (show defaultWidthWithFlag) ["width","w"] (\s opts -> Right $ setopt "width" s opts) "N" "increase or set the output width (default: 80)"-     ,flagNone ["average","A"] (\opts -> setboolopt "average" opts) "show the running average instead of the running total"-     ,flagNone ["related","r"] (\opts -> setboolopt "related" opts) "show the other postings in the transactions of those that would have been shown"-     ]-    ,groupHidden = []-    ,groupNamed = [generalflagsgroup1]-    }- }-  where aliases = ["r","reg"]---- transactionsmode = (defCommandMode ["transactions"]) {---   modeHelp = "show matched transactions and balance in some account(s)"---  ,modeGroupFlags = Group {---      groupUnnamed = []---     ,groupHidden = []---     ,groupNamed = [generalflagsgroup1]---     }---  }--activitymode = (defCommandMode ["activity"]) {-  modeHelp = "show a barchart of transactions per interval"- ,modeHelpSuffix = ["The default interval is daily."]- ,modeGroupFlags = Group {-     groupUnnamed = []-    ,groupHidden = []-    ,groupNamed = [generalflagsgroup1]-    }- }--incomestatementmode = (defCommandMode $ ["incomestatement"]++aliases) {-  modeHelp = "show a simple income statement" `withAliases` aliases- ,modeGroupFlags = Group {-     groupUnnamed = []-    ,groupHidden = []-    ,groupNamed = [generalflagsgroup1]-    }- }-  where aliases = ["is","pl"]--balancesheetmode = (defCommandMode $ ["balancesheet"]++aliases) {-  modeHelp = "show a simple balance sheet" `withAliases` aliases- ,modeGroupFlags = Group {-     groupUnnamed = []-    ,groupHidden = []-    ,groupNamed = [generalflagsgroup1]-    }- }-  where aliases = ["bs"]--cashflowmode = (defCommandMode ["cashflow","cf"]) {-  modeHelp = "show a simple cashflow statement" `withAliases` ["cf"]- ,modeGroupFlags = Group {-     groupUnnamed = []-    ,groupHidden = []-    ,groupNamed = [generalflagsgroup1]-    }- }--statsmode = (defCommandMode $ ["stats"] ++ aliases) {-  modeHelp = "show quick statistics for a journal" `withAliases` aliases- ,modeGroupFlags = Group {-     groupUnnamed = []-    ,groupHidden = []-    ,groupNamed = [generalflagsgroup1]-    }- }-  where aliases = ["s"]--testmode = (defCommandMode ["test"]) {-  modeHelp = "run self-tests, or just the ones matching REGEXPS"- ,modeArgs = ([], Just $ argsFlag "[REGEXPS]")- ,modeGroupFlags = Group {-     groupUnnamed = []-    ,groupHidden = []-    ,groupNamed = [generalflagsgroup3]-    }- }---- hidden commands--convertmode = (defCommandMode ["convert"]) {-  modeValue = [("command","convert")]- ,modeHelp = "convert is no longer needed, just use -f FILE.csv"- ,modeArgs = ([], Just $ argsFlag "[CSVFILE]")- ,modeGroupFlags = Group {-     groupUnnamed = []-    ,groupHidden = helpflags-    ,groupNamed = []-    }- }------- 2. A package-specific data structure holding options used in this--- package and above, parsed from RawOpts.  This represents the--- command-line options that were provided, with all parsing--- completed, but before adding defaults or derived values (XXX add)---+-- CliOpts --- | Command line options. Used in the @hledger@ package and above.+-- | Command line options, used in the @hledger@ package and above.+-- This is the \"opts\" used throughout hledger CLI code.+-- representing the options that arguments that were provided at+-- startup on the command-line. data CliOpts = CliOpts {      rawopts_         :: RawOpts     ,command_         :: String@@ -425,6 +236,9 @@     ,reportopts_      :: ReportOpts  } deriving (Show, Data, Typeable) +instance Default CliOpts where def = defcliopts++defcliopts :: CliOpts defcliopts = CliOpts     def     def@@ -436,14 +250,16 @@     def     def -instance Default CliOpts where def = defcliopts+-- | Convert possibly encoded option values to regular unicode strings.+decodeRawOpts :: RawOpts -> RawOpts+decodeRawOpts = map (\(name,val) -> (name, fromSystemString val))  -- | Parse raw option string values to the desired final data types. -- Any relative smart dates will be converted to fixed dates based on -- today's date. Parsing failures will raise an error. rawOptsToCliOpts :: RawOpts -> IO CliOpts rawOptsToCliOpts rawopts = do-  d <- getCurrentDay+  ropts <- rawOptsToReportOpts rawopts   return defcliopts {               rawopts_         = rawopts              ,command_         = stringopt "command" rawopts@@ -453,80 +269,9 @@              ,debug_           = intopt "debug" rawopts              ,no_new_accounts_ = boolopt "no-new-accounts" rawopts -- add              ,width_           = maybestringopt "width" rawopts    -- register-             ,reportopts_ = defreportopts {-                             begin_     = maybesmartdateopt d "begin" rawopts-                            ,end_       = maybesmartdateopt d "end" rawopts-                            ,period_    = maybeperiodopt d rawopts-                            ,cleared_   = boolopt "cleared" rawopts-                            ,uncleared_ = boolopt "uncleared" rawopts-                            ,cost_      = boolopt "cost" rawopts-                            ,depth_     = maybeintopt "depth" rawopts-                            ,display_   = maybedisplayopt d rawopts-                            ,date2_     = boolopt "date2" rawopts-                            ,empty_     = boolopt "empty" rawopts-                            ,no_elide_  = boolopt "no-elide" rawopts-                            ,real_      = boolopt "real" rawopts-                            ,balancetype_ = balancetypeopt rawopts -- balance-                            ,flat_      = boolopt "flat" rawopts -- balance-                            ,drop_      = intopt "drop" rawopts -- balance-                            ,no_total_  = boolopt "no-total" rawopts -- balance-                            ,daily_     = boolopt "daily" rawopts-                            ,weekly_    = boolopt "weekly" rawopts-                            ,monthly_   = boolopt "monthly" rawopts-                            ,quarterly_ = boolopt "quarterly" rawopts-                            ,yearly_    = boolopt "yearly" rawopts-                            ,format_    = maybestringopt "format" rawopts-                            ,average_   = boolopt "average" rawopts  -- register-                            ,related_   = boolopt "related" rawopts  -- register-                            ,query_     = unwords $ listofstringopt "args" rawopts-                            }+             ,reportopts_      = ropts              }---- | Parse hledger CLI options from these command line arguments and--- add-on command names, or raise any error.-argsToCliOpts :: [String] -> [String] -> IO CliOpts-argsToCliOpts args addons = do-  let-    args'        = moveFlagsAfterCommand args-    cmdargsopts  = System.Console.CmdArgs.Explicit.processValue (mainmode addons) args'-    cmdargsopts' = decodeRawOpts cmdargsopts-  rawOptsToCliOpts cmdargsopts' >>= checkCliOpts---- | A hacky workaround for cmdargs not accepting flags before the--- subcommand name: try to detect and move such flags after the--- command.  This allows the user to put them in either position.--- The order of options is not preserved, but this should be ok.------ Since we're not parsing flags as precisely as cmdargs here, this is--- imperfect. We make a decent effort to:--- - move all no-argument help and input flags--- - move all required-argument help and input flags along with their values, space-separated or not--- - not confuse things further or cause misleading errors.-moveFlagsAfterCommand :: [String] -> [String]-moveFlagsAfterCommand args = move args-  where-    move (f:a:as)           | isMovableNoArgFlag f           = (move $ a:as) ++ [f]-    move (f:v:a:as)         | isMovableReqArgFlag f          = (move $ a:as) ++ [f,v]-    move (fv:a:as)          | isMovableReqArgFlagAndValue fv = (move $ a:as) ++ [fv]-    move ("--debug":v:a:as) | not (null v) && all isDigit v  = (move $ a:as) ++ ["--debug",v]-    move ("--debug":a:as)                                    = (move $ a:as) ++ ["--debug"]-    move (fv@('-':'-':'d':'e':'b':'u':'g':'=':_):a:as)       = (move $ a:as) ++ [fv]-    move as = as--    isMovableNoArgFlag a  = "-" `isPrefixOf` a && dropWhile (=='-') a `elem` noargflagstomove-    isMovableReqArgFlag a = "-" `isPrefixOf` a && dropWhile (=='-') a `elem` reqargflagstomove-    isMovableReqArgFlagAndValue ('-':'-':a:as) = case break (== '=') (a:as) of (f:fs,_) -> (f:fs) `elem` reqargflagstomove-                                                                               _        -> False-    isMovableReqArgFlagAndValue ('-':f:_:_) = [f] `elem` reqargflagstomove-    isMovableReqArgFlagAndValue _ = False--    noargflagstomove  = concatMap flagNames $ filter ((==FlagNone).flagInfo) flagstomove-    reqargflagstomove = concatMap flagNames $ filter ((==FlagReq ).flagInfo) flagstomove-    flagstomove = inputflags ++ helpflags---- | Convert possibly encoded option values to regular unicode strings.-decodeRawOpts = map (\(name,val) -> (name, fromSystemString val))-+   -- | Do final validation of processed opts, raising an error if there is trouble. checkCliOpts :: CliOpts -> IO CliOpts -- or pure.. checkCliOpts opts@CliOpts{reportopts_=ropts} = do@@ -538,115 +283,79 @@     Right _ -> return ()   return opts ------ utils------- | Get the unique suffixes (without hledger-) of hledger-* executables--- found in the current user's PATH, or the empty list if there is any--- problem.-getHledgerAddonCommands :: IO [String]-getHledgerAddonCommands = map (drop (length progname + 1)) `fmap` getHledgerExesInPath---- | Get the unique names of hledger-*{,.hs} executables found in the current--- user's PATH, or the empty list if there is any problem.-getHledgerExesInPath :: IO [String]-getHledgerExesInPath = do-  pathdirs <- splitOn ":" `fmap` getEnvSafe "PATH"-  pathfiles <- concat `fmap` mapM getDirectoryContentsSafe pathdirs-  let hledgernamed = nub $ sort $ filter isHledgerNamed pathfiles-  -- hledgerexes <- filterM isExecutable hledgernamed-  return hledgernamed---- isExecutable f = getPermissions f >>= (return . executable)--isHledgerNamed = isRight . parsewith (do-        string progname-        char '-'-        many1 (letter <|> char '-')-        optional $ (string ".hs" <|> string ".lhs")-        eof-        )--getEnvSafe v = getEnv v `C.catch` (\(_::C.IOException) -> return "")-getDirectoryContentsSafe d = getDirectoryContents d `C.catch` (\(_::C.IOException) -> return [])---- | Raise an error, showing the specified message plus a hint about --help.-optserror = error' . (++ " (run with --help for usage)")--setopt name val = (++ [(name,singleQuoteIfNeeded val)])--setboolopt name = (++ [(name,"")])---- | Is the named option present ?-inRawOpts :: String -> RawOpts -> Bool-inRawOpts name = isJust . lookup name--boolopt = inRawOpts--maybestringopt name = maybe Nothing (Just . stripquotes) . lookup name--stringopt name = fromMaybe "" . maybestringopt name--listofstringopt name rawopts = [v | (k,v) <- rawopts, k==name]--maybeintopt :: String -> RawOpts -> Maybe Int-maybeintopt name rawopts =-    let ms = maybestringopt name rawopts in-    case ms of Nothing -> Nothing-               Just s -> Just $ readDef (optserror $ "could not parse "++name++" number: "++s) s--intopt name = fromMaybe 0 . maybeintopt name+-- Currently only used by some extras/ scripts:+-- | Parse hledger CLI options from the command line using the given+-- cmdargs mode, and either return them or, if a help flag is present,+-- print the mode help and exit the program.+getCliOpts :: Mode RawOpts -> IO CliOpts+getCliOpts mode = do+  args <- getArgs+  let rawopts = decodeRawOpts $ processValue mode args+  opts <- rawOptsToCliOpts rawopts >>= checkCliOpts+  debugArgs args opts+  -- if any (`elem` args) ["--help","-h","-?"]+  when ("help" `inRawOpts` rawopts_ opts) $+    putStr (showModeHelp mode) >> exitSuccess+  return opts+  where+    -- | Print debug info about arguments and options if --debug is present.+    debugArgs :: [String] -> CliOpts -> IO ()+    debugArgs args opts =+      when ("--debug" `elem` args) $ do+        progname <- getProgName+        putStrLn $ "running: " ++ progname+        putStrLn $ "raw args: " ++ show args+        putStrLn $ "processed opts:\n" ++ show opts+        d <- getCurrentDay+        putStrLn $ "search query: " ++ (show $ queryFromOpts d $ reportopts_ opts) -maybesmartdateopt :: Day -> String -> RawOpts -> Maybe Day-maybesmartdateopt d name rawopts =-        case maybestringopt name rawopts of-          Nothing -> Nothing-          Just s -> either-                    (\e -> optserror $ "could not parse "++name++" date: "++show e)-                    Just-                    $ fixSmartDateStrEither' d s+-- CliOpts accessors -maybedisplayopt :: Day -> RawOpts -> Maybe DisplayExp-maybedisplayopt d rawopts =-    maybe Nothing (Just . regexReplaceBy "\\[.+?\\]" fixbracketeddatestr) $ maybestringopt "display" rawopts+-- | Get the account name aliases from options, if any.+aliasesFromOpts :: CliOpts -> [(AccountName,AccountName)]+aliasesFromOpts = map parseAlias . alias_     where-      fixbracketeddatestr "" = ""-      fixbracketeddatestr s = "[" ++ fixSmartDateStr d (init $ tail s) ++ "]"+      -- 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 -maybeperiodopt :: Day -> RawOpts -> Maybe (Interval,DateSpan)-maybeperiodopt d rawopts =-    case maybestringopt "period" rawopts of-      Nothing -> Nothing-      Just s -> either-                (\e -> optserror $ "could not parse period option: "++show e)-                Just-                $ parsePeriodExpr d s+-- | Get the (tilde-expanded, absolute) journal file path from+-- 1. options, 2. an environment variable, or 3. the default.+journalFilePathFromOpts :: CliOpts -> IO String+journalFilePathFromOpts opts = do+  f <- defaultJournalPath+  d <- getCurrentDirectory+  expandPath d $ fromMaybe f $ file_ opts -balancetypeopt :: RawOpts -> BalanceType-balancetypeopt rawopts-    | length [o | o <- ["cumulative","historical"], isset o] > 1-                         = optserror "please specify at most one of --cumulative and --historical"-    | isset "cumulative" = CumulativeBalance-    | isset "historical" = HistoricalBalance-    | otherwise          = PeriodBalance-    where-      isset = flip boolopt rawopts+-- | Get the (tilde-expanded) rules file path from options, if any.+rulesFilePathFromOpts :: CliOpts -> IO (Maybe FilePath)+rulesFilePathFromOpts opts = do+  d <- getCurrentDirectory+  maybe (return Nothing) (fmap Just . expandPath d) $ rules_file_ opts +-- for balance, currently:+ -- | Parse the format option if provided, possibly returning an error, -- otherwise get the default value.-formatFromOpts :: ReportOpts -> Either String [FormatString]-formatFromOpts = maybe (Right defaultBalanceFormatString) parseFormatString . format_+formatFromOpts :: ReportOpts -> Either String [OutputFormat]+formatFromOpts = maybe (Right defaultBalanceFormat) parseStringFormat . format_  -- | Default line format for balance report: "%20(total)  %2(depth_spacer)%-(account)"-defaultBalanceFormatString :: [FormatString]-defaultBalanceFormatString = [+defaultBalanceFormat :: [OutputFormat]+defaultBalanceFormat = [       FormatField False (Just 20) Nothing TotalField     , FormatLiteral "  "     , FormatField True (Just 2) Nothing DepthSpacerField     , FormatField True Nothing Nothing AccountField     ] +-- for register:+ -- | Output width configuration (for register). data OutputWidth =     TotalWidth Width    -- ^ specify the overall width @@ -660,9 +369,11 @@   deriving Show  -- | Default width of hledger console output.-defaultWidth         = 80+defaultWidth :: Int+defaultWidth = 80  -- | Width of hledger console output when the -w flag is used with no value.+defaultWidthWithFlag :: Int defaultWidthWithFlag = 120  -- | Parse the width option if provided, possibly returning an error,@@ -688,69 +399,109 @@ widthp = (string "auto" >> return Auto)     <|> (Width . read) `fmap` many1 digit --- | 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+-- Other utils --- | Get the (tilde-expanded, absolute) journal file path from--- 1. options, 2. an environment variable, or 3. the default.-journalFilePathFromOpts :: CliOpts -> IO String-journalFilePathFromOpts opts = do-  f <- defaultJournalPath-  d <- getCurrentDirectory-  expandPath d $ fromMaybe f $ file_ opts+-- | Get the sorted unique precise names and display names of hledger+-- add-ons found in the current user's PATH. The precise names are the+-- add-on's filename with the "hledger-" prefix removed. The display+-- names have the file extension removed also, except when it's needed+-- for disambiguation.+--+-- -- Also when there are exactly two similar names, one with the .hs or+-- -- .lhs extension and the other with the .exe extension or no+-- -- extension - presumably source and compiled versions of a haskell+-- -- script - we exclude the source version.+--+-- This function can return add-on names which shadow built-in command+-- names, but hledger will ignore these.+--+hledgerAddons :: IO ([String],[String])+hledgerAddons = do+  exes <- hledgerExecutablesInPath+  let precisenames = -- concatMap dropRedundant $+                     -- groupBy (\a b -> dropExtension a == dropExtension b) $+                     map stripprefix exes+  let displaynames = concatMap stripext $+                     groupBy (\a b -> dropExtension a == dropExtension b) $+                     precisenames+  return (precisenames, displaynames)+  where+    stripprefix = drop (length progname + 1)+    -- dropRedundant [f,f2] | takeExtension f `elem` ["",".exe"] && takeExtension f2 `elem` [".hs",".lhs"] = [f]+    -- dropRedundant fs = fs+    stripext [f] = [dropExtension f]+    stripext fs  = fs --- | Get the (tilde-expanded) rules file path from options, if any.-rulesFilePathFromOpts :: CliOpts -> IO (Maybe FilePath)-rulesFilePathFromOpts opts = do-  d <- getCurrentDirectory-  maybe (return Nothing) (fmap Just . expandPath d) $ rules_file_ opts+-- | Get the sorted unique filenames of all hledger-* executables in+-- the current user's PATH. Currently these are: files in any of the+-- PATH directories, named hledger-*, with either no extension (and no+-- periods in the name) or one of the addonExtensions.  Limitations:+-- we do not currently check that the file is really a file (not eg a+-- directory) or whether it has execute permission.+hledgerExecutablesInPath :: IO [String]+hledgerExecutablesInPath = do+  pathdirs <- regexSplit "[:;]" `fmap` getEnvSafe "PATH"+  pathfiles <- concat `fmap` mapM getDirectoryContentsSafe pathdirs+  return $ nub $ sort $ filter isHledgerExeName pathfiles+  -- XXX should exclude directories and files without execute permission.+  -- These will do a stat for each hledger-*, probably ok.+  -- But they need paths, not just filenames+  -- hledgerexes  <- filterM doesFileExist hledgernamed+  -- hledgerexes' <- filterM isExecutable hledgerexes+  -- return hledgerexes --- | Get a mode's help message as a nicely wrapped string.-showModeHelp :: Mode a -> String-showModeHelp =-  (showText defaultWrap :: [Text] -> String)-  .-  (helpText [] HelpFormatDefault :: Mode a -> [Text])+-- isExecutable f = getPermissions f >>= (return . executable) --- not used:+isHledgerExeName :: String -> Bool+isHledgerExeName = isRight . parsewith hledgerexenamep+    where+      hledgerexenamep = do+        string progname+        char '-'+        many1 (noneOf ".")+        optional (string "." >> choice' (map string addonExtensions))+        eof --- | Print debug info about arguments and options if --debug is present.-debugArgs :: [String] -> CliOpts -> IO ()-debugArgs args opts =-  when ("--debug" `elem` args) $ do-    progname <- getProgName-    putStrLn $ "running: " ++ progname-    putStrLn $ "raw args: " ++ show args-    putStrLn $ "processed opts:\n" ++ show opts-    d <- getCurrentDay-    putStrLn $ "search query: " ++ (show $ queryFromOpts d $ reportopts_ opts)+stripAddonExtension :: String -> String+stripAddonExtension = regexReplace re "" where re = "\\.(" ++ intercalate "|" addonExtensions ++ ")$" +addonExtensions :: [String]+addonExtensions =+  ["bat"+  ,"com"+  ,"exe"+  ,"hs"+  ,"lhs"+  ,"pl"+  ,"py"+  ,"rb"+  ,"rkt"+  ,"sh"+  -- ,""+  ]++getEnvSafe :: String -> IO String+getEnvSafe v = getEnv v `C.catch` (\(_::C.IOException) -> return "")++getDirectoryContentsSafe :: FilePath -> IO [String]+getDirectoryContentsSafe d =+    (filter (not . (`elem` [".",".."])) `fmap` getDirectoryContents d) `C.catch` (\(_::C.IOException) -> return [])+ -- not used:+-- -- | Print debug info about arguments and options if --debug is present.+-- debugArgs :: [String] -> CliOpts -> IO ()+-- debugArgs args opts =+--   when ("--debug" `elem` args) $ do+--     progname <- getProgName+--     putStrLn $ "running: " ++ progname+--     putStrLn $ "raw args: " ++ show args+--     putStrLn $ "processed opts:\n" ++ show opts+--     d <- getCurrentDay+--     putStrLn $ "search query: " ++ (show $ queryFromOpts d $ reportopts_ opts) --- | Parse hledger CLI options from the command line using the given--- cmdargs mode, and either return them or, if a help flag is present,--- print the mode help and exit the program.-getCliOpts :: Mode RawOpts -> IO CliOpts-getCliOpts mode = do-  args <- getArgs-  let rawopts = decodeRawOpts $ processValue mode args-  opts <- rawOptsToCliOpts rawopts >>= checkCliOpts-  debugArgs args opts-  -- if any (`elem` args) ["--help","-h","-?"]-  when ("help" `inRawOpts` rawopts_ opts) $-    putStr (showModeHelp mode) >> exitSuccess-  return opts+-- tests +tests_Hledger_Cli_Options :: Test tests_Hledger_Cli_Options = TestList  [  ]
Hledger/Cli/Print.hs view
@@ -5,11 +5,14 @@ -}  module Hledger.Cli.Print (-  print'- ,showTransactions+  printmode+ ,print'  ,tests_Hledger_Cli_Print-) where+)+where+ import Data.List+import System.Console.CmdArgs.Explicit import Test.HUnit  import Hledger@@ -17,57 +20,67 @@ import Hledger.Utils.UTF8IOCompat (putStr) import Hledger.Cli.Options ++printmode = (defCommandMode $ ["print"] ++ aliases) {+  modeHelp = "show transaction entries" `withAliases` aliases+ ,modeGroupFlags = Group {+     groupUnnamed = []+    ,groupHidden = []+    ,groupNamed = [generalflagsgroup1]+    }+ }+  where aliases = []+ -- | Print journal transactions in standard format. print' :: CliOpts -> Journal -> IO () print' CliOpts{reportopts_=ropts} j = do   d <- getCurrentDay-  putStr $ showTransactions ropts (queryFromOpts d ropts) j--showTransactions :: ReportOpts -> Query -> Journal -> String-showTransactions opts q j = entriesReportAsText opts q $ entriesReport opts q j+  let q = queryFromOpts d ropts+  putStr $ entriesReportAsText ropts q $ entriesReport ropts q j -tests_showTransactions = [-  "showTransactions" ~: do+entriesReportAsText :: ReportOpts -> Query -> EntriesReport -> String+entriesReportAsText _ _ items = concatMap showTransactionUnelided items -   -- "print expenses" ~:-   do -    let opts = defreportopts{query_="expenses"}-    d <- getCurrentDay-    showTransactions opts (queryFromOpts d opts) samplejournal `is` unlines-     ["2008/06/03 * eat & shop"-     ,"    expenses:food                $1"-     ,"    expenses:supplies            $1"-     ,"    assets:cash                 $-2"-     ,""-     ]+-- XXX+-- tests_showTransactions = [+--   "showTransactions" ~: do -  -- , "print report with depth arg" ~:-   do -    let opts = defreportopts{depth_=Just 2}-    d <- getCurrentDay-    showTransactions opts (queryFromOpts d opts) samplejournal `is` unlines-      ["2008/01/01 income"-      ,"    assets:bank:checking            $1"-      ,"    income:salary                  $-1"-      ,""-      ,"2008/06/01 gift"-      ,"    assets:bank:checking            $1"-      ,"    income:gifts                   $-1"-      ,""-      ,"2008/06/03 * eat & shop"-      ,"    expenses:food                $1"-      ,"    expenses:supplies            $1"-      ,"    assets:cash                 $-2"-      ,""-      ,"2008/12/31 * pay off"-      ,"    liabilities:debts               $1"-      ,"    assets:bank:checking           $-1"-      ,""-      ]- ]+--    -- "print expenses" ~:+--    do +--     let opts = defreportopts{query_="expenses"}+--     d <- getCurrentDay+--     showTransactions opts (queryFromOpts d opts) samplejournal `is` unlines+--      ["2008/06/03 * eat & shop"+--      ,"    expenses:food                $1"+--      ,"    expenses:supplies            $1"+--      ,"    assets:cash                 $-2"+--      ,""+--      ] -entriesReportAsText :: ReportOpts -> Query -> EntriesReport -> String-entriesReportAsText _ _ items = concatMap showTransactionUnelided items+--   -- , "print report with depth arg" ~:+--    do +--     let opts = defreportopts{depth_=Just 2}+--     d <- getCurrentDay+--     showTransactions opts (queryFromOpts d opts) samplejournal `is` unlines+--       ["2008/01/01 income"+--       ,"    assets:bank:checking            $1"+--       ,"    income:salary                  $-1"+--       ,""+--       ,"2008/06/01 gift"+--       ,"    assets:bank:checking            $1"+--       ,"    income:gifts                   $-1"+--       ,""+--       ,"2008/06/03 * eat & shop"+--       ,"    expenses:food                $1"+--       ,"    expenses:supplies            $1"+--       ,"    assets:cash                 $-2"+--       ,""+--       ,"2008/12/31 * pay off"+--       ,"    liabilities:debts               $1"+--       ,"    assets:bank:checking           $-1"+--       ,""+--       ]+--  ] -tests_Hledger_Cli_Print = TestList-  tests_showTransactions+tests_Hledger_Cli_Print = TestList []+  -- tests_showTransactions
Hledger/Cli/Register.hs view
@@ -5,7 +5,8 @@ -}  module Hledger.Cli.Register (-  register+  registermode+ ,register  ,postingsReportAsText  -- ,showPostingWithBalanceForVty  ,tests_Hledger_Cli_Register@@ -13,6 +14,7 @@  import Data.List import Data.Maybe+import System.Console.CmdArgs.Explicit import Test.HUnit import Text.Printf @@ -21,6 +23,21 @@ import Hledger.Utils.UTF8IOCompat (putStr) import Hledger.Cli.Options ++registermode = (defCommandMode $ ["register"] ++ aliases) {+  modeHelp = "show postings and running total" `withAliases` aliases+ ,modeGroupFlags = Group {+     groupUnnamed = [+      flagNone ["historical","H"] (\opts -> setboolopt "historical" opts) "include prior postings in the running total"+     ,flagNone ["average","A"] (\opts -> setboolopt "average" opts) "show a running average instead of the running total"+     ,flagNone ["related","r"] (\opts -> setboolopt "related" opts) "show postings' siblings instead"+     ,flagOpt (show defaultWidthWithFlag) ["width","w"] (\s opts -> Right $ setopt "width" s opts) "N" "set output width to 120, or N (default: 80)"+     ]+    ,groupHidden = []+    ,groupNamed = [generalflagsgroup1]+    }+ }+  where aliases = ["reg"]  -- | Print a (posting) register report. register :: CliOpts -> Journal -> IO ()
Hledger/Cli/Stats.hs view
@@ -4,12 +4,17 @@  -} -module Hledger.Cli.Stats+module Hledger.Cli.Stats (+  statsmode+ ,stats+) where+ import Data.List import Data.Maybe import Data.Ord import Data.Time.Calendar+import System.Console.CmdArgs.Explicit import Text.Printf import qualified Data.Map as Map @@ -19,6 +24,16 @@ import Hledger.Utils.UTF8IOCompat (putStr)  +statsmode = (defCommandMode $ ["stats"] ++ aliases) {+  modeHelp = "show some journal statistics" `withAliases` aliases+ ,modeGroupFlags = Group {+     groupUnnamed = []+    ,groupHidden = []+    ,groupNamed = [generalflagsgroup1]+    }+ }+  where aliases = []+ -- like Register.summarisePostings -- | Print various statistics for the journal. stats :: CliOpts -> Journal -> IO ()@@ -26,7 +41,7 @@   d <- getCurrentDay   let q = queryFromOpts d reportopts_       l = ledgerFromJournal q j-      reportspan = (ledgerDateSpan l) `orDatesFrom` (queryDateSpan False q)+      reportspan = (ledgerDateSpan l) `spanDefaultsFrom` (queryDateSpan False q)       intervalspans = splitSpan (intervalFromOpts reportopts_) reportspan       showstats = showLedgerStats l d       s = intercalate "\n" $ map showstats intervalspans
Hledger/Cli/Tests.hs view
@@ -6,8 +6,12 @@  -} -module Hledger.Cli.Tests+module Hledger.Cli.Tests (+  testmode+ ,test'+) where+ import Control.Monad import System.Exit import Test.HUnit@@ -34,22 +38,32 @@    then exitFailure    else exitWith ExitSuccess +testmode = (defCommandMode ["test"]) {+  modeHelp = "run built-in self-tests"+ ,modeArgs = ([], Just $ argsFlag "[REGEXPS]")+ ,modeGroupFlags = Group {+     groupUnnamed = []+    ,groupHidden = []+    ,groupNamed = [generalflagsgroup3]+    }+ }+ -- | Run all or just the matched unit tests and return their HUnit result counts. runTests :: CliOpts -> IO Counts runTests = liftM (fst . flip (,) 0) . runTestTT . flatTests --- | Run all or just the matched unit tests until the first failure or--- error, returning the name of the problem test if any.-runTestsTillFailure :: CliOpts -> IO (Maybe String)-runTestsTillFailure _ = undefined -- do-  -- let ts = flatTests opts-  --     results = liftM (fst . flip (,) 0) $ runTestTT $-  --     firstproblem = find (\counts -> )+-- -- | Run all or just the matched unit tests until the first failure or+-- -- error, returning the name of the problem test if any.+-- runTestsTillFailure :: CliOpts -> IO (Maybe String)+-- runTestsTillFailure _ = undefined -- do+--   -- let ts = flatTests opts+--   --     results = liftM (fst . flip (,) 0) $ runTestTT $+--   --     firstproblem = find (\counts -> )  -- | All or pattern-matched tests, as a flat list to show simple names. flatTests opts = TestList $ filter (matchesAccount (queryFromOpts nulldate $ reportopts_ opts) . testName) $ flattenTests tests_Hledger_Cli --- | All or pattern-matched tests, in the original suites to show hierarchical names.-hierarchicalTests opts = filterTests (matchesAccount (queryFromOpts nulldate $ reportopts_ opts) . testName) tests_Hledger_Cli+-- -- | All or pattern-matched tests, in the original suites to show hierarchical names.+-- hierarchicalTests opts = filterTests (matchesAccount (queryFromOpts nulldate $ reportopts_ opts) . testName) tests_Hledger_Cli  #endif
hledger.cabal view
@@ -1,8 +1,7 @@ name:           hledger--- also in cpp-options below-version: 0.22.2-stability:      beta-category:       Finance+version:        0.23+stability:      stable+category:       Finance, Console synopsis:       The main command-line interface for the hledger accounting tool. description:                 hledger is a library and set of user tools for working@@ -18,7 +17,7 @@ maintainer:     Simon Michael <simon@joyful.com> homepage:       http://hledger.org bug-reports:    http://hledger.org/bugs-tested-with:    GHC==7.2.2, GHC==7.4.2, GHC==7.6.3, GHC==7.8+tested-with:    GHC==7.2.2, GHC==7.4.2, GHC==7.6.3, GHC==7.8.2 cabal-version:  >= 1.10 build-type:     Simple -- data-dir:       data@@ -43,7 +42,7 @@     Default:       True  library-  cpp-options:    -DVERSION="0.22.2"+  cpp-options:    -DVERSION="0.23"   ghc-options:    -W   default-language: Haskell2010   -- should be the same as below@@ -55,6 +54,7 @@                   Hledger.Cli.Utils                   Hledger.Cli.Version                   Hledger.Cli.Add+                  Hledger.Cli.Accounts                   Hledger.Cli.Balance                   Hledger.Cli.Balancesheet                   Hledger.Cli.Cashflow@@ -65,7 +65,7 @@                   Hledger.Cli.Stats   -- should be the same as below   build-depends:-                  hledger-lib == 0.22.2+                  hledger-lib == 0.23                  ,base >= 4.3 && < 5                  -- ,cabal-file-th                  ,containers@@ -82,12 +82,22 @@                  ,process                  ,regexpr >= 0.5.1                  ,safe >= 0.2-                 ,shakespeare-text == 1.0.*+-- http://www.yesodweb.com/blog/2014/04/consolidation-progress+-- in order to support both the old and new versions of shakespeare,+-- you just need to ensure that you have both the shakespeare and+-- deprecated packages listed in your cabal file. In other words, if+-- previously you depended on hamlet, now you should depend on hamlet+-- and shakespeare. When you're ready to drop backwards compatibility,+-- simply put a lower bound of >= 2.0 on shakespeare and remove the+-- deprecated packages.+                 ,shakespeare-text >= 1.0 && < 1.2+                 ,shakespeare      >= 1.0 && < 2.1                  ,split >= 0.1 && < 0.3                  ,text >= 0.11 && < 1.2                  ,tabular >= 0.2 && < 0.3                  ,time                  ,utf8-string >= 0.3.5 && < 0.4+                 ,wizards == 1.0.*   if impl(ghc >= 7.4)     build-depends: pretty-show >= 1.6.4 @@ -109,6 +119,7 @@                   Hledger.Cli.Utils                   Hledger.Cli.Version                   Hledger.Cli.Add+                  Hledger.Cli.Accounts                   Hledger.Cli.Balance                   Hledger.Cli.Balancesheet                   Hledger.Cli.Cashflow@@ -117,14 +128,14 @@                   Hledger.Cli.Print                   Hledger.Cli.Register                   Hledger.Cli.Stats-  cpp-options:    -DVERSION="0.22.2"+  cpp-options:    -DVERSION="0.23"   ghc-options:    -W   if flag(threaded)        ghc-options:   -threaded  -- should be the same as above   default-language: Haskell2010   build-depends:-                  hledger-lib == 0.22.2+                  hledger-lib == 0.23                  ,base >= 4.3 && < 5                  ,containers                  ,cmdargs >= 0.10 && < 0.11@@ -140,12 +151,14 @@                  ,process                  ,regexpr >= 0.5.1                  ,safe >= 0.2-                 ,shakespeare-text == 1.0.*+                 ,shakespeare-text >= 1.0 && < 1.2+                 ,shakespeare      >= 1.0 && < 2.1                  ,split >= 0.1 && < 0.3                  ,tabular >= 0.2 && < 0.3                  ,text >= 0.11 && < 1.2                  ,time                  ,utf8-string >= 0.3.5 && < 0.4+                 ,wizards == 1.0.*   if impl(ghc >= 7.4)     build-depends: pretty-show >= 1.6.4 @@ -172,7 +185,8 @@                , process                , regexpr                , safe-               , shakespeare-text+               , shakespeare-text >= 1.0 && < 1.2+               , shakespeare      >= 1.0 && < 2.1                , split                ,tabular >= 0.2 && < 0.3                , test-framework@@ -180,5 +194,6 @@                , text                , time                , transformers+               , wizards == 1.0.*   if impl(ghc >= 7.4)     build-depends: pretty-show >= 1.6.4