module Hledger.Cli.Main where
import Data.Char (isDigit)
import Data.List
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, optserror)
import Hledger.Reports.ReportOptions (period_, interval_, queryFromOpts)
import Hledger.Utils
mainmode addons = defMode {
modeNames = [progname]
,modeHelp = unlines []
,modeHelpSuffix = [""]
,modeArgs = ([], Just $ argsFlag "[ARGS]")
,modeGroupModes = Group {
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)]
,groupUnnamed = [
helpmode
,manmode
,infomode
]
,groupHidden = [
testmode
,oldconvertmode
]
}
,modeGroupFlags = Group {
groupNamed = [generalflagsgroup3]
,groupUnnamed = []
,groupHidden =
detailedversionflag :
inputflags
}
}
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 = processValue (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 ++ helpflags
noargflagstomove = concatMap flagNames $ filter ((==FlagNone).flagInfo) flagstomove
reqargflagstomove =
concatMap flagNames $ filter ((==FlagReq ).flagInfo) flagstomove
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
(addonPreciseNames', addonDisplayNames') <- hledgerAddons
let addonPreciseNames = filter (not . (`elem` builtinCommandNames) . dropExtension) addonPreciseNames'
let addonDisplayNames = filter (not . (`elem` builtinCommandNames)) addonDisplayNames'
opts <- argsToCliOpts args addonPreciseNames
let
cmd = command_ opts
isInternalCommand = cmd `elem` builtinCommandNames
isExternalCommand = not (null cmd) && cmd `elem` addonPreciseNames
isBadCommand = not (null rawcmd) && null cmd
hasVersion = ("--version" `elem`)
hasDetailedVersion = ("--version+" `elem`)
printUsage = putStr $ showModeUsage $ mainmode addonDisplayNames
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 addonDisplayNames)
| hasManFlag argsbeforecmd = dbgIO "" "--man before command, showing general manual with man" >> runManForTopic (topicForMode $ mainmode addonDisplayNames)
| hasInfoFlag argsbeforecmd = dbgIO "" "--info before command, showing general manual with info" >> runInfoForTopic (topicForMode $ mainmode addonDisplayNames)
| 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 general usage" >> printUsage
| 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 = optserror ("could not understand the arguments "++show args) >> exitFailure
runHledgerCommand