cli: first of several cleanups; separate commands list & usage (#297)

This commit is contained in:
Simon Michael 2017-03-28 18:39:35 -07:00
parent e24eb155e7
commit 895a66eb06
5 changed files with 180 additions and 74 deletions

View File

@ -102,8 +102,8 @@ import Hledger.Cli.Version
-- | Common help flags: --help, --debug, --version... -- | Common help flags: --help, --debug, --version...
helpflags :: [Flag RawOpts] helpflags :: [Flag RawOpts]
helpflags = [ helpflags = [
flagNone ["h"] (setboolopt "h") "show general usage or (after COMMAND, the command's usage" flagNone ["h"] (setboolopt "h") "show general usage or (after CMD, the command's usage"
,flagNone ["help"] (setboolopt "help") "show the current program's manual as plain text (or after an add-on COMMAND, the add-on's manual)" ,flagNone ["help"] (setboolopt "help") "show the current program's manual as plain text (or after an addon CMD, the add-on's manual)"
,flagNone ["man"] (setboolopt "man") "show the current program's manual with man" ,flagNone ["man"] (setboolopt "man") "show the current program's manual with man"
,flagNone ["info"] (setboolopt "info") "show the current program's manual with info" ,flagNone ["info"] (setboolopt "info") "show the current program's manual with info"
-- ,flagNone ["browse-args"] (setboolopt "browse-args") "use a web UI to select options and build up a command line" -- ,flagNone ["browse-args"] (setboolopt "browse-args") "use a web UI to select options and build up a command line"

View File

@ -36,11 +36,15 @@ See "Hledger.Data.Ledger" for more examples.
-} -}
{-# LANGUAGE QuasiQuotes #-}
module Hledger.Cli.Main where module Hledger.Cli.Main where
-- import Control.Monad -- import Control.Monad
import Data.Char (isDigit) import Data.Char (isDigit)
import Data.String.Here
import Data.List import Data.List
import Data.List.Split (splitOn)
import Safe import Safe
import System.Console.CmdArgs.Explicit as C import System.Console.CmdArgs.Explicit as C
import System.Environment import System.Environment
@ -76,52 +80,63 @@ import Hledger.Utils
-- | The overall cmdargs mode describing command-line options for hledger. -- | The overall cmdargs mode describing command-line options for hledger.
mainmode addons = defMode { mainmode addons = defMode {
modeNames = [progname] modeNames = [progname ++ " [CMD]"]
,modeHelp = unlines []
,modeHelpSuffix = [""]
,modeArgs = ([], Just $ argsFlag "[ARGS]") ,modeArgs = ([], Just $ argsFlag "[ARGS]")
,modeHelp = unlines ["hledger's command line interface"]
,modeGroupModes = Group { ,modeGroupModes = Group {
-- modes (commands) in named groups: -- subcommands in the unnamed group, shown first:
groupNamed = [ groupUnnamed = [
("Data entry commands", [
addmode
])
,("\nReporting commands", [
printmode
,accountsmode
,balancemode
,registermode
,incomestatementmode
,balancesheetmode
,cashflowmode
,activitymode
,statsmode
])
] ]
++ case addons of [] -> [] -- subcommands in named groups:
cs -> [("\nAdd-on commands", map quickAddonCommandMode cs)] ,groupNamed = [
-- modes in the unnamed group, shown first without a heading:
,groupUnnamed = [
helpmode
,manmode
,infomode
] ]
-- modes handled but not shown -- subcommands handled but not shown in the help:
,groupHidden = [ ,groupHidden = [
testmode oldconvertmode
,oldconvertmode ,accountsmode
] ,activitymode
,addmode
,balancemode
,balancesheetmode
,cashflowmode
,helpmode
,incomestatementmode
,infomode
,manmode
,printmode
,registermode
,statsmode
,testmode
] ++ map quickAddonCommandMode addons
} }
,modeGroupFlags = Group { ,modeGroupFlags = Group {
-- flags in named groups: -- flags in named groups:
groupNamed = [generalflagsgroup3] groupNamed = [
-- flags in the unnamed group, shown last without a heading: ( "General input flags", inputflags)
,("\nGeneral reporting flags", reportflags)
,("\nGeneral help flags", helpflags)
]
-- flags in the unnamed group, shown last:
,groupUnnamed = [] ,groupUnnamed = []
-- flags accepted but not shown in the help: -- flags handled but not shown in the help:
,groupHidden = ,groupHidden =
detailedversionflag : [detailedversionflag]
inputflags -- included here so they'll not raise a confusing error if present with no COMMAND -- ++ inputflags -- included here so they'll not raise a confusing error if present with no COMMAND
} }
,modeHelpSuffix = lines $ regexReplace "PROGNAME" progname [here|Examples:
PROGNAME list commands
PROGNAME CMD [--] [OPTS] [ARGS] run a command (use -- with addon commands)
PROGNAME-CMD [OPTS] [ARGS] or run addon commands directly
PROGNAME -h hledger usage
PROGNAME CMD -h command usage
PROGNAME --help PROGNAME manual
PROGNAME --man PROGNAME manual as man page
PROGNAME --info PROGNAME manual as info manual
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"]) { oldconvertmode = (defCommandMode ["convert"]) {
@ -160,8 +175,8 @@ argsToCliOpts args addons = do
-- --
-- Since we're not parsing flags as precisely as cmdargs here, this is -- Since we're not parsing flags as precisely as cmdargs here, this is
-- imperfect. We make a decent effort to: -- imperfect. We make a decent effort to:
-- - move all no-argument help and input flags -- - move all no-argument help/input/report flags
-- - move all required-argument help and input flags along with their values, space-separated or not -- - move all required-argument help/input/report flags along with their values, space-separated or not
-- - not confuse things further or cause misleading errors. -- - not confuse things further or cause misleading errors.
moveFlagsAfterCommand :: [String] -> [String] moveFlagsAfterCommand :: [String] -> [String]
moveFlagsAfterCommand args = moveArgs $ ensureDebugHasArg args moveFlagsAfterCommand args = moveArgs $ ensureDebugHasArg args
@ -197,11 +212,93 @@ isValue "-" = True
isValue ('-':_) = False isValue ('-':_) = False
isValue _ = True isValue _ = True
flagstomove = inputflags ++ helpflags flagstomove = inputflags ++ reportflags ++ helpflags
noargflagstomove = concatMap flagNames $ filter ((==FlagNone).flagInfo) flagstomove noargflagstomove = concatMap flagNames $ filter ((==FlagNone).flagInfo) flagstomove
reqargflagstomove = -- filter (/= "debug") $ reqargflagstomove = -- filter (/= "debug") $
concatMap flagNames $ filter ((==FlagReq ).flagInfo) flagstomove concatMap flagNames $ filter ((==FlagReq ).flagInfo) flagstomove
-- | Template for the commands list. Includes an entry for known (or
-- hypothetical) builtin and addon commands; these will be filtered
-- based on the commands found at runtime. COUNT is replaced with the
-- number of commands found. OTHERCMDS is replaced with an entry for
-- each unknown addon command found. The command descriptions here
-- should be synced with the commands' builtin help and the command
-- list in the hledger manual.
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
check-dates check transactions are ordered by date
check-dupes 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
print-unique show only transactions with unique descriptions
register (reg) show postings and running total
register-match 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
-- | Extract the command names from a commands list like the above:
-- the first word (or words separated by |) of lines beginning with a space.
commandsFromCommandsList :: String -> [String]
commandsFromCommandsList s = concatMap (splitOn "|") [w | ' ':l <- lines s, let w:_ = words l]
-- | Print the commands list, modifying the template above based on
-- the currently available addons. Missing addons will be removed, and
-- extra addons will be added under Misc.
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" (init $ unlines [' ':w | w <- unknownCommandsFound]) $
unlines $ concatMap adjustline $ lines commandsListTemplate
commandsList =
regexReplace "COUNT" (show $ length $ commandsFromCommandsList commandsList1)
commandsList1
-- | Let's go. -- | Let's go.
main :: IO () main :: IO ()
main = do main = do
@ -234,21 +331,21 @@ main = do
dbgIO "raw args after command" argsaftercmd dbgIO "raw args after command" argsaftercmd
-- Search PATH for add-ons, excluding any that match built-in command names -- Search PATH for add-ons, excluding any that match built-in command names
addonNames' <- hledgerAddons addons' <- hledgerAddons
let addonNames = filter (not . (`elem` builtinCommandNames) . dropExtension) addonNames' let addons = filter (not . (`elem` builtinCommandNames) . dropExtension) addons'
-- parse arguments with cmdargs -- parse arguments with cmdargs
opts <- argsToCliOpts args addonNames opts <- argsToCliOpts args addons
-- select an action and run it. -- select an action and run it.
let let
cmd = command_ opts -- the full matched internal or external command name, if any cmd = command_ opts -- the full matched internal or external command name, if any
isInternalCommand = cmd `elem` builtinCommandNames -- not (null cmd) && not (cmd `elem` addons) isInternalCommand = cmd `elem` builtinCommandNames -- not (null cmd) && not (cmd `elem` addons)
isExternalCommand = not (null cmd) && cmd `elem` addonNames -- probably isExternalCommand = not (null cmd) && cmd `elem` addons -- probably
isBadCommand = not (null rawcmd) && null cmd isBadCommand = not (null rawcmd) && null cmd
hasVersion = ("--version" `elem`) hasVersion = ("--version" `elem`)
hasDetailedVersion = ("--version+" `elem`) hasDetailedVersion = ("--version+" `elem`)
printUsage = putStr $ showModeUsage $ mainmode addonNames printUsage = putStr $ showModeUsage $ mainmode addons
badCommandError = error' ("command "++rawcmd++" is not recognized, run with no command to see a list") >> exitFailure badCommandError = error' ("command "++rawcmd++" is not recognized, run with no command to see a list") >> exitFailure
hasShortHelpFlag args = any (`elem` args) ["-h"] hasShortHelpFlag args = any (`elem` args) ["-h"]
hasLongHelpFlag args = any (`elem` args) ["--help"] hasLongHelpFlag args = any (`elem` args) ["--help"]
@ -276,16 +373,16 @@ main = do
runHledgerCommand runHledgerCommand
-- high priority flags and situations. -h, then --help, then --info are highest priority. -- high priority flags and situations. -h, then --help, then --info are highest priority.
| hasShortHelpFlag argsbeforecmd = dbgIO "" "-h before command, showing general usage" >> printUsage | hasShortHelpFlag argsbeforecmd = dbgIO "" "-h before command, showing general usage" >> printUsage
| hasLongHelpFlag argsbeforecmd = dbgIO "" "--help before command, showing general manual" >> printHelpForTopic (topicForMode $ mainmode addonNames) | 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 addonNames) | 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 addonNames) | hasInfoFlag argsbeforecmd = dbgIO "" "--info before command, showing general manual with info" >> runInfoForTopic (topicForMode $ mainmode addons)
| not (hasSomeHelpFlag argsaftercmd) && (hasVersion argsbeforecmd || (hasVersion argsaftercmd && isInternalCommand)) | not (hasSomeHelpFlag argsaftercmd) && (hasVersion argsbeforecmd || (hasVersion argsaftercmd && isInternalCommand))
= putStrLn prognameandversion = putStrLn prognameandversion
| not (hasSomeHelpFlag argsaftercmd) && (hasDetailedVersion argsbeforecmd || (hasDetailedVersion argsaftercmd && isInternalCommand)) | not (hasSomeHelpFlag argsaftercmd) && (hasDetailedVersion argsbeforecmd || (hasDetailedVersion argsaftercmd && isInternalCommand))
= putStrLn prognameanddetailedversion = putStrLn prognameanddetailedversion
-- \| (null externalcmd) && "binary-filename" `inRawOpts` rawopts = putStrLn $ binaryfilename progname -- \| (null externalcmd) && "binary-filename" `inRawOpts` rawopts = putStrLn $ binaryfilename progname
-- \| "--browse-args" `elem` args = System.Console.CmdArgs.Helper.execute "cmdargs-browser" mainmode' args >>= (putStr . show) -- \| "--browse-args" `elem` args = System.Console.CmdArgs.Helper.execute "cmdargs-browser" mainmode' args >>= (putStr . show)
| isNullCommand = dbgIO "" "no command, showing general usage" >> printUsage | isNullCommand = dbgIO "" "no command, showing commands list" >> printCommandsList addons
| isBadCommand = badCommandError | isBadCommand = badCommandError
-- internal commands -- internal commands

View File

@ -87,6 +87,7 @@ library
, directory , directory
, file-embed >=0.0.10 && <0.1 , file-embed >=0.0.10 && <0.1
, filepath , filepath
, here
, pretty-show >=1.6.4 , pretty-show >=1.6.4
, process , process
, temporary , temporary
@ -171,6 +172,7 @@ executable hledger
, directory , directory
, file-embed >=0.0.10 && <0.1 , file-embed >=0.0.10 && <0.1
, filepath , filepath
, here
, pretty-show >=1.6.4 , pretty-show >=1.6.4
, process , process
, temporary , temporary
@ -232,6 +234,7 @@ test-suite test
, directory , directory
, file-embed >=0.0.10 && <0.1 , file-embed >=0.0.10 && <0.1
, filepath , filepath
, here
, pretty-show >=1.6.4 , pretty-show >=1.6.4
, process , process
, temporary , temporary
@ -292,6 +295,7 @@ benchmark bench
, directory , directory
, file-embed >=0.0.10 && <0.1 , file-embed >=0.0.10 && <0.1
, filepath , filepath
, here
, pretty-show >=1.6.4 , pretty-show >=1.6.4
, process , process
, temporary , temporary

View File

@ -68,6 +68,7 @@ dependencies:
- directory - directory
- file-embed >=0.0.10 && <0.1 - file-embed >=0.0.10 && <0.1
- filepath - filepath
- here
- pretty-show >=1.6.4 - pretty-show >=1.6.4
- process - process
- temporary - temporary

View File

@ -71,63 +71,67 @@ hledger balance --version
# help # help
# 3. with no command, show general help # 3. with no command, show commands list
hledger hledger
>>> /^hledger \[COMMAND\]/ >>> /^Commands available/
>>>=0 >>>=0
# 4. no-command help still works if there are flags, at least the common ones # 4. no-command help still works if there are flags, at least the common ones
hledger -fsomefile hledger -fsomefile
>>> /^hledger \[COMMAND\]/ >>> /^Commands available/
>>>=0 >>>=0
# 5. and also with a space between flag and value # 5. and also with a space between flag and value
hledger -f somefile hledger -f somefile
>>> /^hledger \[COMMAND\]/ >>> /^Commands available/
>>>=0 >>>=0
# 6. with -h, and possibly other common flags present, show general usage # 6. with -h, and possibly other common flags present, show general usage
hledger -h --version -f /dev/null hledger -h --version -f /dev/null
>>> /^hledger \[COMMAND\]/ >>> /^hledger \[CMD\]/
>>>=0 >>>=0
# 7. with -h before COMMAND, show general usage # 7. with -h before COMMAND, show general usage
hledger -h balance --cost hledger -h balance --cost
>>> /^hledger \[COMMAND\]/ >>> /^hledger \[CMD\]/
>>>=0 >>>=0
# 8. with -h after command, show command usage # 8. with -h after command, show command usage
hledger balance -h hledger balance -h
>>> /^balance \[OPTIONS\]/ >>> /balance \[OPTIONS\]/
>>>=0 >>>=0
# 9. should work with deprecated commands too # 9. with an unrecognised command, give an error and non-zero exit status
hledger convert -h
>>>
>>>2 /no longer needed/
>>>=1
# 10. with an unrecognised command, give general usage and non-zero exit status
hledger nosuchcommand hledger nosuchcommand
>>> >>>
>>>2 /not recognized/ >>>2 /not recognized.*to see a list/
>>>=1 >>>=1
# flag positions # flag positions
# 11. most flags can not go before command # 10. general flags can go before command
hledger --daily register hledger -f /dev/null --alias somealiases --rules-file -h --help --version --debug 1 --daily register
>>> >>> /^hledger \[CMD\]/
>>>2 /Unknown flag: --daily/
>>>=1
# 12. help and input flags can go before command
hledger -f /dev/null --alias somealiases --rules-file -h --help --version --debug 1 register --daily
>>> /^hledger \[COMMAND\]/
>>>=0 >>>=0
# 13. or after it, and spaces in options are optional # 11. or after it, and spaces in options are optional
hledger register -f/dev/null --alias=somealiases --rules-file -h --version --debug 1 --daily hledger register -f/dev/null --alias=somealiases --rules-file -h --version --debug 1 --daily
>>> /^register \[OPTIONS\]/ >>> /^register \[OPTIONS\]/
>>>=0 >>>=0
# 12. general flags before command should work
hledger -f /dev/null --daily register
>>>
>>>=0
# 13. command-specific flags can go after command
hledger -f /dev/null register --daily
>>>
>>>=0
# 14. but not before it
hledger --related register
>>>
>>>2 /Unknown flag: --related/
>>>=1