739 lines
26 KiB
Haskell
739 lines
26 KiB
Haskell
{-# LANGUAGE TemplateHaskell, ScopedTypeVariables, DeriveDataTypeable #-}
|
|
{-|
|
|
|
|
Command-line options for the hledger program, and related utilities.
|
|
|
|
-}
|
|
|
|
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,
|
|
helpflags,
|
|
inputflags,
|
|
reportflags,
|
|
generalflagsgroup1,
|
|
generalflagsgroup2,
|
|
generalflagsgroup3,
|
|
|
|
-- * 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,
|
|
|
|
-- * CLI option accessors
|
|
-- | Some options require more processing. Possibly these should be merged into argsToCliOpts.
|
|
aliasesFromOpts,
|
|
formatFromOpts,
|
|
journalFilePathFromOpts,
|
|
rulesFilePathFromOpts,
|
|
OutputWidth(..),
|
|
Width(..),
|
|
defaultWidth,
|
|
defaultWidthWithFlag,
|
|
widthFromOpts,
|
|
|
|
-- * utilities
|
|
getHledgerAddonCommands,
|
|
argsToCliOpts,
|
|
moveFlagsAfterCommand,
|
|
decodeRawOpts,
|
|
checkCliOpts,
|
|
rawOptsToCliOpts,
|
|
optserror,
|
|
showModeHelp,
|
|
debugArgs,
|
|
getCliOpts,
|
|
|
|
-- * tests
|
|
tests_Hledger_Cli_Options
|
|
|
|
)
|
|
where
|
|
|
|
import qualified Control.Exception as C
|
|
-- import Control.Monad (filterM)
|
|
import Control.Monad (when)
|
|
import Data.List
|
|
import Data.List.Split
|
|
import Data.Maybe
|
|
import Data.PPrint (pprint)
|
|
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 Test.HUnit
|
|
import Text.ParserCombinators.Parsec as P
|
|
import Text.Printf
|
|
|
|
import Hledger
|
|
import Hledger.Data.FormatStrings as Format
|
|
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 help flags: --help, --debug, --version...
|
|
helpflags = [
|
|
flagNone ["help","h","?"] (setboolopt "help") "Display general help or (with --help after COMMAND) command help."
|
|
-- ,flagNone ["browse-args"] (setboolopt "browse-args") "use a web UI to select options and build up a command line"
|
|
,flagNone ["debug"] (setboolopt "debug") "Show extra debug output"
|
|
,flagNone ["version","V"] (setboolopt "version") "Print version information"
|
|
]
|
|
|
|
-- | Common input-related flags: --file, --rules-file, --alias...
|
|
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"
|
|
]
|
|
|
|
-- | Common report-related flags: --period, --cost, --display etc.
|
|
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"
|
|
]
|
|
|
|
argsFlag desc = flagArg (\s opts -> Right $ setopt "args" s opts) desc
|
|
|
|
generalflagstitle = "\nGeneral flags"
|
|
generalflagsgroup1 = (generalflagstitle, inputflags ++ reportflags ++ helpflags)
|
|
generalflagsgroup2 = (generalflagstitle, inputflags ++ helpflags)
|
|
generalflagsgroup3 = (generalflagstitle, helpflags)
|
|
|
|
-- cmdargs modes
|
|
|
|
-- | A basic mode template.
|
|
defMode :: Mode RawOpts
|
|
defMode = Mode {
|
|
modeNames = []
|
|
,modeHelp = ""
|
|
,modeHelpSuffix = []
|
|
,modeValue = []
|
|
,modeCheck = Right
|
|
,modeReform = const Nothing
|
|
,modeExpandAt = True
|
|
,modeGroupFlags = Group {
|
|
groupNamed = []
|
|
,groupUnnamed = [
|
|
flagNone ["help","h","?"] (setboolopt "help") "Display command help."
|
|
]
|
|
,groupHidden = []
|
|
}
|
|
,modeArgs = ([], Nothing)
|
|
,modeGroupModes = toGroup []
|
|
}
|
|
|
|
-- | A basic subcommand mode with the given command name(s).
|
|
defCommandMode names = defMode {
|
|
modeNames=names
|
|
,modeValue=[("command", headDef "" names)]
|
|
,modeArgs = ([], Just $ argsFlag "[PATTERNS]")
|
|
}
|
|
|
|
-- | A basic subcommand mode suitable for an add-on command.
|
|
defAddonCommandMode addon = defMode {
|
|
modeNames = [addon]
|
|
,modeHelp = printf "run %s-%s" progname addon
|
|
,modeValue=[("command",addon)]
|
|
,modeGroupFlags = Group {
|
|
groupUnnamed = []
|
|
,groupHidden = []
|
|
,groupNamed = [generalflagsgroup1]
|
|
}
|
|
,modeArgs = ([], Just $ argsFlag "[ARGS]")
|
|
}
|
|
|
|
-- | 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 ++ ")"
|
|
|
|
|
|
-- | 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"
|
|
-- -- ,"expressions which filter by account name. Prefix a pattern with desc: to"
|
|
-- -- ,"filter by transaction description instead, prefix with not: to negate it."
|
|
-- -- ,"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 ["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)
|
|
--
|
|
|
|
-- cli options, used in hledger and above
|
|
data CliOpts = CliOpts {
|
|
rawopts_ :: RawOpts
|
|
,command_ :: String
|
|
,file_ :: Maybe FilePath
|
|
,rules_file_ :: Maybe FilePath
|
|
,alias_ :: [String]
|
|
,debug_ :: Bool
|
|
,no_new_accounts_ :: Bool -- add
|
|
,width_ :: Maybe String -- register
|
|
,reportopts_ :: ReportOpts
|
|
} deriving (Show, Data, Typeable)
|
|
|
|
defcliopts = CliOpts
|
|
def
|
|
def
|
|
def
|
|
def
|
|
def
|
|
def
|
|
def
|
|
def
|
|
def
|
|
|
|
instance Default CliOpts where def = defcliopts
|
|
|
|
-- | 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
|
|
return defcliopts {
|
|
rawopts_ = rawopts
|
|
,command_ = stringopt "command" rawopts
|
|
,file_ = maybestringopt "file" rawopts
|
|
,rules_file_ = maybestringopt "rules-file" rawopts
|
|
,alias_ = map stripquotes $ listofstringopt "alias" rawopts
|
|
,debug_ = boolopt "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
|
|
,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
|
|
}
|
|
}
|
|
|
|
-- | 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 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
|
|
case formatFromOpts ropts of
|
|
Left err -> optserror $ "could not parse format option: "++err
|
|
Right _ -> return ()
|
|
case widthFromOpts opts of
|
|
Left err -> optserror $ "could not parse width option: "++err
|
|
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
|
|
|
|
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
|
|
|
|
maybedisplayopt :: Day -> RawOpts -> Maybe DisplayExp
|
|
maybedisplayopt d rawopts =
|
|
maybe Nothing (Just . regexReplaceBy "\\[.+?\\]" fixbracketeddatestr) $ maybestringopt "display" rawopts
|
|
where
|
|
fixbracketeddatestr "" = ""
|
|
fixbracketeddatestr s = "[" ++ fixSmartDateStr d (init $ tail s) ++ "]"
|
|
|
|
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
|
|
|
|
-- | 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_
|
|
|
|
-- | Default line format for balance report: "%20(total) %2(depth_spacer)%-(account)"
|
|
defaultBalanceFormatString :: [FormatString]
|
|
defaultBalanceFormatString = [
|
|
FormatField False (Just 20) Nothing TotalField
|
|
, FormatLiteral " "
|
|
, FormatField True (Just 2) Nothing DepthSpacerField
|
|
, FormatField True Nothing Nothing AccountField
|
|
]
|
|
|
|
-- | Output width configuration (for register).
|
|
data OutputWidth =
|
|
TotalWidth Width -- ^ specify the overall width
|
|
| FieldWidths [Width] -- ^ specify each field's width
|
|
deriving Show
|
|
|
|
-- | A width value.
|
|
data Width =
|
|
Width Int -- ^ set width to exactly this number of characters
|
|
| Auto -- ^ set width automatically from available space
|
|
deriving Show
|
|
|
|
-- | Default width of hledger console output.
|
|
defaultWidth = 80
|
|
|
|
-- | Width of hledger console output when the -w flag is used with no value.
|
|
defaultWidthWithFlag = 120
|
|
|
|
-- | Parse the width option if provided, possibly returning an error,
|
|
-- otherwise get the default value.
|
|
widthFromOpts :: CliOpts -> Either String OutputWidth
|
|
widthFromOpts CliOpts{width_=Nothing} = Right $ TotalWidth $ Width defaultWidth
|
|
widthFromOpts CliOpts{width_=Just ""} = Right $ TotalWidth $ Width defaultWidthWithFlag
|
|
widthFromOpts CliOpts{width_=Just s} = parseWidth s
|
|
|
|
parseWidth :: String -> Either String OutputWidth
|
|
parseWidth s = case (runParser outputwidthp () "(unknown)") s of
|
|
Left e -> Left $ show e
|
|
Right x -> Right x
|
|
|
|
outputwidthp :: GenParser Char st OutputWidth
|
|
outputwidthp =
|
|
try (do w <- widthp
|
|
ws <- many1 (char ',' >> widthp)
|
|
return $ FieldWidths $ w:ws)
|
|
<|> TotalWidth `fmap` widthp
|
|
|
|
widthp :: GenParser Char st Width
|
|
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
|
|
|
|
-- | 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 (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 a mode's help message as a nicely wrapped string.
|
|
showModeHelp :: Mode a -> String
|
|
showModeHelp =
|
|
(showText defaultWrap :: [Text] -> String)
|
|
.
|
|
(helpText [] HelpFormatDefault :: Mode a -> [Text])
|
|
|
|
-- | 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
|
|
putStrLn . show =<< pprint 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_Hledger_Cli_Options = TestList
|
|
[
|
|
]
|