module Hledger.Cli.Main where
import Data.Char (isDigit)
import Data.String.Here
import Data.List
import Data.List.Split (splitOn)
import Safe
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
import Hledger.Cli.DocFiles
import Hledger.Cli.Help
import Hledger.Cli.Histogram
import Hledger.Cli.Incomestatement
import Hledger.Cli.Info
import Hledger.Cli.Man
import Hledger.Cli.Print
import Hledger.Cli.Register
import Hledger.Cli.Stats
import Hledger.Cli.CliOptions
import Hledger.Cli.Tests
import Hledger.Cli.Utils
import Hledger.Cli.Version
import Hledger.Data.Dates (getCurrentDay)
import Hledger.Data.RawOptions (RawOpts)
import Hledger.Reports.ReportOptions (period_, interval_, queryFromOpts)
import Hledger.Utils
mainmode addons = defMode {
modeNames = [progname ++ " [CMD]"]
,modeArgs = ([], Just $ argsFlag "[ARGS]")
,modeHelp = unlines ["hledger's command line interface"]
,modeGroupModes = Group {
groupUnnamed = [
]
,groupNamed = [
]
,groupHidden = [
oldconvertmode
,accountsmode
,activitymode
,addmode
,balancemode
,balancesheetmode
,cashflowmode
,helpmode
,incomestatementmode
,infomode
,manmode
,printmode
,registermode
,statsmode
,testmode
] ++ map quickAddonCommandMode addons
}
,modeGroupFlags = Group {
groupNamed = [
( "General input flags", inputflags)
,("\nGeneral reporting flags", reportflags)
,("\nGeneral help flags", helpflags)
]
,groupUnnamed = []
,groupHidden =
[detailedversionflag]
}
,modeHelpSuffix = lines $ regexReplace "PROGNAME" progname [here|Examples:
PROGNAME list commands
PROGNAME CMD [
PROGNAMECMD [OPTS] [ARGS] or run addon commands directly
PROGNAME h general usage
PROGNAME CMD h command usage
PROGNAME
PROGNAME
PROGNAME
PROGNAME help list help topics
PROGNAME help TOPIC TOPIC manual
PROGNAME man TOPIC TOPIC manual as man page
PROGNAME info TOPIC TOPIC manual as info manual
|]
}
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
argsToCliOpts :: [String] -> [String] -> IO CliOpts
argsToCliOpts args addons = do
let
args' = moveFlagsAfterCommand args
cmdargsopts = either usageError id $ process (mainmode addons) args'
cmdargsopts' = decodeRawOpts cmdargsopts
rawOptsToCliOpts cmdargsopts'
moveFlagsAfterCommand :: [String] -> [String]
moveFlagsAfterCommand args = moveArgs $ ensureDebugHasArg args
where
ensureDebugHasArg as =
case break (=="--debug") as of
(bs,"--debug":c:cs) | null c || not (all isDigit c) -> bs++"--debug=1":c:cs
(bs,"--debug":[]) -> bs++"--debug=1":[]
_ -> as
moveArgs (f:a:as) | isMovableNoArgFlag f = (moveArgs $ a:as) ++ [f]
moveArgs (f:v:a:as) | isMovableReqArgFlag f, isValue v = (moveArgs $ a:as) ++ [f,v]
moveArgs (fv:a:as) | isMovableReqArgFlagAndValue fv = (moveArgs $ a:as) ++ [fv]
moveArgs (f:a:as) | isMovableReqArgFlag f, not (isValue a) = (moveArgs $ a:as) ++ [f]
moveArgs 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 ('-':shortflag:_:_) = [shortflag] `elem` reqargflagstomove
isMovableReqArgFlagAndValue _ = False
isValue "-" = True
isValue ('-':_) = False
isValue _ = True
flagstomove = inputflags ++ reportflags ++ helpflags
noargflagstomove = concatMap flagNames $ filter ((==FlagNone).flagInfo) flagstomove
reqargflagstomove =
concatMap flagNames $ filter ((==FlagReq ).flagInfo) flagstomove
commandsListTemplate :: String
commandsListTemplate = [here|Commands available (COUNT):
Standard reports:
accounts show chart of accounts
balancesheet (bs) show a balance sheet
cashflow (cf) show a cashflow statement
incomestatement (is) show an income statement
transactions (txns) show transactions in some account
General reporting:
activity show a bar chart of posting counts per interval
balance (bal) show accounts and balances
budget add automated postings/txns/bucket accts (experimental)
chart generate simple balance pie charts (experimental)
check check more powerful balance assertions
checkdates check transactions are ordered by date
checkdupes check for accounts with the same leaf name
irr calculate internal rate of return of an investment
prices show market price records
print show transaction journal entries
printunique show only transactions with unique descriptions
register (reg) show postings and running total
registermatch show best matching transaction for a description
stats show some journal statistics
Interfaces:
add console ui for adding transactions
api web api server
iadd curses ui for adding transactions
ui curses ui
web web ui
Misc:
autosync download/deduplicate/convert OFX data
equity generate transactions to zero & restore account balances
interest generate interest transactions
rewrite add automated postings to certain transactions
test run some self tests
OTHERCMDS
Help: (see also h, CMD h, --help|--man|--info)
help|man|info show any of the hledger manuals in text/man/info format
|]
knownCommands :: [String]
knownCommands = sort $ commandsFromCommandsList commandsListTemplate
commandsFromCommandsList :: String -> [String]
commandsFromCommandsList s = concatMap (splitOn "|") [w | ' ':l <- lines s, let w:_ = words l]
printCommandsList :: [String] -> IO ()
printCommandsList addonsFound = putStr commandsList
where
commandsFound = builtinCommandNames ++ addonsFound
unknownCommandsFound = addonsFound \\ knownCommands
adjustline (' ':l) | not $ w `elem` commandsFound = []
where w = takeWhile (not . (`elem` "| ")) l
adjustline l = [l]
commandsList1 =
regexReplace "OTHERCMDS" (unlines [' ':w | w <- unknownCommandsFound]) $
unlines $ concatMap adjustline $ lines commandsListTemplate
commandsList =
regexReplace "COUNT" (show $ length $ commandsFromCommandsList commandsList1)
commandsList1
main :: IO ()
main = do
args <- getArgs
let
args' = moveFlagsAfterCommand args
isFlag = ("-" `isPrefixOf`)
isNonEmptyNonFlag s = not (isFlag s) && not (null s)
rawcmd = headDef "" $ takeWhile isNonEmptyNonFlag args'
isNullCommand = null rawcmd
(argsbeforecmd, argsaftercmd') = break (==rawcmd) args
argsaftercmd = drop 1 argsaftercmd'
dbgIO :: Show a => String -> a -> IO ()
dbgIO = tracePrettyAtIO 2
dbgIO "running" prognameandversion
dbgIO "raw args" args
dbgIO "raw args rearranged for cmdargs" args'
dbgIO "raw command is probably" rawcmd
dbgIO "raw args before command" argsbeforecmd
dbgIO "raw args after command" argsaftercmd
addons' <- hledgerAddons
let addons = filter (not . (`elem` builtinCommandNames) . dropExtension) addons'
opts <- argsToCliOpts args addons
let
cmd = command_ opts
isInternalCommand = cmd `elem` builtinCommandNames
isExternalCommand = not (null cmd) && cmd `elem` addons
isBadCommand = not (null rawcmd) && null cmd
hasVersion = ("--version" `elem`)
hasDetailedVersion = ("--version+" `elem`)
printUsage = putStr $ showModeUsage $ mainmode addons
badCommandError = error' ("command "++rawcmd++" is not recognized, run with no command to see a list") >> exitFailure
hasShortHelpFlag args = any (`elem` args) ["-h"]
hasLongHelpFlag args = any (`elem` args) ["--help"]
hasManFlag args = any (`elem` args) ["--man"]
hasInfoFlag args = any (`elem` args) ["--info"]
hasSomeHelpFlag args = hasShortHelpFlag args || hasLongHelpFlag args || hasManFlag args || hasInfoFlag args
f `orShowHelp` mode
| hasShortHelpFlag args = putStr $ showModeUsage mode
| hasLongHelpFlag args = printHelpForTopic t
| hasManFlag args = runManForTopic t
| hasInfoFlag args = runInfoForTopic t
| otherwise = f
where t = topicForMode mode
dbgIO "processed opts" opts
dbgIO "command matched" cmd
dbgIO "isNullCommand" isNullCommand
dbgIO "isInternalCommand" isInternalCommand
dbgIO "isExternalCommand" isExternalCommand
dbgIO "isBadCommand" isBadCommand
d <- getCurrentDay
dbgIO "period from opts" (period_ $ reportopts_ opts)
dbgIO "interval from opts" (interval_ $ reportopts_ opts)
dbgIO "query from opts & args" (queryFromOpts d $ reportopts_ opts)
let
runHledgerCommand
| hasShortHelpFlag argsbeforecmd = dbgIO "" "-h before command, showing general usage" >> printUsage
| hasLongHelpFlag argsbeforecmd = dbgIO "" "--help before command, showing general manual" >> printHelpForTopic (topicForMode $ mainmode addons)
| hasManFlag argsbeforecmd = dbgIO "" "--man before command, showing general manual with man" >> runManForTopic (topicForMode $ mainmode addons)
| hasInfoFlag argsbeforecmd = dbgIO "" "--info before command, showing general manual with info" >> runInfoForTopic (topicForMode $ mainmode addons)
| not (hasSomeHelpFlag argsaftercmd) && (hasVersion argsbeforecmd || (hasVersion argsaftercmd && isInternalCommand))
= putStrLn prognameandversion
| not (hasSomeHelpFlag argsaftercmd) && (hasDetailedVersion argsbeforecmd || (hasDetailedVersion argsaftercmd && isInternalCommand))
= putStrLn prognameanddetailedversion
| isNullCommand = dbgIO "" "no command, showing commands list" >> printCommandsList addons
| isBadCommand = badCommandError
| cmd == "activity" = withJournalDo opts histogram `orShowHelp` activitymode
| cmd == "add" = (journalFilePathFromOpts opts >>= (ensureJournalFileExists . head) >> withJournalDo opts add) `orShowHelp` addmode
| cmd == "accounts" = withJournalDo opts accounts `orShowHelp` accountsmode
| cmd == "balance" = withJournalDo opts balance `orShowHelp` balancemode
| cmd == "balancesheet" = withJournalDo opts balancesheet `orShowHelp` balancesheetmode
| cmd == "cashflow" = withJournalDo opts cashflow `orShowHelp` cashflowmode
| cmd == "incomestatement" = withJournalDo opts incomestatement `orShowHelp` incomestatementmode
| cmd == "print" = withJournalDo opts print' `orShowHelp` printmode
| cmd == "register" = withJournalDo opts register `orShowHelp` registermode
| cmd == "stats" = withJournalDo opts stats `orShowHelp` statsmode
| cmd == "test" = test' opts `orShowHelp` testmode
| cmd == "help" = help' opts `orShowHelp` helpmode
| cmd == "man" = man opts `orShowHelp` manmode
| cmd == "info" = info' opts `orShowHelp` infomode
| isExternalCommand = do
let externalargs = argsbeforecmd ++ filter (not.(=="--")) argsaftercmd
let shellcmd = printf "%s-%s %s" progname cmd (unwords' externalargs) :: String
dbgIO "external command selected" cmd
dbgIO "external command arguments" (map quoteIfNeeded externalargs)
dbgIO "running shell command" shellcmd
system shellcmd >>= exitWith
| cmd == "convert" = error' (modeHelp oldconvertmode) >> exitFailure
| otherwise = usageError ("could not understand the arguments "++show args) >> exitFailure
runHledgerCommand