imp: cli: config files can now provide the command name
If the first thing in a config file's general section is a non-flag argument, that will be used as the command name argument, taking precedence over any command line arguments.
This commit is contained in:
parent
a928ed994b
commit
46897cd30b
@ -96,7 +96,7 @@ import Data.Function ((&))
|
||||
import Data.Functor ((<&>))
|
||||
import Data.List
|
||||
import qualified Data.List.NonEmpty as NE
|
||||
import Data.Maybe (isJust)
|
||||
import Data.Maybe (isJust, fromMaybe)
|
||||
import Data.Text (pack, Text)
|
||||
import Data.Time.Clock.POSIX (getPOSIXTime)
|
||||
import Safe
|
||||
@ -191,6 +191,7 @@ main :: IO ()
|
||||
main = withGhcDebug' $ do
|
||||
|
||||
-- 0. let's go!
|
||||
|
||||
let
|
||||
-- Trace helpers. These always trace to stderr, even when running `hledger ui`;
|
||||
-- that's ok as conf is a hledger cli feature for now.
|
||||
@ -198,24 +199,24 @@ main = withGhcDebug' $ do
|
||||
dbgIO = ptraceAtIO verboseDebugLevel
|
||||
dbgIO1 = ptraceAtIO 1
|
||||
dbgIO2 = ptraceAtIO 2
|
||||
|
||||
dbgIO "running" prognameandversion
|
||||
|
||||
starttime <- getPOSIXTime
|
||||
|
||||
-- give ghc-debug a chance to take control
|
||||
when (ghcDebugMode == GDPauseAtStart) $ ghcDebugPause'
|
||||
|
||||
-- try to encourage user's $PAGER to display ANSI when supported
|
||||
when useColorOnStdout setupPager
|
||||
|
||||
-- Search PATH for addon commands. Exclude any that match builtin command names.
|
||||
addons <- hledgerAddons <&> filter (not . (`elem` builtinCommandNames) . dropExtension)
|
||||
|
||||
---------------------------------------------------------------
|
||||
-- 1. Preliminary command line parsing.
|
||||
|
||||
dbgIO "\n1. Preliminary command line parsing" ()
|
||||
|
||||
-- Naming notes:
|
||||
-- "arg" often has the most general meaning, including things like: -f, --flag, flagvalue, arg, >file, &, etc.
|
||||
-- confcmdarg, clicmdarg = the first non-flag argument, from config file or cli = the subcommand name
|
||||
-- cmdname = the full unabbreviated command name, or ""
|
||||
-- confcmdargs = arguments for the subcommand, from config file
|
||||
|
||||
-- Do some argument preprocessing to help cmdargs
|
||||
cliargs <- getArgs
|
||||
>>= expandArgsAt -- interpolate @ARGFILEs
|
||||
@ -226,144 +227,167 @@ main = withGhcDebug' $ do
|
||||
(cliargsbeforecmd, cliargsaftercmd) = second (drop 1) $ break (==clicmdarg) cliargs
|
||||
dbgIO "cli args" cliargs
|
||||
dbg1IO "cli args with command first, if any" cliargswithcmdfirst
|
||||
dbgIO "command argument found" clicmdarg
|
||||
dbgIO "cli args before command" cliargsbeforecmd
|
||||
dbgIO "cli args after command" cliargsaftercmd
|
||||
dbgIO "cli command argument found" clicmdarg
|
||||
dbgIO "cli args before command" cliargsbeforecmd
|
||||
dbgIO "cli args after command" cliargsaftercmd
|
||||
dbgIO "cli args without command" cliargswithoutcmd
|
||||
|
||||
-- Now try to identify the full subcommand name, so we can look for
|
||||
-- command-specific options in config files (clicmdarg may be only an abbreviation).
|
||||
-- For this do a preliminary cmdargs parse of the arguments with cli-specific options removed.
|
||||
-- If no command was provided, or if the command line contains a bad flag
|
||||
-- or a wrongly present/missing flag argument, cmd will be "".
|
||||
---------------------------------------------------------------
|
||||
dbgIO "\n2. Read the config file if any" ()
|
||||
|
||||
-- Identify any --conf/--no-conf options.
|
||||
-- Run cmdargs on just the args that look conf-related.
|
||||
let
|
||||
rawopts1 = cmdargsParse
|
||||
"for command name"
|
||||
(mainmode addons)
|
||||
cliargswithcmdfirstwithoutclispecific
|
||||
cmd = stringopt "command" rawopts1
|
||||
-- XXX better error message when cmdargs fails (eg spaced/quoted/malformed flag values) ?
|
||||
nocmdprovided = null clicmdarg
|
||||
badcmdprovided = null cmd && not nocmdprovided
|
||||
isaddoncmd = not (null cmd) && cmd `elem` addons
|
||||
-- isbuiltincmd = cmd `elem` builtinCommandNames
|
||||
mcmdmodeaction = findBuiltinCommand cmd
|
||||
effectivemode = maybe (mainmode []) fst mcmdmodeaction
|
||||
cliconfargs = dropUnsupportedOpts confflagsmode cliargswithoutcmd
|
||||
cliconfrawopts = cmdargsParse "for conf options" confflagsmode cliconfargs
|
||||
|
||||
-- Read extra general and command-specific args/opts from the config file, if any.
|
||||
(conf, mconffile) <-
|
||||
seq cliconfrawopts $ -- order debug output
|
||||
getConf cliconfrawopts
|
||||
|
||||
---------------------------------------------------------------
|
||||
dbgIO "\n3. Identify a command name from config file or command line" ()
|
||||
|
||||
-- Try to identify the subcommand name,
|
||||
-- from the first non-flag general argument in the config file,
|
||||
-- or if there is none, from the first non-flag argument on the command line.
|
||||
|
||||
let
|
||||
confallgenargs = confLookup "general" conf
|
||||
-- we don't try to move flags/values preceding a command argument here;
|
||||
-- if a command name is written in the config file, it must be first
|
||||
(confcmdarg, confothergenargs) = case confallgenargs of
|
||||
a:as | not $ isFlagArg a -> (a,as)
|
||||
as -> ("",as)
|
||||
cmdarg = if not $ null confcmdarg then confcmdarg else clicmdarg
|
||||
nocmdprovided = null cmdarg
|
||||
|
||||
-- The argument may be an abbreviated command name.
|
||||
-- Run cmdargs on conf + cli args to get the full command name.
|
||||
-- If no command argument was provided, or if cmdargs fails because
|
||||
-- the command line contains a bad flag or wrongly present/missing flag value,
|
||||
-- cmdname will be "".
|
||||
args = confallgenargs <> cliargswithcmdfirstwithoutclispecific
|
||||
cmdname = stringopt "command" $ cmdargsParse "for command name" (mainmode addons) args
|
||||
badcmdprovided = null cmdname && not nocmdprovided
|
||||
isaddoncmd = not (null cmdname) && cmdname `elem` addons
|
||||
|
||||
-- And get the builtin command's action, if any.
|
||||
mbuiltincmdaction = findBuiltinCommand cmdname
|
||||
effectivemode = maybe (mainmode []) fst mbuiltincmdaction
|
||||
|
||||
when (isJust mconffile) $ do
|
||||
unless (null confcmdarg) $
|
||||
dbgIO1 "using command name argument from config file" confcmdarg
|
||||
dbgIO "cli args with command first and no cli-specific opts" cliargswithcmdfirstwithoutclispecific
|
||||
dbgIO1 "command found" cmd
|
||||
dbgIO1 "command found" cmdname
|
||||
dbgIO "no command provided" nocmdprovided
|
||||
dbgIO "bad command provided" badcmdprovided
|
||||
dbgIO "is addon command" isaddoncmd
|
||||
|
||||
---------------------------------------------------------------
|
||||
-- 2. Read extra options from a config file.
|
||||
dbgIO "\n4. Get applicable options/arguments from config file" ()
|
||||
|
||||
dbgIO "\n2. Read options from a config file" ()
|
||||
-- Identify any --conf/--no-conf options.
|
||||
-- For this parse with cmdargs a second time, this time with just the args that look conf-related.
|
||||
let cliconfargs = dropUnsupportedOpts confflagsmode cliargswithoutcmd
|
||||
dbgIO "cli args without command" cliargswithoutcmd
|
||||
-- dbgIO "cli conf args" cliconfargs
|
||||
let rawopts2 = cmdargsParse "for conf options" confflagsmode cliconfargs
|
||||
|
||||
-- Read extra general and command-specific args/opts from the config file if found.
|
||||
-- Ignore any general opts or cli-specific opts not known to be supported by the command.
|
||||
(conf, mconffile) <- getConf rawopts2
|
||||
let
|
||||
genargsfromconf = confLookup "general" conf
|
||||
addoncmdssupportinggenopts = ["ui", "web"] -- addons known to support hledger general options
|
||||
supportedgenargsfromconf
|
||||
| cmd `elem` addoncmdssupportinggenopts =
|
||||
[a | a <- genargsfromconf, not $ any (`isPrefixOf` a) addoncmdssupportinggenopts]
|
||||
| cmdname `elem` addoncmdssupportinggenopts =
|
||||
[a | a <- confothergenargs, not $ any (`isPrefixOf` a) addoncmdssupportinggenopts]
|
||||
| isaddoncmd = []
|
||||
| otherwise = dropUnsupportedOpts effectivemode genargsfromconf
|
||||
excludedgenargsfromconf = genargsfromconf \\ supportedgenargsfromconf
|
||||
cmdargsfromconf
|
||||
| null cmd = []
|
||||
| otherwise = confLookup cmd conf & if isaddoncmd then ("--":) else id
|
||||
| otherwise = dropUnsupportedOpts effectivemode confothergenargs
|
||||
excludedgenargsfromconf = confothergenargs \\ supportedgenargsfromconf
|
||||
confcmdargs
|
||||
| null cmdname = []
|
||||
| otherwise = confLookup cmdname conf & if isaddoncmd then ("--":) else id
|
||||
|
||||
when (isJust mconffile) $ do
|
||||
dbgIO1 "using extra general args from config file" genargsfromconf
|
||||
dbgIO1 "using general args from config file" confothergenargs
|
||||
unless (null excludedgenargsfromconf) $
|
||||
dbgIO1 "excluded general args from config file, not supported by this command" excludedgenargsfromconf
|
||||
dbgIO1 "using extra command args from config file" cmdargsfromconf
|
||||
dbgIO1 "using subcommand args from config file" confcmdargs
|
||||
|
||||
---------------------------------------------------------------
|
||||
-- 3. Combine cli and config file args and parse with cmdargs a third time.
|
||||
-- A bad flag or flag argument will cause the program to exit with an error here.
|
||||
dbgIO "\n5. Combine config file and command line args" ()
|
||||
|
||||
dbgIO "\n3. Combine command line and config file args" ()
|
||||
let
|
||||
finalargs =
|
||||
(if null clicmdarg then [] else [clicmdarg]) <> supportedgenargsfromconf <> cmdargsfromconf <> cliargswithoutcmd
|
||||
[cmdarg | not $ null cmdarg] <> supportedgenargsfromconf <> confcmdargs <> cliargswithoutcmd
|
||||
& replaceNumericFlags -- convert any -NUM opts from the config file
|
||||
-- finalargs' <- expandArgsAt finalargs -- expand @ARGFILEs in the config file ? don't bother
|
||||
let rawopts3 = cmdargsParse "for all options" (mainmode addons) finalargs
|
||||
dbgIO1 "final args" finalargs
|
||||
|
||||
-- Run cmdargs on command name + supported conf general args + conf subcommand args + cli args to get the final options.
|
||||
-- A bad flag or flag argument will cause the program to exit with an error here.
|
||||
let rawopts = cmdargsParse "final command line" (mainmode addons) finalargs
|
||||
|
||||
---------------------------------------------------------------
|
||||
-- 4. Finally, select an action and run it.
|
||||
seq rawopts $ -- order debug output
|
||||
dbgIO "\n6. Select an action and run it" ()
|
||||
|
||||
dbgIO "\n4. Select an action" ()
|
||||
-- We check for the help/doc/version flags first, since they are a high priority.
|
||||
-- (A perfectionist might think they should be so high priority that adding -h
|
||||
-- to an invalid command line would show help. But cmdargs tends to fail first,
|
||||
-- preventing this, and trying to detect them without cmdargs, and always do the
|
||||
-- right thing with builtin commands and addon commands, gets much too complicated.)
|
||||
let
|
||||
helpFlag = boolopt "help" rawopts3
|
||||
tldrFlag = boolopt "tldr" rawopts3
|
||||
infoFlag = boolopt "info" rawopts3
|
||||
manFlag = boolopt "man" rawopts3
|
||||
versionFlag = boolopt "version" rawopts3
|
||||
helpFlag = boolopt "help" rawopts
|
||||
tldrFlag = boolopt "tldr" rawopts
|
||||
infoFlag = boolopt "info" rawopts
|
||||
manFlag = boolopt "man" rawopts
|
||||
versionFlag = boolopt "version" rawopts
|
||||
|
||||
if
|
||||
-- 4.1. no command and a help/doc flag found - show general help/docs
|
||||
-- 6.1. no command and a help/doc flag found - show general help/docs
|
||||
| nocmdprovided && helpFlag -> pager $ showModeUsage (mainmode []) ++ "\n"
|
||||
| nocmdprovided && tldrFlag -> runTldrForPage "hledger"
|
||||
| nocmdprovided && infoFlag -> runInfoForTopic "hledger" Nothing
|
||||
| nocmdprovided && manFlag -> runManForTopic "hledger" Nothing
|
||||
|
||||
-- 4.2. --version flag found and none of these other conditions - show version
|
||||
-- 6.2. --version flag found and none of these other conditions - show version
|
||||
| versionFlag && not (isaddoncmd || helpFlag || tldrFlag || infoFlag || manFlag) -> putStrLn prognameandversion
|
||||
|
||||
-- 4.3. there's a command argument, but it's bad - show error
|
||||
-- 6.3. there's a command argument, but it's bad - show error
|
||||
| badcmdprovided -> error' $ "command "++clicmdarg++" is not recognized, run with no command to see a list"
|
||||
|
||||
-- 4.4. no command found, nothing else to do - show the commands list
|
||||
| nocmdprovided -> dbgIO "" "no command, showing commands list" >> printCommandsList prognameandversion addons
|
||||
-- 6.4. no command found, nothing else to do - show the commands list
|
||||
| nocmdprovided -> dbgIO1 "no command, showing commands list" () >> printCommandsList prognameandversion addons
|
||||
|
||||
-- 4.5. builtin command found
|
||||
| Just (cmdmode, cmdaction) <- mcmdmodeaction -> do
|
||||
-- 6.5. builtin command found
|
||||
| Just (cmdmode, cmdaction) <- mbuiltincmdaction -> do
|
||||
let mmodecmdname = headMay $ modeNames cmdmode
|
||||
dbgIO1 "running builtin command mode" $ fromMaybe "" mmodecmdname
|
||||
|
||||
-- validate opts/args more and convert to CliOpts
|
||||
opts <- rawOptsToCliOpts rawopts3 >>= \opts0 -> return opts0{progstarttime_=starttime}
|
||||
opts <- rawOptsToCliOpts rawopts >>= \opts0 -> return opts0{progstarttime_=starttime}
|
||||
dbgIO2 "processed opts" opts
|
||||
dbgIO "period from opts" (period_ . _rsReportOpts $ reportspec_ opts)
|
||||
dbgIO "interval from opts" (interval_ . _rsReportOpts $ reportspec_ opts)
|
||||
dbgIO "query from opts & args" (_rsQuery $ reportspec_ opts)
|
||||
let
|
||||
mcmdname = headMay $ modeNames cmdmode
|
||||
tldrpagename = maybe "hledger" (("hledger-"<>)) mcmdname
|
||||
let tldrpagename = maybe "hledger" (("hledger-"<>)) mmodecmdname
|
||||
|
||||
-- run the builtin command according to its type
|
||||
if
|
||||
-- help/doc flag - show command help/docs
|
||||
-- 6.5.1. help/doc flag - show command help/docs
|
||||
| helpFlag -> pager $ showModeUsage cmdmode ++ "\n"
|
||||
| tldrFlag -> runTldrForPage tldrpagename
|
||||
| infoFlag -> runInfoForTopic "hledger" mcmdname
|
||||
| manFlag -> runManForTopic "hledger" mcmdname
|
||||
| infoFlag -> runInfoForTopic "hledger" mmodecmdname
|
||||
| manFlag -> runManForTopic "hledger" mmodecmdname
|
||||
|
||||
-- builtin command which should not require or read the journal - run it
|
||||
| cmd `elem` ["demo","help","test"] ->
|
||||
cmdaction opts $ error' $ cmd++" tried to read the journal but is not supposed to"
|
||||
-- 6.5.2. builtin command which should not require or read the journal - run it
|
||||
| cmdname `elem` ["demo","help","test"] ->
|
||||
cmdaction opts $ error' $ cmdname++" tried to read the journal but is not supposed to"
|
||||
|
||||
-- builtin command which should create the journal if missing - do that and run it
|
||||
| cmd `elem` ["add","import"] -> do
|
||||
-- 6.5.3. builtin command which should create the journal if missing - do that and run it
|
||||
| cmdname `elem` ["add","import"] -> do
|
||||
ensureJournalFileExists . NE.head =<< journalFilePathFromOpts opts
|
||||
withJournalDo opts (cmdaction opts)
|
||||
|
||||
-- all other builtin commands - read the journal and if successful run the command with it
|
||||
-- 6.5.4. all other builtin commands - read the journal and if successful run the command with it
|
||||
| otherwise -> withJournalDo opts $ cmdaction opts
|
||||
|
||||
-- 4.6. external addon command found - run it,
|
||||
-- 6.6. external addon command found - run it,
|
||||
-- passing any cli arguments written after the command name
|
||||
-- and any command-specific opts from the config file.
|
||||
-- Any "--" arguments, which sometimes must be used in the command line
|
||||
@ -375,24 +399,24 @@ main = withGhcDebug' $ do
|
||||
-- are not passed since we can't be sure they're supported.
|
||||
| isaddoncmd -> do
|
||||
let
|
||||
addonargs0 = filter (/="--") $ supportedgenargsfromconf <> cmdargsfromconf <> cliargswithoutcmd
|
||||
addonargs0 = filter (/="--") $ supportedgenargsfromconf <> confcmdargs <> cliargswithoutcmd
|
||||
addonargs = dropCliSpecificOpts addonargs0
|
||||
shellcmd = printf "%s-%s %s" progname cmd (unwords' addonargs) :: String
|
||||
dbgIO "addon command selected" cmd
|
||||
shellcmd = printf "%s-%s %s" progname cmdname (unwords' addonargs) :: String
|
||||
dbgIO "addon command selected" cmdname
|
||||
dbgIO "addon command arguments after removing cli-specific opts" (map quoteIfNeeded addonargs)
|
||||
dbgIO1 "running addon" shellcmd
|
||||
system shellcmd >>= exitWith
|
||||
|
||||
-- deprecated command found
|
||||
-- cmd == "convert" = error' (modeHelp oldconvertmode) >> exitFailure
|
||||
-- cmdname == "convert" = error' (modeHelp oldconvertmode) >> exitFailure
|
||||
|
||||
-- 4.7. something else (shouldn't happen) - show an error
|
||||
-- 6.7. something else (shouldn't happen) - show an error
|
||||
| otherwise -> usageError $
|
||||
"could not understand the arguments "++show finalargs
|
||||
<> if null genargsfromconf then "" else "\ngeneral arguments added from config file: "++show genargsfromconf
|
||||
<> if null cmdargsfromconf then "" else "\ncommand arguments added from config file: "++show cmdargsfromconf
|
||||
<> if null confothergenargs then "" else "\ngeneral arguments added from config file: "++show confothergenargs
|
||||
<> if null confcmdargs then "" else "\ncommand arguments added from config file: "++show confcmdargs
|
||||
|
||||
-- 5. And we're done.
|
||||
-- 7. And we're done.
|
||||
-- Give ghc-debug a final chance to take control.
|
||||
when (ghcDebugMode == GDPauseAtEnd) $ ghcDebugPause'
|
||||
|
||||
@ -409,8 +433,8 @@ argsToCliOpts args addons = do
|
||||
let
|
||||
(_, _, args0) = moveFlagsAfterCommand args
|
||||
args1 = replaceNumericFlags args0
|
||||
rawopts3 = cmdargsParse "for options" (mainmode addons) args1
|
||||
rawOptsToCliOpts rawopts3
|
||||
rawopts = cmdargsParse "for options" (mainmode addons) args1
|
||||
rawOptsToCliOpts rawopts
|
||||
|
||||
-- | Parse the given command line arguments/options with the given cmdargs mode,
|
||||
-- after adding values to any valueless --debug flags,
|
||||
@ -422,6 +446,7 @@ cmdargsParse desc m args0 = process m (ensureDebugFlagHasVal args0)
|
||||
& either
|
||||
(\e -> error' $ e <> " while parsing these args " <> desc <> ": " <> unwords (map quoteIfNeeded args0))
|
||||
(traceOrLogAt verboseDebugLevel ("cmdargs: parsing " <> desc <> ": " <> show args0))
|
||||
-- XXX better error message when cmdargs fails (eg spaced/quoted/malformed flag values) ?
|
||||
|
||||
-- | cmdargs does not allow flags (options) to appear before the subcommand argument.
|
||||
-- We prefer to hide this restriction from the user, making the CLI more forgiving.
|
||||
|
||||
@ -512,17 +512,16 @@ You can save a set of command line options and arguments in a file,
|
||||
and then reuse them by writing `@FILENAME` as a command line argument.
|
||||
Eg: `hledger bal @foo.args`.
|
||||
|
||||
(Inside the argument file, each line should contain just one option or argument.
|
||||
An argument file's format is more restrictive than the command line.
|
||||
Each line should contain just one option or argument.
|
||||
Don't use spaces except inside quotes; write `=` or nothing between a flag and its argument.
|
||||
For the special characters mentioned above, use one less level of quoting than
|
||||
you would at the command prompt.)
|
||||
|
||||
Argument files are now superseded by..
|
||||
If you use quotes, they must enclose the whole line.
|
||||
For the special characters mentioned above, use one less level of quoting than you would at the command line.
|
||||
|
||||
## Config files
|
||||
|
||||
As of hledger 1.40, you can optionally save command line options (or arguments)
|
||||
to be used when running hledger commands, in a config file. Here's a small example:
|
||||
With hledger 1.40+, you can save extra command line options and arguments
|
||||
in a more featureful hledger config file. Here's a small example:
|
||||
|
||||
```conf
|
||||
# General options are listed first, one or more per line.
|
||||
@ -570,14 +569,7 @@ Eg (some operating systems need the `-S`, some don't):
|
||||
You can put not only options, but also arguments in a config file.
|
||||
This is probably more useful in special-purpose config files, not an automatic one.
|
||||
|
||||
There's an exception to this: a config file can't provide the command argument, currently
|
||||
([#2231](https://github.com/simonmichael/hledger/issues/2231)).
|
||||
If you need that, you can do it in the shebang line instead. Eg:
|
||||
```
|
||||
#!/usr/bin/env -S hledger balance --conf
|
||||
```
|
||||
|
||||
The config file feature has been added in hledger 1.40 and is considered *experimental*.
|
||||
The config file feature was added in hledger 1.40 and is considered *experimental*.
|
||||
|
||||
## Shell completions
|
||||
|
||||
@ -741,7 +733,7 @@ or use `--real` to exclude transactions that use them.
|
||||
#### Beancount costs
|
||||
|
||||
Beancount doesn't allow [redundant cost notation](https://hledger.org/hledger.html#combining-costs-and-equity-conversion-postings)
|
||||
as hledger does. If you have entries like this, you may need to comment out either the costs or the equity postings.
|
||||
as hledger does. If you have entries like this, you will need to comment out either the costs or the equity postings.
|
||||
|
||||
#### Beancount operating currency
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user