imp: config file: --conf, --no-conf/-n, improve debug output

This commit is contained in:
Simon Michael 2024-06-25 06:36:11 +01:00
parent 6180a162b2
commit 5739bff249
4 changed files with 78 additions and 36 deletions

View File

@ -1,8 +1,10 @@
#!/usr/bin/env -S hledger --conf
# hledger.conf - extra options(/arguments) to be added to hledger commands. # hledger.conf - extra options(/arguments) to be added to hledger commands.
# hledger looks for a hledger.conf file in the current directory or above, # hledger looks for a hledger.conf file in the current directory or above,
# or in your home directory with a dotted name: $HOME/.hledger.conf, # or in your home directory with a dotted name: $HOME/.hledger.conf,
# or in your XDG config directory: $HOME/.config/hledger/hledger.conf. # or in your XDG config directory: $HOME/.config/hledger/hledger.conf.
# You can also execute a conf file with a shebang line like the one above.
# 1. This first, unnamed section is typically used for general options. # 1. This first, unnamed section is typically used for general options.
# These affect all commands, or when not supported will be ignored. # These affect all commands, or when not supported will be ignored.

View File

@ -88,7 +88,7 @@ module Hledger.Cli (
) )
where where
import Control.Monad (when) import Control.Monad (when, unless)
import Data.List import Data.List
import qualified Data.List.NonEmpty as NE import qualified Data.List.NonEmpty as NE
import Data.Time.Clock.POSIX (getPOSIXTime) import Data.Time.Clock.POSIX (getPOSIXTime)
@ -112,6 +112,7 @@ import Data.Bifunctor (second)
import Data.Function ((&)) import Data.Function ((&))
import Data.Functor ((<&>)) import Data.Functor ((<&>))
import Data.List.Extra (nubSort) import Data.List.Extra (nubSort)
import Data.Maybe (isJust)
-- | The overall cmdargs mode describing hledger's command-line options and subcommands. -- | The overall cmdargs mode describing hledger's command-line options and subcommands.
@ -134,7 +135,10 @@ mainmode addons = defMode {
-- flags in named groups: (keep synced with Hledger.Cli.CliOptions.highlightHelp) -- flags in named groups: (keep synced with Hledger.Cli.CliOptions.highlightHelp)
groupNamed = cligeneralflagsgroups1 groupNamed = cligeneralflagsgroups1
-- flags in the unnamed group, shown last: -- flags in the unnamed group, shown last:
,groupUnnamed = [] ,groupUnnamed = [
flagReq ["conf"] (\s opts -> Right $ setopt "conf" s opts) "CONFFILE" "Use extra options defined in this config file. If not specified, searches upward and in XDG config dir for hledger.conf (or .hledger.conf in $HOME)."
,flagNone ["no-conf","n"] (setboolopt "no-conf") "ignore any config file"
]
-- flags handled but not shown in the help: -- flags handled but not shown in the help:
,groupHidden = ,groupHidden =
[detailedversionflag] [detailedversionflag]
@ -216,12 +220,14 @@ main = withGhcDebug' $ do
-- For this we do a preliminary cmdargs parse of the command line arguments. -- For this we do a preliminary cmdargs parse of the command line arguments.
-- 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 "".
-- (Also find any --conf-file/--no-conf options.)
let let
-- cliargswithcmdfirst' = filter (/= "--debug") cliargswithcmdfirst -- cliargswithcmdfirst' = filter (/= "--debug") cliargswithcmdfirst
-- XXX files --debug fails here, eg. -- XXX files --debug fails here, eg.
-- How to parse the command name with cmdargs without passing unsupported flags that it will reject ? -- How to parse the command name with cmdargs without passing unsupported flags that it will reject ?
-- Is --debug the only flag like this ? -- Is --debug the only flag like this ?
cmd = cmdargsParse cliargswithcmdfirst addons & stringopt "command" rawopts0 = cmdargsParse cliargswithcmdfirst addons
cmd = stringopt "command" rawopts0
-- 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
@ -238,7 +244,7 @@ main = withGhcDebug' $ do
-- Read any extra general and command-specific args/opts from a config file. -- Read any extra general and command-specific args/opts from a config file.
-- Ignore any general opts not known to be supported by the command. -- Ignore any general opts not known to be supported by the command.
conf <- getConf (conf, mconffile) <- getConf rawopts0
let let
genargsfromconf = confLookup "general" conf genargsfromconf = confLookup "general" conf
supportedgenargsfromconf supportedgenargsfromconf
@ -248,9 +254,11 @@ main = withGhcDebug' $ do
cmdargsfromconf cmdargsfromconf
| null cmd = [] | null cmd = []
| otherwise = confLookup cmd conf & if isaddoncmd then ("--":) else id | otherwise = confLookup cmd conf & if isaddoncmd then ("--":) else id
dbgIO1 "extra general args from config file" genargsfromconf when (isJust mconffile) $ do
dbgIO1 "excluded general args from config file, not supported by this command" excludedgenargsfromconf dbgIO1 "extra general args from config file" genargsfromconf
dbgIO1 "extra command args from config file" cmdargsfromconf unless (null excludedgenargsfromconf) $
dbgIO1 "excluded general args from config file, not supported by this command" excludedgenargsfromconf
dbgIO1 "extra command args from config file" cmdargsfromconf
--------------------------------------------------------------- ---------------------------------------------------------------
-- Combine cli and config file args and parse with cmdargs. -- Combine cli and config file args and parse with cmdargs.

View File

@ -146,8 +146,8 @@ prognameandversion =
-- | Common input-related flags: --file, --rules-file, --alias... -- | Common input-related flags: --file, --rules-file, --alias...
inputflags :: [Flag RawOpts] inputflags :: [Flag RawOpts]
inputflags = [ inputflags = [
flagReq ["file","f"] (\s opts -> Right $ setopt "file" s opts) "FILE" "Read data from FILE, or from stdin if -. Can be specified more than once. If not specified, reads from $LEDGER_FILE or $HOME/.hledger.journal." flagReq ["file","f"] (\s opts -> Right $ setopt "file" s opts) "[FMT:]FILE" "Read data from FILE, or from stdin if FILE is -, inferring format from extension or a FMT: prefix. Can be specified more than once. If not specified, reads from $LEDGER_FILE or $HOME/.hledger.journal."
,flagReq ["rules-file"] (\s opts -> Right $ setopt "rules-file" s opts) "RULEFILE" "Use conversion rules from this file for converting subsequent CSV/SSV/TSV files. If not specified, uses FILE.rules for each such FILE." ,flagReq ["rules"] (\s opts -> Right $ setopt "rules" s opts) "RULESFILE" "Use rules defined in this rules file for converting subsequent CSV/SSV/TSV files. If not specified, uses FILE.csv.rules for each FILE.csv." -- see also hiddenflags
,flagReq ["alias"] (\s opts -> Right $ setopt "alias" s opts) "A=B|/RGX/=RPL" "transform account names from A to B, or by replacing regular expression matches" ,flagReq ["alias"] (\s opts -> Right $ setopt "alias" s opts) "A=B|/RGX/=RPL" "transform account names from A to B, or by replacing regular expression matches"
,flagNone ["auto"] (setboolopt "auto") "generate extra postings by applying auto posting rules (\"=\") to all transactions" ,flagNone ["auto"] (setboolopt "auto") "generate extra postings by applying auto posting rules (\"=\") to all transactions"

View File

@ -5,6 +5,7 @@ Read extra CLI arguments from a hledger config file.
{-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE TupleSections #-} {-# LANGUAGE TupleSections #-}
{-# LANGUAGE MultiWayIf #-}
module Hledger.Cli.Conf ( module Hledger.Cli.Conf (
getConf getConf
@ -25,7 +26,7 @@ import System.FilePath ((</>), takeDirectory)
import Text.Megaparsec import Text.Megaparsec
import Text.Megaparsec.Char import Text.Megaparsec.Char
import Hledger (error', strip, words') import Hledger (error', strip, words', RawOpts, boolopt, maybestringopt, expandPath)
import Hledger.Read.Common import Hledger.Read.Common
import Hledger.Utils.Parse import Hledger.Utils.Parse
import Hledger.Utils.Debug import Hledger.Utils.Debug
@ -70,55 +71,86 @@ confLookup cmd Conf{confSections} =
M.lookup cmd $ M.lookup cmd $
M.fromList [(csName,csArgs) | ConfSection{csName,csArgs} <- confSections] M.fromList [(csName,csArgs) | ConfSection{csName,csArgs} <- confSections]
-- | Try to read a hledger config file from several places. -- | Try to read a hledger config file from several places,
-- If no config file is found, this returns a null Conf. -- or from a file specified by --conf,
-- returning its path and a Conf parsed from it.
-- If no config file is found, or --no-conf is used, this returns a null Conf.
-- Any other IO or parse failure will raise an error. -- Any other IO or parse failure will raise an error.
getConf :: IO Conf getConf :: RawOpts -> IO (Conf, Maybe FilePath)
getConf = do getConf rawopts = do
mconftxt <- confFilePaths >>= readFirstConfFile defconfpaths <- confFilePaths
case mconftxt of let noconf = boolopt "no-conf" rawopts
Nothing -> return $ traceAt 1 "no config file found" nullconf let mconffile0 = maybestringopt "conf" rawopts
Just (f,s) -> mconffile <- case mconffile0 of
Just f -> Just <$> (getCurrentDirectory >>= flip expandPath f)
Nothing -> return Nothing
let confpaths = maybe defconfpaths (:[]) mconffile
mconftxt <- readFirstConfFile confpaths
if
| noconf -> return $ traceAt 1 "ignoring config files" (nullconf, Nothing)
| Nothing <- mconftxt -> return $ traceAt 1 "no config file found" (nullconf, Nothing)
| Just (f,s) <- mconftxt ->
case parseConf f (T.pack s) of case parseConf f (T.pack s) of
Left err -> error' $ errorBundlePretty err -- customErrorBundlePretty err Left err -> error' $ errorBundlePretty err -- customErrorBundlePretty err
Right ss -> return nullconf{ Right ss -> return (nullconf{
confFile = f confFile = f
,confText = s ,confText = s
,confFormat = 1 ,confFormat = 1
,confSections = ss ,confSections = ss
} }, Just f)
-- | Get the possible paths for a hledger config file, depending on the current directory. -- | Get the possible paths for a hledger config file, depending on the current directory.
confFilePaths :: IO [FilePath] confFilePaths :: IO [FilePath]
confFilePaths = do confFilePaths = do
ds <- confDirs ds <- confDirs
home <- getHomeDirectory home <- getHomeDirectory
return $ dbg1 "possible config files" $ return $ dbg8 "possible config files" $
flip map ds $ \d -> d </> if d==home then ".hledger.conf" else "hledger.conf" flip map ds $ \d -> d </> if d==home then ".hledger.conf" else "hledger.conf"
-- | Get the directories to check for a hledger config file. -- | Get the directories to check for a hledger config file.
confDirs :: IO [FilePath] confDirs :: IO [FilePath]
confDirs = do confDirs = do
dirs <- getDirsUpToHomeOrRoot xdgc <- getXdgDirectory XdgConfig "hledger"
xdgdir <- getXdgDirectory XdgConfig "hledger"
return $ dbg1 "conf dirs" $ dirs <> [xdgdir]
-- | Get this directory and all of its parents up to ~ or /.
getDirsUpToHomeOrRoot :: IO [FilePath]
getDirsUpToHomeOrRoot = do
home <- getHomeDirectory home <- getHomeDirectory
let here <- getCurrentDirectory
go d = -- lowdirs <- getDirsUpToHomeOrRoot
if d=="/" || d==home -- highdirs <-
then [d] -- case lastMay lowdirs of
else d : go (takeDirectory d) -- Just d | d==home -> getDirsUpToRoot $ takeDirectory d
dbg1 "dirs up to home or root" . -- _ -> return []
go <$> getCurrentDirectory -- let dirs = lowdirs <> [xdgdir] <> highdirs
dirs <- getDirsUpToRoot here
let dirs2 = if home `elem` dirs then dirs else dirs <> [home]
let dirs3 = if xdgc `elem` dirs2 then dirs2 else dirs2 <> [xdgc]
return $ dbg1 "searching config dirs" dirs3
-- -- | Get this directory and all of its parents up to ~ or /.
-- getDirsUpToHomeOrRoot :: IO [FilePath]
-- getDirsUpToHomeOrRoot = do
-- home <- getHomeDirectory
-- let
-- go d =
-- if d=="/" || d==home
-- then [d]
-- else d : go (takeDirectory d)
-- dbg1 "dirs up to home or root" .
-- go <$> getCurrentDirectory
-- | Get this directory and all of its parents up to /.
getDirsUpToRoot :: FilePath -> IO [FilePath]
getDirsUpToRoot dir = return $ go [] dir
where
go seen d = if
| d `elem` seen || length seen >= 100 -> [] -- just in case
| d=="/" -> [d]
| otherwise -> d : go (d:seen) (takeDirectory d)
-- dbg1 "dirs up to root" .
-- go <$> getCurrentDirectory
-- | Read the first of these files that exists. -- | Read the first of these files that exists.
readFirstConfFile :: [FilePath] -> IO (Maybe (FilePath, String)) readFirstConfFile :: [FilePath] -> IO (Maybe (FilePath, String))
readFirstConfFile fs = do readFirstConfFile fs = do
let dd = dbg1With (("config file found: "<>).fst) let dd = dbg1With (("using config file: "<>).fst)
mapM (fmap (fmap dd).readConfFile) fs <&> headMay . catMaybes mapM (fmap (fmap dd).readConfFile) fs <&> headMay . catMaybes
-- | Read this file and return its path and contents, if it exists. -- | Read this file and return its path and contents, if it exists.