feat: config file: ignore unsupported general options; refactor

This commit is contained in:
Simon Michael 2024-06-23 01:01:34 +01:00
parent 3797fe13d3
commit 6b24c09a58
2 changed files with 108 additions and 51 deletions

View File

@ -53,7 +53,7 @@ In hledger, debug levels are used as follows:
Debug level: What to show: Debug level: What to show:
------------ --------------------------------------------------------- ------------ ---------------------------------------------------------
0 normal command output only (no warnings, eg) 0 normal command output only (no warnings, eg)
1 useful warnings, most common troubleshooting info, eg valuation 1 useful warnings, most common troubleshooting info (config file args, valuation..)
2 common troubleshooting info, more detail 2 common troubleshooting info, more detail
3 report options selection 3 report options selection
4 report generation 4 report generation

View File

@ -111,7 +111,6 @@ import Hledger.Cli.Version
import Data.Bifunctor (second) import Data.Bifunctor (second)
import Data.Function ((&)) import Data.Function ((&))
import Data.Functor ((<&>)) import Data.Functor ((<&>))
import Control.Monad.Extra (unless)
import Data.List.Extra (nubSort) import Data.List.Extra (nubSort)
import Data.Char (isDigit) import Data.Char (isDigit)
@ -174,8 +173,9 @@ main = withGhcDebug' $ do
-- let's go! -- let's go!
let let
dbgIO :: Show a => String -> a -> IO () -- this signature is needed dbgIO, dbgIO1 :: Show a => String -> a -> IO () -- this signature is needed
dbgIO = ptraceAtIO 8 dbgIO = ptraceAtIO 8
dbgIO1 = ptraceAtIO 1
dbgIO "running" prognameandversion dbgIO "running" prognameandversion
starttime <- getPOSIXTime starttime <- getPOSIXTime
@ -193,15 +193,19 @@ main = withGhcDebug' $ do
let let
cliargswithcmdfirst = cliargs & moveFlagsAfterCommand cliargswithcmdfirst = cliargs & moveFlagsAfterCommand
isNonEmptyNonFlag s = not $ null s || "-" `isPrefixOf` s isNonEmptyNonFlag s = not $ null s || "-" `isPrefixOf` s
clicmdarg = headDef "" $ takeWhile isNonEmptyNonFlag cliargswithcmdfirst (clicmdarg, cliargswithoutcmd) =
case span isNonEmptyNonFlag cliargswithcmdfirst of
(a:as,bs) -> (a,as++bs)
([],bs) -> ("",bs)
nocmdprovided = null clicmdarg nocmdprovided = null clicmdarg
(cliargsbeforecmd, cliargsaftercmd) = second (drop 1) $ break (==clicmdarg) cliargs (cliargsbeforecmd, cliargsaftercmd) = second (drop 1) $ break (==clicmdarg) cliargs
dbgIO "cli args" cliargs dbgIO "cli args" cliargs
dbgIO "cli args with command argument first, if any" cliargswithcmdfirst dbgIO "cli args with command argument first, if any" cliargswithcmdfirst
dbgIO "command argument found" clicmdarg dbgIO "command argument found" clicmdarg
dbgIO "cli args before command" cliargsbeforecmd dbgIO "cli args without command" cliargswithoutcmd
dbgIO "cli args after command" cliargsaftercmd dbgIO "cli args before command" cliargsbeforecmd
dbgIO "cli args after command" cliargsaftercmd
-- Search PATH for addon commands. Exclude any that match builtin command names. -- Search PATH for addon commands. Exclude any that match builtin command names.
addons <- hledgerAddons <&> filter (not . (`elem` builtinCommandNames) . dropExtension) addons <- hledgerAddons <&> filter (not . (`elem` builtinCommandNames) . dropExtension)
@ -212,32 +216,40 @@ main = withGhcDebug' $ do
-- If no command was provided, or if the command line contains a bad flag -- If no command was provided, or if the command line contains a bad flag
-- or a wrongly present/missing flag argument, cmd will be "". -- or a wrongly present/missing flag argument, cmd will be "".
let let
-- cliargswithcmdfirst' = filter (/= "--debug") cliargswithcmdfirst
-- XXX files --debug fails here, eg.
-- How to parse the command name with cmdargs without passing unsupported flags that it will reject ?
-- Is --debug the only flag like this ?
cmd = cmdargsParse cliargswithcmdfirst addons & stringopt "command" cmd = cmdargsParse cliargswithcmdfirst addons & stringopt "command"
-- XXX may need a better error message when cmdargs fails to parse (eg spaced/quoted/malformed flag values) -- XXX may need a better error message when cmdargs fails to parse (eg spaced/quoted/malformed flag values)
badcmdprovided = null cmd && not nocmdprovided badcmdprovided = null cmd && not nocmdprovided
isaddoncmd = not (null cmd) && cmd `elem` addons isaddoncmd = not (null cmd) && cmd `elem` addons
-- isbuiltincmd = cmd `elem` builtinCommandNames -- isbuiltincmd = cmd `elem` builtinCommandNames
mcmdmodeaction = findBuiltinCommand cmd
effectivemode = maybe (mainmode []) fst mcmdmodeaction
dbgIO "nocmdprovided" nocmdprovided dbgIO "nocmdprovided" nocmdprovided
dbgIO "badcmdprovided" badcmdprovided dbgIO "badcmdprovided" badcmdprovided
dbgIO "cmd found" cmd dbgIO1 "cmd found" cmd
dbgIO "isaddoncmd" isaddoncmd dbgIO "isaddoncmd" isaddoncmd
-- Read any extra general args/opts, and any command-specific ones, from a config file. -- Read any extra general args/opts, and any command-specific ones, from a config file.
-- (Ignoring any general args not supported by the current command.)
-- And insert them before the user's args, with adjustments, to get the final args. -- And insert them before the user's args, with adjustments, to get the final args.
conf <- getConf conf <- getConf
let let
genargsfromconf = confLookup "general" conf genargsfromconf = confLookup "general" conf
supportedgenargsfromconf = dropUnsupportedOpts effectivemode genargsfromconf
cmdargsfromconf = if null cmd then [] else confLookup cmd conf cmdargsfromconf = if null cmd then [] else confLookup cmd conf
argsfromcli = drop 1 cliargswithcmdfirst
finalargs = -- (avoid breaking vs code haskell highlighting..) finalargs = -- (avoid breaking vs code haskell highlighting..)
(if null clicmdarg then [] else [clicmdarg]) <> genargsfromconf <> cmdargsfromconf <> argsfromcli (if null clicmdarg then [] else [clicmdarg]) <> supportedgenargsfromconf <> cmdargsfromconf <> cliargswithoutcmd
& replaceNumericFlags -- convert any -NUM opts from the config file & replaceNumericFlags -- convert any -NUM opts from the config file
-- finalargs' <- expandArgsAt finalargs -- expand any @ARGFILEs from the config file ? don't bother -- finalargs' <- expandArgsAt finalargs -- expand @ARGFILEs in the config file ? don't bother
unless (null genargsfromconf) $ dbgIO ("extra general args from config file") genargsfromconf dbgIO1 "extra general args from config file" genargsfromconf
unless (null cmdargsfromconf) $ dbgIO ("extra command args from config file") cmdargsfromconf dbgIO1 "excluded general args from config file not supported by this command" $ genargsfromconf \\ supportedgenargsfromconf
dbgIO "final args" finalargs dbgIO1 "extra command args from config file" cmdargsfromconf
dbgIO "final args to be parsed by cmdargs" finalargs
-- Now parse these in full, first to RawOpts with cmdargs, then to hledger CliOpts. -- Now parse these in full, first to RawOpts with cmdargs, then to hledger CliOpts.
-- At this point a bad flag or flag argument will cause the program to exit with an error. -- At this point a bad flag or flag argument will cause the program to exit with an error.
@ -281,7 +293,7 @@ main = withGhcDebug' $ do
| nocmdprovided -> dbgIO "" "no command, showing commands list" >> printCommandsList prognameandversion addons | nocmdprovided -> dbgIO "" "no command, showing commands list" >> printCommandsList prognameandversion addons
-- builtin command found -- builtin command found
| Just (cmdmode, cmdaction) <- findBuiltinCommand cmd, | Just (cmdmode, cmdaction) <- mcmdmodeaction,
let mcmdname = headMay $ modeNames cmdmode, let mcmdname = headMay $ modeNames cmdmode,
let tldrpagename = maybe "hledger" (("hledger-"<>)) mcmdname -> let tldrpagename = maybe "hledger" (("hledger-"<>)) mcmdname ->
if if
@ -398,50 +410,95 @@ ensureDebugHasVal as = case break (=="--debug") as of
moveFlagsAfterCommand :: [String] -> [String] moveFlagsAfterCommand :: [String] -> [String]
moveFlagsAfterCommand args = insertFlagsAfterCommand $ moveFlagArgs (args, []) moveFlagsAfterCommand args = insertFlagsAfterCommand $ moveFlagArgs (args, [])
where where
allbuiltinflags = modeAndSubmodeFlags $ mainmode []
flagsToArgs flags = nubSort [ if length f == 1 then "-"++f else "--"++f | f <- nubSort $ concatMap flagNames flags]
novalflagargs = flagsToArgs $ filter ((==FlagNone).flagInfo) allbuiltinflags
reqvalflagargs = flagsToArgs $ filter ((==FlagReq).flagInfo) allbuiltinflags
optvalflagargs = flagsToArgs $ filter isOptValFlag allbuiltinflags
isOptValFlag f = case flagInfo f of
FlagOpt _ -> True
FlagOptRare _ -> True
_ -> False
isshort ('-':c:_) = c /= '-'
isshort _ = False
islong ('-':'-':_:_) = True
islong _ = False
isflag a = isshort a || islong a
shortreqvalflagargs = filter isshort reqvalflagargs
longreqvalflagargs_ = map (++"=") $ filter islong reqvalflagargs
longoptvalflagargs_ = map (++"=") $ filter islong optvalflagargs ++ ["--debug"]
-- Is this a short or long flag argument that should be moved,
-- and is its following argument a value that also should be moved ?
-- Returns:
-- 0 (not a flag)
-- 1 (single flag, maybe with joined argument; or multiple joined short flags)
-- 2 (flag with value in the next argument).
isMovableFlagArg :: String -> Int
isMovableFlagArg a
| a `elem` novalflagargs = 1 -- short or long no-val flag
| a `elem` reqvalflagargs = 2 -- short or long req-val flag, value is in next argument
| a `elem` optvalflagargs = 1 -- long (or short ?) opt-val flag, assume no value
| any (`isPrefixOf` a) shortreqvalflagargs = 1 -- short req-val flag, value is joined
| any (`isPrefixOf` a) longreqvalflagargs_ = 1 -- long req-val flag, value is joined with =
| any (`isPrefixOf` a) longoptvalflagargs_ = 1 -- long opt-val flag, value is joined with =
| isflag a = 1 -- an addon flag (or mistyped flag) we don't know, assume no value or value is joined
| otherwise = 0 -- not a flag
moveFlagArgs ((a:b:cs), moved) moveFlagArgs ((a:b:cs), moved)
| isMovableFlagArg a == 2 = moveFlagArgs (cs, moved++[a,b]) | isMovableFlagArg a == 2 = moveFlagArgs (cs, moved++[a,b])
| isMovableFlagArg a == 1 = moveFlagArgs (b:cs, moved++[a]) | isMovableFlagArg a == 1 = moveFlagArgs (b:cs, moved++[a])
| otherwise = (a:b:cs, moved) | otherwise = (a:b:cs, moved)
where
-- Is this a short or long flag argument that should be moved,
-- and is its following argument a value that also should be moved ?
-- Returns:
-- 0 (not a flag)
-- 1 (single flag, maybe with joined argument; or multiple joined short flags)
-- 2 (flag with value in the next argument).
isMovableFlagArg :: String -> Int
isMovableFlagArg a1
| a1 `elem` novalflagargs = 1 -- short or long no-val flag
| a1 `elem` reqvalflagargs = 2 -- short or long req-val flag, value is in next argument
| a1 `elem` optvalflagargs = 1 -- long (or short ?) opt-val flag, assume no value
| any (`isPrefixOf` a1) shortreqvalflagargs = 1 -- short req-val flag, value is joined
| any (`isPrefixOf` a1) longreqvalflagargs_ = 1 -- long req-val flag, value is joined with =
| any (`isPrefixOf` a1) longoptvalflagargs_ = 1 -- long opt-val flag, value is joined with =
| isFlagArg a1 = 1 -- an addon flag (or mistyped flag) we don't know, assume no value or value is joined
| otherwise = 0 -- not a flag
moveFlagArgs (as, moved) = (as, moved) moveFlagArgs (as, moved) = (as, moved)
insertFlagsAfterCommand ([], flags) = flags insertFlagsAfterCommand ([], flags) = flags
insertFlagsAfterCommand (command1:args2, flags) = [command1] ++ flags ++ args2 insertFlagsAfterCommand (command1:args2, flags) = [command1] ++ flags ++ args2
-- All Flags provided by hledger and its builtin comands.
allbuiltinflags = modeAndSubmodeFlags $ mainmode []
-- Flag arguments are command line arguments beginning with - or --
-- (followed by a short of long flag name, and possibly joined short flags or a joined value).
isShortFlagArg ('-':c:_) = c /= '-'
isShortFlagArg _ = False
isLongFlagArg ('-':'-':_:_) = True
isLongFlagArg _ = False
isFlagArg a = isShortFlagArg a || isLongFlagArg a
-- Given a list of Flags, return all of their supported short and long flag names as flag arguments
-- (a sorted list of the unique flag names with - or -- prefixes).
flagsToArgs flags = nubSort [ if length f == 1 then "-"++f else "--"++f | f <- nubSort $ concatMap flagNames flags]
-- hledger flag args grouped by whether their flag expects no value, a required value, or an optional value.
novalflagargs = flagsToArgs $ filter ((==FlagNone).flagInfo) allbuiltinflags
reqvalflagargs = flagsToArgs $ filter ((==FlagReq).flagInfo) allbuiltinflags
optvalflagargs = flagsToArgs $ filter isOptValFlag allbuiltinflags
where
isOptValFlag f = case flagInfo f of
FlagOpt _ -> True
FlagOptRare _ -> True
_ -> False
-- Short flag args that expect a required value.
shortreqvalflagargs = filter isShortFlagArg reqvalflagargs
-- Long flag args that expect a required value or optional value respectively, with = appended.
longreqvalflagargs_ = map (++"=") $ filter isLongFlagArg reqvalflagargs
longoptvalflagargs_ = map (++"=") $ filter isLongFlagArg optvalflagargs ++ ["--debug"]
-- Is this flag arg one that requires a value ?
isReqValFlagArg a = a `elem` reqvalflagargs
-- | Given a hledger cmdargs mode and a list of command line arguments, try to drop any of the
-- arguments which seem to be flags not supported by this mode. Also drop their values if any.
dropUnsupportedOpts :: Mode RawOpts -> [String] -> [String]
dropUnsupportedOpts m = \case
[] -> []
a:as -> if
| isLongFlagArg a,
let f = takeWhile (/='=') a,
let as' = if isReqValFlagArg f && '=' `notElem` a then drop 1 as else as
->
if m `supportsFlag` f
then a : go as
else go as'
| isShortFlagArg a,
let f = take 2 a,
let as' = if isReqValFlagArg f && length a == 2 then drop 1 as else as
->
if m `supportsFlag` f
then a : go as
else go as'
| otherwise -> a : dropUnsupportedOpts m as
where
go = dropUnsupportedOpts m
supportsFlag m1 flagarg = elem flagarg $ flagsToArgs $ modeAndSubmodeFlags m1
-- | Get all the flags defined in a mode or its immediate subcommands, -- | Get all the flags defined in a mode or its immediate subcommands,
-- whether in named, unnamed or hidden groups (does not recurse into subsubcommands). -- whether in named, unnamed or hidden groups (does not recurse into subsubcommands).
modeAndSubmodeFlags :: Mode a -> [Flag a] modeAndSubmodeFlags :: Mode a -> [Flag a]