    {-|
    Read extra CLI arguments from a hledger config file.
    -}

    {-# LANGUAGE ScopedTypeVariables #-}
    {-# LANGUAGE NamedFieldPuns #-}
    {-# LANGUAGE MultiWayIf #-}

    module Hledger.Cli.Conf (
       Conf
      ,SectionName
      ,getConf
      ,getConf'
      ,nullconf
      ,confLookup
      ,activeConfFile
      ,activeLocalConfFile
      ,activeUserConfFile
      ,confFiles
      ,userConfFiles
      ,parseConf
    )
    where

    import Control.Exception (handle)
    import Control.Monad (void, forM)
    import Control.Monad.Identity (Identity)
    import Data.Functor ((<&>))
    import Data.Map qualified as M
    import Data.Maybe (catMaybes)
    import Data.Text (Text)
    import Data.Text qualified as T (pack)
    import Safe (headMay, lastDef)
    import System.Directory (getHomeDirectory, getXdgDirectory, XdgDirectory (XdgConfig), doesFileExist, getCurrentDirectory)
    import System.FilePath ((</>), takeDirectory)
    import Text.Megaparsec as M
    import Text.Megaparsec.Char

    import Hledger (error', strip, words', RawOpts, expandPath)
    import Hledger.Read.Common
    import Hledger.Utils.Parse
    import Hledger.Utils.Debug
    import Hledger.Data.RawOptions (collectopts)

    -- | A hledger config file.
    data Conf = Conf {
       Conf -> FilePath
    confFile :: FilePath
      -- ,confText :: String
      ,Conf -> Int
    confFormat :: Int
      ,Conf -> [ConfSection]
    confSections :: [ConfSection]
    } deriving (Conf -> Conf -> Bool
    (Conf -> Conf -> Bool) -> (Conf -> Conf -> Bool) -> Eq Conf
    forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
    $c== :: Conf -> Conf -> Bool
    == :: Conf -> Conf -> Bool
    $c/= :: Conf -> Conf -> Bool
    /= :: Conf -> Conf -> Bool
    Eq,Int -> Conf -> ShowS
    [Conf] -> ShowS
    Conf -> FilePath
    (Int -> Conf -> ShowS)
    -> (Conf -> FilePath) -> ([Conf] -> ShowS) -> Show Conf
    forall a.
    (Int -> a -> ShowS) -> (a -> FilePath) -> ([a] -> ShowS) -> Show a
    $cshowsPrec :: Int -> Conf -> ShowS
    showsPrec :: Int -> Conf -> ShowS
    $cshow :: Conf -> FilePath
    show :: Conf -> FilePath
    $cshowList :: [Conf] -> ShowS
    showList :: [Conf] -> ShowS
    Show)

    -- | One section in a hledger config file.
    data ConfSection = ConfSection {
       ConfSection -> FilePath
    csName :: SectionName
      ,ConfSection -> [FilePath]
    csArgs :: [Arg]
    } deriving (ConfSection -> ConfSection -> Bool
    (ConfSection -> ConfSection -> Bool)
    -> (ConfSection -> ConfSection -> Bool) -> Eq ConfSection
    forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
    $c== :: ConfSection -> ConfSection -> Bool
    == :: ConfSection -> ConfSection -> Bool
    $c/= :: ConfSection -> ConfSection -> Bool
    /= :: ConfSection -> ConfSection -> Bool
    Eq,Int -> ConfSection -> ShowS
    [ConfSection] -> ShowS
    ConfSection -> FilePath
    (Int -> ConfSection -> ShowS)
    -> (ConfSection -> FilePath)
    -> ([ConfSection] -> ShowS)
    -> Show ConfSection
    forall a.
    (Int -> a -> ShowS) -> (a -> FilePath) -> ([a] -> ShowS) -> Show a
    $cshowsPrec :: Int -> ConfSection -> ShowS
    showsPrec :: Int -> ConfSection -> ShowS
    $cshow :: ConfSection -> FilePath
    show :: ConfSection -> FilePath
    $cshowList :: [ConfSection] -> ShowS
    showList :: [ConfSection] -> ShowS
    Show)

    -- | The name of a config file section, with surrounding brackets and whitespace removed.
    type SectionName = String

    -- | A command line argument to be passed to CmdArgs.process.
    -- It seems this should be a single command line argument (or flag or flag value).
    -- If it contains spaces, those are treated as part of a single argument, as with CMD a "b c".
    type Arg = String

    nullconf :: Conf
    nullconf = Conf {
       confFile :: FilePath
    confFile = FilePath
    ""
      ,confFormat :: Int
    confFormat = Int
    1
      ,confSections :: [ConfSection]
    confSections = []
    }

    -- | The --conf or --no-conf or default config file specified by command line options.
    data ConfFileSpec =
        SomeConfFile FilePath  -- ^ use config file specified with --conf
      | NoConfFile             -- ^ don't use any config file (--no-conf)
      | AutoConfFile           -- ^ use the config file found by directory search (default)
      deriving (ConfFileSpec -> ConfFileSpec -> Bool
    (ConfFileSpec -> ConfFileSpec -> Bool)
    -> (ConfFileSpec -> ConfFileSpec -> Bool) -> Eq ConfFileSpec
    forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
    $c== :: ConfFileSpec -> ConfFileSpec -> Bool
    == :: ConfFileSpec -> ConfFileSpec -> Bool
    $c/= :: ConfFileSpec -> ConfFileSpec -> Bool
    /= :: ConfFileSpec -> ConfFileSpec -> Bool
    Eq,Int -> ConfFileSpec -> ShowS
    [ConfFileSpec] -> ShowS
    ConfFileSpec -> FilePath
    (Int -> ConfFileSpec -> ShowS)
    -> (ConfFileSpec -> FilePath)
    -> ([ConfFileSpec] -> ShowS)
    -> Show ConfFileSpec
    forall a.
    (Int -> a -> ShowS) -> (a -> FilePath) -> ([a] -> ShowS) -> Show a
    $cshowsPrec :: Int -> ConfFileSpec -> ShowS
    showsPrec :: Int -> ConfFileSpec -> ShowS
    $cshow :: ConfFileSpec -> FilePath
    show :: ConfFileSpec -> FilePath
    $cshowList :: [ConfFileSpec] -> ShowS
    showList :: [ConfFileSpec] -> ShowS
    Show)

    -- Get the conf file specification from options,
    -- considering the rightmost --conf or --no-conf option if any.
    confFileSpecFromRawOpts :: RawOpts -> ConfFileSpec
    confFileSpecFromRawOpts :: RawOpts -> ConfFileSpec
    confFileSpecFromRawOpts = ConfFileSpec -> [ConfFileSpec] -> ConfFileSpec
    forall a. a -> [a] -> a
    lastDef ConfFileSpec
    AutoConfFile ([ConfFileSpec] -> ConfFileSpec)
    -> (RawOpts -> [ConfFileSpec]) -> RawOpts -> ConfFileSpec
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    . ((FilePath, FilePath) -> Maybe ConfFileSpec)
    -> RawOpts -> [ConfFileSpec]
    forall a. ((FilePath, FilePath) -> Maybe a) -> RawOpts -> [a]
    collectopts (FilePath, FilePath) -> Maybe ConfFileSpec
    cfsFromRawOpt
      where
        cfsFromRawOpt :: (FilePath, FilePath) -> Maybe ConfFileSpec
    cfsFromRawOpt (FilePath
    "conf",FilePath
    f)    = ConfFileSpec -> Maybe ConfFileSpec
    forall a. a -> Maybe a
    Just (ConfFileSpec -> Maybe ConfFileSpec)
    -> ConfFileSpec -> Maybe ConfFileSpec
    forall a b. (a -> b) -> a -> b
    $ FilePath -> ConfFileSpec
    SomeConfFile FilePath
    f
        cfsFromRawOpt (FilePath
    "no-conf",FilePath
    _) = ConfFileSpec -> Maybe ConfFileSpec
    forall a. a -> Maybe a
    Just (ConfFileSpec -> Maybe ConfFileSpec)
    -> ConfFileSpec -> Maybe ConfFileSpec
    forall a b. (a -> b) -> a -> b
    $ ConfFileSpec
    NoConfFile
        cfsFromRawOpt (FilePath, FilePath)
    _             = Maybe ConfFileSpec
    forall a. Maybe a
    Nothing

    -- config reading

    -- | Fetch all the arguments/options defined in a section with this name, if it exists.
    -- This should be "general" for the unnamed first section, or a hledger command name.
    confLookup :: SectionName -> Conf -> [Arg]
    confLookup :: FilePath -> Conf -> [FilePath]
    confLookup FilePath
    cmd Conf{[ConfSection]
    confSections :: Conf -> [ConfSection]
    confSections :: [ConfSection]
    confSections} =
      [FilePath]
    -> ([FilePath] -> [FilePath]) -> Maybe [FilePath] -> [FilePath]
    forall b a. b -> (a -> b) -> Maybe a -> b
    maybe [] ((FilePath -> [FilePath]) -> [FilePath] -> [FilePath]
    forall (t :: * -> *) a b. Foldable t => (a -> [b]) -> t a -> [b]
    concatMap FilePath -> [FilePath]
    words') (Maybe [FilePath] -> [FilePath]) -> Maybe [FilePath] -> [FilePath]
    forall a b. (a -> b) -> a -> b
    $  -- XXX PARTIAL
      FilePath -> Map FilePath [FilePath] -> Maybe [FilePath]
    forall k a. Ord k => k -> Map k a -> Maybe a
    M.lookup FilePath
    cmd (Map FilePath [FilePath] -> Maybe [FilePath])
    -> Map FilePath [FilePath] -> Maybe [FilePath]
    forall a b. (a -> b) -> a -> b
    $
      [(FilePath, [FilePath])] -> Map FilePath [FilePath]
    forall k a. Ord k => [(k, a)] -> Map k a
    M.fromList [(FilePath
    csName,[FilePath]
    csArgs) | ConfSection{FilePath
    csName :: ConfSection -> FilePath
    csName :: FilePath
    csName,[FilePath]
    csArgs :: ConfSection -> [FilePath]
    csArgs :: [FilePath]
    csArgs} <- [ConfSection]
    confSections]

    -- | Try to read a hledger config from a config file specified by --conf,
    -- or the first config file found in any of several default file paths.
    -- If --no-conf was used, or if no file was specified or found, this returns a null Conf.
    -- If a specified file, or the first file found, can not be read or parsed, this returns an error message.
    -- Otherwise this returns the parsed Conf, and the file path.
    getConf :: RawOpts -> IO (Either String (Conf, Maybe FilePath))
    getConf :: RawOpts -> IO (Either FilePath (Conf, Maybe FilePath))
    getConf RawOpts
    rawopts = do
      -- As in Cli.hs, conf debug output always goes to stderr;
      -- that's ok as conf is a hledger cli feature for now.
      case RawOpts -> ConfFileSpec
    confFileSpecFromRawOpts RawOpts
    rawopts of
        ConfFileSpec
    NoConfFile     -> Either FilePath (Conf, Maybe FilePath)
    -> IO (Either FilePath (Conf, Maybe FilePath))
    forall a. a -> IO a
    forall (m :: * -> *) a. Monad m => a -> m a
    return (Either FilePath (Conf, Maybe FilePath)
     -> IO (Either FilePath (Conf, Maybe FilePath)))
    -> Either FilePath (Conf, Maybe FilePath)
    -> IO (Either FilePath (Conf, Maybe FilePath))
    forall a b. (a -> b) -> a -> b
    $ (Conf, Maybe FilePath) -> Either FilePath (Conf, Maybe FilePath)
    forall a b. b -> Either a b
    Right ((Conf, Maybe FilePath) -> Either FilePath (Conf, Maybe FilePath))
    -> (Conf, Maybe FilePath) -> Either FilePath (Conf, Maybe FilePath)
    forall a b. (a -> b) -> a -> b
    $ FilePath -> (Conf, Maybe FilePath) -> (Conf, Maybe FilePath)
    forall a. FilePath -> a -> a
    dbg1Msg FilePath
    "ignoring config files" (Conf
    nullconf, Maybe FilePath
    forall a. Maybe a
    Nothing)
        SomeConfFile FilePath
    f -> IO FilePath
    getCurrentDirectory IO FilePath -> (FilePath -> IO FilePath) -> IO FilePath
    forall a b. IO a -> (a -> IO b) -> IO b
    forall (m :: * -> *) a b. Monad m => m a -> (a -> m b) -> m b
    >>= (FilePath -> FilePath -> IO FilePath)
    -> FilePath -> FilePath -> IO FilePath
    forall a b c. (a -> b -> c) -> b -> a -> c
    flip FilePath -> FilePath -> IO FilePath
    expandPath FilePath
    f IO FilePath
    -> (FilePath -> IO (Either FilePath (Conf, Maybe FilePath)))
    -> IO (Either FilePath (Conf, Maybe FilePath))
    forall a b. IO a -> (a -> IO b) -> IO b
    forall (m :: * -> *) a b. Monad m => m a -> (a -> m b) -> m b
    >>= FilePath -> IO (Either FilePath (Conf, Maybe FilePath))
    readConfFile (FilePath -> IO (Either FilePath (Conf, Maybe FilePath)))
    -> ShowS -> FilePath -> IO (Either FilePath (Conf, Maybe FilePath))
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    . FilePath -> ShowS
    forall a. Show a => FilePath -> a -> a
    dbg1 FilePath
    "using specified config file"
        ConfFileSpec
    AutoConfFile   -> do
          fs <- IO [FilePath]
    confFiles
          case fs of
            FilePath
    f:[FilePath]
    _ -> FilePath -> [FilePath] -> IO ()
    forall (m :: * -> *) a.
    (MonadIO m, Show a) =>
    FilePath -> a -> m ()
    dbg8IO FilePath
    "found config files" [FilePath]
    fs IO () -> IO () -> IO ()
    forall a b. IO a -> IO b -> IO b
    forall (m :: * -> *) a b. Monad m => m a -> m b -> m b
    >> FilePath -> FilePath -> IO ()
    forall (m :: * -> *) a.
    (MonadIO m, Show a) =>
    FilePath -> a -> m ()
    dbg1IO FilePath
    "using nearest config file" FilePath
    f IO ()
    -> IO (Either FilePath (Conf, Maybe FilePath))
    -> IO (Either FilePath (Conf, Maybe FilePath))
    forall a b. IO a -> IO b -> IO b
    forall (m :: * -> *) a b. Monad m => m a -> m b -> m b
    >> FilePath -> IO (Either FilePath (Conf, Maybe FilePath))
    readConfFile FilePath
    f
            []  -> Either FilePath (Conf, Maybe FilePath)
    -> IO (Either FilePath (Conf, Maybe FilePath))
    forall a. a -> IO a
    forall (m :: * -> *) a. Monad m => a -> m a
    return (Either FilePath (Conf, Maybe FilePath)
     -> IO (Either FilePath (Conf, Maybe FilePath)))
    -> Either FilePath (Conf, Maybe FilePath)
    -> IO (Either FilePath (Conf, Maybe FilePath))
    forall a b. (a -> b) -> a -> b
    $ (Conf, Maybe FilePath) -> Either FilePath (Conf, Maybe FilePath)
    forall a b. b -> Either a b
    Right ((Conf, Maybe FilePath) -> Either FilePath (Conf, Maybe FilePath))
    -> (Conf, Maybe FilePath) -> Either FilePath (Conf, Maybe FilePath)
    forall a b. (a -> b) -> a -> b
    $ FilePath -> (Conf, Maybe FilePath) -> (Conf, Maybe FilePath)
    forall a. FilePath -> a -> a
    dbg1Msg FilePath
    "no config file found" (Conf
    nullconf, Maybe FilePath
    forall a. Maybe a
    Nothing)

    -- | Like getConf but throws an error on failure.
    getConf' :: RawOpts -> IO (Conf, Maybe FilePath)
    getConf' :: RawOpts -> IO (Conf, Maybe FilePath)
    getConf' RawOpts
    rawopts = RawOpts -> IO (Either FilePath (Conf, Maybe FilePath))
    getConf RawOpts
    rawopts IO (Either FilePath (Conf, Maybe FilePath))
    -> (Either FilePath (Conf, Maybe FilePath)
        -> IO (Conf, Maybe FilePath))
    -> IO (Conf, Maybe FilePath)
    forall a b. IO a -> (a -> IO b) -> IO b
    forall (m :: * -> *) a b. Monad m => m a -> (a -> m b) -> m b
    >>= (FilePath -> IO (Conf, Maybe FilePath))
    -> ((Conf, Maybe FilePath) -> IO (Conf, Maybe FilePath))
    -> Either FilePath (Conf, Maybe FilePath)
    -> IO (Conf, Maybe FilePath)
    forall a c b. (a -> c) -> (b -> c) -> Either a b -> c
    either (FilePath -> IO (Conf, Maybe FilePath)
    forall a. FilePath -> a
    error' (FilePath -> IO (Conf, Maybe FilePath))
    -> ShowS -> FilePath -> IO (Conf, Maybe FilePath)
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    . ShowS
    forall a. Show a => a -> FilePath
    show) (Conf, Maybe FilePath) -> IO (Conf, Maybe FilePath)
    forall a. a -> IO a
    forall (m :: * -> *) a. Monad m => a -> m a
    return

    -- | Read this config file and parse its contents, or return an error message.
    readConfFile :: FilePath -> IO (Either String (Conf, Maybe FilePath))
    readConfFile :: FilePath -> IO (Either FilePath (Conf, Maybe FilePath))
    readConfFile FilePath
    f = (IOError -> IO (Either FilePath (Conf, Maybe FilePath)))
    -> IO (Either FilePath (Conf, Maybe FilePath))
    -> IO (Either FilePath (Conf, Maybe FilePath))
    forall e a. Exception e => (e -> IO a) -> IO a -> IO a
    handle (\(IOError
    e::IOError) -> Either FilePath (Conf, Maybe FilePath)
    -> IO (Either FilePath (Conf, Maybe FilePath))
    forall a. a -> IO a
    forall (m :: * -> *) a. Monad m => a -> m a
    return (Either FilePath (Conf, Maybe FilePath)
     -> IO (Either FilePath (Conf, Maybe FilePath)))
    -> Either FilePath (Conf, Maybe FilePath)
    -> IO (Either FilePath (Conf, Maybe FilePath))
    forall a b. (a -> b) -> a -> b
    $ FilePath -> Either FilePath (Conf, Maybe FilePath)
    forall a b. a -> Either a b
    Left (FilePath -> Either FilePath (Conf, Maybe FilePath))
    -> FilePath -> Either FilePath (Conf, Maybe FilePath)
    forall a b. (a -> b) -> a -> b
    $ IOError -> FilePath
    forall a. Show a => a -> FilePath
    show IOError
    e) (IO (Either FilePath (Conf, Maybe FilePath))
     -> IO (Either FilePath (Conf, Maybe FilePath)))
    -> IO (Either FilePath (Conf, Maybe FilePath))
    -> IO (Either FilePath (Conf, Maybe FilePath))
    forall a b. (a -> b) -> a -> b
    $ do
      -- avoid GHC 9.10.1's ugly stack trace when calling readFile on a nonexistent file
      exists <- FilePath -> IO Bool
    doesFileExist FilePath
    f
      case exists of
        Bool
    False -> Either FilePath (Conf, Maybe FilePath)
    -> IO (Either FilePath (Conf, Maybe FilePath))
    forall a. a -> IO a
    forall (m :: * -> *) a. Monad m => a -> m a
    return (Either FilePath (Conf, Maybe FilePath)
     -> IO (Either FilePath (Conf, Maybe FilePath)))
    -> Either FilePath (Conf, Maybe FilePath)
    -> IO (Either FilePath (Conf, Maybe FilePath))
    forall a b. (a -> b) -> a -> b
    $ FilePath -> Either FilePath (Conf, Maybe FilePath)
    forall a b. a -> Either a b
    Left (FilePath -> Either FilePath (Conf, Maybe FilePath))
    -> FilePath -> Either FilePath (Conf, Maybe FilePath)
    forall a b. (a -> b) -> a -> b
    $ FilePath
    f FilePath -> ShowS
    forall a. Semigroup a => a -> a -> a
    <> FilePath
    " does not exist"
        Bool
    True -> do
          ecs <- FilePath -> IO FilePath
    readFile FilePath
    f IO FilePath
    -> (FilePath
        -> Either
             (ParseErrorBundle Text HledgerParseErrorData) [ConfSection])
    -> IO
         (Either
            (ParseErrorBundle Text HledgerParseErrorData) [ConfSection])
    forall (f :: * -> *) a b. Functor f => f a -> (a -> b) -> f b
    <&> FilePath
    -> Text
    -> Either
         (ParseErrorBundle Text HledgerParseErrorData) [ConfSection]
    parseConf FilePath
    f (Text
     -> Either
          (ParseErrorBundle Text HledgerParseErrorData) [ConfSection])
    -> (FilePath -> Text)
    -> FilePath
    -> Either
         (ParseErrorBundle Text HledgerParseErrorData) [ConfSection]
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    . FilePath -> Text
    T.pack
          case ecs of
            Left ParseErrorBundle Text HledgerParseErrorData
    err -> Either FilePath (Conf, Maybe FilePath)
    -> IO (Either FilePath (Conf, Maybe FilePath))
    forall a. a -> IO a
    forall (m :: * -> *) a. Monad m => a -> m a
    return (Either FilePath (Conf, Maybe FilePath)
     -> IO (Either FilePath (Conf, Maybe FilePath)))
    -> Either FilePath (Conf, Maybe FilePath)
    -> IO (Either FilePath (Conf, Maybe FilePath))
    forall a b. (a -> b) -> a -> b
    $ FilePath -> Either FilePath (Conf, Maybe FilePath)
    forall a b. a -> Either a b
    Left (FilePath -> Either FilePath (Conf, Maybe FilePath))
    -> FilePath -> Either FilePath (Conf, Maybe FilePath)
    forall a b. (a -> b) -> a -> b
    $ ParseErrorBundle Text HledgerParseErrorData -> FilePath
    forall s e.
    (VisualStream s, TraversableStream s, ShowErrorComponent e) =>
    ParseErrorBundle s e -> FilePath
    errorBundlePretty ParseErrorBundle Text HledgerParseErrorData
    err -- customErrorBundlePretty err
            Right [ConfSection]
    cs -> Either FilePath (Conf, Maybe FilePath)
    -> IO (Either FilePath (Conf, Maybe FilePath))
    forall a. a -> IO a
    forall (m :: * -> *) a. Monad m => a -> m a
    return (Either FilePath (Conf, Maybe FilePath)
     -> IO (Either FilePath (Conf, Maybe FilePath)))
    -> Either FilePath (Conf, Maybe FilePath)
    -> IO (Either FilePath (Conf, Maybe FilePath))
    forall a b. (a -> b) -> a -> b
    $ (Conf, Maybe FilePath) -> Either FilePath (Conf, Maybe FilePath)
    forall a b. b -> Either a b
    Right (Conf
    nullconf{
              confFile     = f
              ,confFormat   = 1
              ,confSections = cs
              },
              FilePath -> Maybe FilePath
    forall a. a -> Maybe a
    Just FilePath
    f
              )

    -- -- | Like readConf, but throw an error on failure.
    -- readConfFile' :: FilePath -> IO (Conf, Maybe FilePath)
    -- readConfFile' f = readConfFile f >>= either (error' . show) return

    -- | Get the highest precedence config file, based on the current directory.
    activeConfFile :: IO (Maybe FilePath)
    activeConfFile :: IO (Maybe FilePath)
    activeConfFile = [FilePath] -> Maybe FilePath
    forall a. [a] -> Maybe a
    headMay ([FilePath] -> Maybe FilePath)
    -> IO [FilePath] -> IO (Maybe FilePath)
    forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
    <$> IO [FilePath]
    confFiles

    -- | Get the highest precedence local config file:
    -- a config file in the current directory or above, that is not a user-wide config file.
    activeLocalConfFile :: IO (Maybe FilePath)
    activeLocalConfFile :: IO (Maybe FilePath)
    activeLocalConfFile = do
      ufs <- IO [FilePath]
    userConfFiles
      mf <- headMay <$> confFiles
      return $ case mf of
        Just FilePath
    f | FilePath
    f FilePath -> [FilePath] -> Bool
    forall (t :: * -> *) a. (Foldable t, Eq a) => a -> t a -> Bool
    `notElem` [FilePath]
    ufs -> FilePath -> Maybe FilePath
    forall a. a -> Maybe a
    Just FilePath
    f
        Maybe FilePath
    _ -> Maybe FilePath
    forall a. Maybe a
    Nothing

    -- | Get the highest precedence user-wide config file, based on the current directory.
    -- (This may not be the active config file.)
    activeUserConfFile :: IO (Maybe FilePath)
    activeUserConfFile :: IO (Maybe FilePath)
    activeUserConfFile = [FilePath] -> Maybe FilePath
    forall a. [a] -> Maybe a
    headMay ([FilePath] -> Maybe FilePath)
    -> IO [FilePath] -> IO (Maybe FilePath)
    forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
    <$> IO [FilePath]
    userConfFiles

    -- | Get the possibleConfFiles which exist, based on the current directory.
    confFiles :: IO [FilePath]
    confFiles :: IO [FilePath]
    confFiles = IO [FilePath]
    possibleConfFiles IO [FilePath] -> ([FilePath] -> IO [FilePath]) -> IO [FilePath]
    forall a b. IO a -> (a -> IO b) -> IO b
    forall (m :: * -> *) a b. Monad m => m a -> (a -> m b) -> m b
    >>= [FilePath] -> IO [FilePath]
    existingFiles

    -- | Get the possibleUserConfFiles which exist, based on the current directory.
    userConfFiles :: IO [FilePath]
    userConfFiles :: IO [FilePath]
    userConfFiles = IO [FilePath]
    possibleUserConfFiles IO [FilePath] -> ([FilePath] -> IO [FilePath]) -> IO [FilePath]
    forall a b. IO a -> (a -> IO b) -> IO b
    forall (m :: * -> *) a b. Monad m => m a -> (a -> m b) -> m b
    >>= [FilePath] -> IO [FilePath]
    existingFiles

    -- | Filter a list of paths to just the existing files.
    existingFiles :: [FilePath] -> IO [FilePath]
    existingFiles :: [FilePath] -> IO [FilePath]
    existingFiles [FilePath]
    fs =
      ([Maybe FilePath] -> [FilePath])
    -> IO [Maybe FilePath] -> IO [FilePath]
    forall a b. (a -> b) -> IO a -> IO b
    forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
    fmap [Maybe FilePath] -> [FilePath]
    forall a. [Maybe a] -> [a]
    catMaybes (IO [Maybe FilePath] -> IO [FilePath])
    -> IO [Maybe FilePath] -> IO [FilePath]
    forall a b. (a -> b) -> a -> b
    $ [FilePath]
    -> (FilePath -> IO (Maybe FilePath)) -> IO [Maybe FilePath]
    forall (t :: * -> *) (m :: * -> *) a b.
    (Traversable t, Monad m) =>
    t a -> (a -> m b) -> m (t b)
    forM [FilePath]
    fs ((FilePath -> IO (Maybe FilePath)) -> IO [Maybe FilePath])
    -> (FilePath -> IO (Maybe FilePath)) -> IO [Maybe FilePath]
    forall a b. (a -> b) -> a -> b
    $ \FilePath
    f -> do
        exists <- FilePath -> IO Bool
    doesFileExist FilePath
    f
        return $ if exists then Just f else Nothing

    -- | Get the possible paths for a hledger config file, highest precedence first:
    -- hledger.conf in the current directory,
    -- hledger.conf in any parent directory,
    -- .hledger.conf in the home directory,
    -- or hledger.conf in the XdgConfig directory.
    possibleConfFiles :: IO [FilePath]
    possibleConfFiles :: IO [FilePath]
    possibleConfFiles = do
      ds   <- IO [FilePath]
    possibleConfDirs
      home <- getHomeDirectory
      return $ dbg8 "possible config file paths" $
        flip map ds $ \FilePath
    d -> FilePath
    d FilePath -> ShowS
    </> if FilePath
    dFilePath -> FilePath -> Bool
    forall a. Eq a => a -> a -> Bool
    ==FilePath
    home then FilePath
    ".hledger.conf" else FilePath
    "hledger.conf"

    -- | Like possibleConfFiles, but consider only user-wide hledger config files:
    -- .hledger.conf in the home directory,
    -- or hledger.conf in the XdgConfig directory.
    possibleUserConfFiles :: IO [FilePath]
    possibleUserConfFiles :: IO [FilePath]
    possibleUserConfFiles = do
      home <- IO FilePath
    getHomeDirectory
      xdgc <- getXdgDirectory XdgConfig "hledger"
      let ds = [FilePath
    home,FilePath
    xdgc]
      return $ dbg8 "possible user config file paths" $
        flip map ds $ \FilePath
    d -> FilePath
    d FilePath -> ShowS
    </> if FilePath
    dFilePath -> FilePath -> Bool
    forall a. Eq a => a -> a -> Bool
    ==FilePath
    home then FilePath
    ".hledger.conf" else FilePath
    "hledger.conf"

    -- | Get the directories where a hledger config file could be, highest precedence first:
    -- the current directory, any parent directory, the home directory, or the XdgConfig directory.
    possibleConfDirs :: IO [FilePath]
    possibleConfDirs :: IO [FilePath]
    possibleConfDirs = do
      xdgc <- XdgDirectory -> FilePath -> IO FilePath
    getXdgDirectory XdgDirectory
    XdgConfig FilePath
    "hledger"
      home <- getHomeDirectory
      here <- getCurrentDirectory
      dirs <- getDirsUpToRoot here
      let dirs2 = if FilePath
    home FilePath -> [FilePath] -> Bool
    forall a. Eq a => a -> [a] -> Bool
    forall (t :: * -> *) a. (Foldable t, Eq a) => a -> t a -> Bool
    `elem` [FilePath]
    dirs then [FilePath]
    dirs else [FilePath]
    dirs [FilePath] -> [FilePath] -> [FilePath]
    forall a. Semigroup a => a -> a -> a
    <> [FilePath
    home]
      let dirs3 = if FilePath
    xdgc FilePath -> [FilePath] -> Bool
    forall a. Eq a => a -> [a] -> Bool
    forall (t :: * -> *) a. (Foldable t, Eq a) => a -> t a -> Bool
    `elem` [FilePath]
    dirs2 then [FilePath]
    dirs2 else [FilePath]
    dirs2 [FilePath] -> [FilePath] -> [FilePath]
    forall a. Semigroup a => a -> a -> a
    <> [FilePath
    xdgc]
      return $ dbg8 "searching config dirs" dirs3

    -- | Get this directory and all of its parents up to /.
    getDirsUpToRoot :: FilePath -> IO [FilePath]
    getDirsUpToRoot :: FilePath -> IO [FilePath]
    getDirsUpToRoot FilePath
    dir = [FilePath] -> IO [FilePath]
    forall a. a -> IO a
    forall (m :: * -> *) a. Monad m => a -> m a
    return ([FilePath] -> IO [FilePath]) -> [FilePath] -> IO [FilePath]
    forall a b. (a -> b) -> a -> b
    $ [FilePath] -> FilePath -> [FilePath]
    go [] FilePath
    dir
      where
        go :: [FilePath] -> FilePath -> [FilePath]
    go [FilePath]
    seen FilePath
    d = if
          | FilePath
    d FilePath -> [FilePath] -> Bool
    forall a. Eq a => a -> [a] -> Bool
    forall (t :: * -> *) a. (Foldable t, Eq a) => a -> t a -> Bool
    `elem` [FilePath]
    seen Bool -> Bool -> Bool
    || [FilePath] -> Int
    forall a. [a] -> Int
    forall (t :: * -> *) a. Foldable t => t a -> Int
    length [FilePath]
    seen Int -> Int -> Bool
    forall a. Ord a => a -> a -> Bool
    >= Int
    100 -> []  -- just in case
          | FilePath
    dFilePath -> FilePath -> Bool
    forall a. Eq a => a -> a -> Bool
    ==FilePath
    "/"    -> [FilePath
    d]
          | Bool
    otherwise -> FilePath
    d FilePath -> [FilePath] -> [FilePath]
    forall a. a -> [a] -> [a]
    : [FilePath] -> FilePath -> [FilePath]
    go (FilePath
    dFilePath -> [FilePath] -> [FilePath]
    forall a. a -> [a] -> [a]
    :[FilePath]
    seen) (ShowS
    takeDirectory FilePath
    d)

    -- config file parsing

    parseConf :: FilePath -> Text -> Either (ParseErrorBundle Text HledgerParseErrorData) [ConfSection]
    parseConf :: FilePath
    -> Text
    -> Either
         (ParseErrorBundle Text HledgerParseErrorData) [ConfSection]
    parseConf = Parsec HledgerParseErrorData Text [ConfSection]
    -> FilePath
    -> Text
    -> Either
         (ParseErrorBundle Text HledgerParseErrorData) [ConfSection]
    forall e s a.
    Parsec e s a -> FilePath -> s -> Either (ParseErrorBundle s e) a
    runParser Parsec HledgerParseErrorData Text [ConfSection]
    confp

    dp :: String -> TextParser m ()
    dp :: forall (m :: * -> *). FilePath -> TextParser m ()
    dp = TextParser m () -> FilePath -> TextParser m ()
    forall a b. a -> b -> a
    const (TextParser m () -> FilePath -> TextParser m ())
    -> TextParser m () -> FilePath -> TextParser m ()
    forall a b. (a -> b) -> a -> b
    $ () -> TextParser m ()
    forall a. a -> ParsecT HledgerParseErrorData Text m a
    forall (m :: * -> *) a. Monad m => a -> m a
    return ()  -- no-op
    -- dp = dbgparse 0  -- trace parse state at this --debug level

    whitespacep, commentlinesp, restoflinep :: TextParser Identity ()
    whitespacep :: TextParser Identity ()
    whitespacep   = ParsecT HledgerParseErrorData Text Identity FilePath
    -> TextParser Identity ()
    forall (f :: * -> *) a. Functor f => f a -> f ()
    void (ParsecT HledgerParseErrorData Text Identity FilePath
     -> TextParser Identity ())
    -> ParsecT HledgerParseErrorData Text Identity FilePath
    -> TextParser Identity ()
    forall a b. (a -> b) -> a -> b
    $ {- dp "whitespacep"   >> -} ParsecT HledgerParseErrorData Text Identity Char
    -> ParsecT HledgerParseErrorData Text Identity FilePath
    forall (m :: * -> *) a. MonadPlus m => m a -> m [a]
    many ParsecT HledgerParseErrorData Text Identity Char
    forall s (m :: * -> *).
    (Stream s, Char ~ Token s) =>
    ParsecT HledgerParseErrorData s m Char
    spacenonewline
    commentlinesp :: TextParser Identity ()
    commentlinesp = ParsecT HledgerParseErrorData Text Identity [()]
    -> TextParser Identity ()
    forall (f :: * -> *) a. Functor f => f a -> f ()
    void (ParsecT HledgerParseErrorData Text Identity [()]
     -> TextParser Identity ())
    -> ParsecT HledgerParseErrorData Text Identity [()]
    -> TextParser Identity ()
    forall a b. (a -> b) -> a -> b
    $ {- dp "commentlinesp" >> -} TextParser Identity ()
    -> ParsecT HledgerParseErrorData Text Identity [()]
    forall (m :: * -> *) a. MonadPlus m => m a -> m [a]
    many (FilePath -> TextParser Identity ()
    forall (m :: * -> *). FilePath -> TextParser m ()
    emptyorcommentlinep2 FilePath
    "#")
    restoflinep :: TextParser Identity ()
    restoflinep   = TextParser Identity () -> TextParser Identity ()
    forall (f :: * -> *) a. Functor f => f a -> f ()
    void (TextParser Identity () -> TextParser Identity ())
    -> TextParser Identity () -> TextParser Identity ()
    forall a b. (a -> b) -> a -> b
    $ {- dp "restoflinep"   >> -} TextParser Identity ()
    whitespacep TextParser Identity ()
    -> TextParser Identity () -> TextParser Identity ()
    forall a b.
    ParsecT HledgerParseErrorData Text Identity a
    -> ParsecT HledgerParseErrorData Text Identity b
    -> ParsecT HledgerParseErrorData Text Identity b
    forall (m :: * -> *) a b. Monad m => m a -> m b -> m b
    >> FilePath -> TextParser Identity ()
    forall (m :: * -> *). FilePath -> TextParser m ()
    emptyorcommentlinep2 FilePath
    "#"

    confp :: TextParser Identity [ConfSection]  -- a monadic TextParser to allow reusing other hledger parsers
    confp :: Parsec HledgerParseErrorData Text [ConfSection]
    confp = do
      FilePath -> TextParser Identity ()
    forall (m :: * -> *). FilePath -> TextParser m ()
    dp FilePath
    "confp"
      TextParser Identity ()
    commentlinesp
      genas <- ParsecT HledgerParseErrorData Text Identity FilePath
    -> ParsecT HledgerParseErrorData Text Identity [FilePath]
    forall (m :: * -> *) a. MonadPlus m => m a -> m [a]
    many ParsecT HledgerParseErrorData Text Identity FilePath
    arglinep
      let s = FilePath -> [FilePath] -> ConfSection
    ConfSection FilePath
    "general" [FilePath]
    genas
      ss <- many $ do
        (n, ma) <- sectionstartp
        as <- many arglinep
        return $ ConfSection n (maybe as (:as) ma)
      eof
      return $ s:ss

    -- parse a section name and possibly arguments written on the same line
    sectionstartp :: TextParser Identity (String, Maybe String)
    sectionstartp :: TextParser Identity (FilePath, Maybe FilePath)
    sectionstartp = do
      FilePath -> TextParser Identity ()
    forall (m :: * -> *). FilePath -> TextParser m ()
    dp FilePath
    "sectionstartp"
      Token Text
    -> ParsecT HledgerParseErrorData Text Identity (Token Text)
    forall e s (m :: * -> *).
    (MonadParsec e s m, Token s ~ Char) =>
    Token s -> m (Token s)
    char Char
    Token Text
    '['
      n <- ShowS
    -> ParsecT HledgerParseErrorData Text Identity FilePath
    -> ParsecT HledgerParseErrorData Text Identity FilePath
    forall a b.
    (a -> b)
    -> ParsecT HledgerParseErrorData Text Identity a
    -> ParsecT HledgerParseErrorData Text Identity b
    forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
    fmap ShowS
    strip (ParsecT HledgerParseErrorData Text Identity FilePath
     -> ParsecT HledgerParseErrorData Text Identity FilePath)
    -> ParsecT HledgerParseErrorData Text Identity FilePath
    -> ParsecT HledgerParseErrorData Text Identity FilePath
    forall a b. (a -> b) -> a -> b
    $ ParsecT HledgerParseErrorData Text Identity Char
    -> ParsecT HledgerParseErrorData Text Identity FilePath
    forall (m :: * -> *) a. MonadPlus m => m a -> m [a]
    some (ParsecT HledgerParseErrorData Text Identity Char
     -> ParsecT HledgerParseErrorData Text Identity FilePath)
    -> ParsecT HledgerParseErrorData Text Identity Char
    -> ParsecT HledgerParseErrorData Text Identity FilePath
    forall a b. (a -> b) -> a -> b
    $ [Token Text]
    -> ParsecT HledgerParseErrorData Text Identity (Token Text)
    forall (f :: * -> *) e s (m :: * -> *).
    (Foldable f, MonadParsec e s m) =>
    f (Token s) -> m (Token s)
    noneOf FilePath
    [Token Text]
    "]#\n"
      char ']'
      -- dp "sectionstartp2"
      whitespacep
      -- dp "sectionstartp3"
      ma <- fmap (fmap strip) $ optional $ some $ noneOf "#\n"
      -- dp "sectionstartp4"
      restoflinep
      -- dp "sectionstartp5"
      commentlinesp
      -- dp "sectionstartp6"
      return (n, ma)

    arglinep :: TextParser Identity String
    arglinep :: ParsecT HledgerParseErrorData Text Identity FilePath
    arglinep = do
      FilePath -> TextParser Identity ()
    forall (m :: * -> *). FilePath -> TextParser m ()
    dp FilePath
    "arglinep"
      ParsecT HledgerParseErrorData Text Identity Char
    -> TextParser Identity ()
    forall a.
    ParsecT HledgerParseErrorData Text Identity a
    -> TextParser Identity ()
    forall e s (m :: * -> *) a. MonadParsec e s m => m a -> m ()
    notFollowedBy (ParsecT HledgerParseErrorData Text Identity Char
     -> TextParser Identity ())
    -> ParsecT HledgerParseErrorData Text Identity Char
    -> TextParser Identity ()
    forall a b. (a -> b) -> a -> b
    $ Token Text
    -> ParsecT HledgerParseErrorData Text Identity (Token Text)
    forall e s (m :: * -> *).
    (MonadParsec e s m, Token s ~ Char) =>
    Token s -> m (Token s)
    char Char
    Token Text
    '['
      -- dp "arglinep2"
      TextParser Identity ()
    whitespacep
      -- dp "arglinep3"
      a <- ParsecT HledgerParseErrorData Text Identity Char
    -> ParsecT HledgerParseErrorData Text Identity FilePath
    forall (m :: * -> *) a. MonadPlus m => m a -> m [a]
    some (ParsecT HledgerParseErrorData Text Identity Char
     -> ParsecT HledgerParseErrorData Text Identity FilePath)
    -> ParsecT HledgerParseErrorData Text Identity Char
    -> ParsecT HledgerParseErrorData Text Identity FilePath
    forall a b. (a -> b) -> a -> b
    $ [Token Text]
    -> ParsecT HledgerParseErrorData Text Identity (Token Text)
    forall (f :: * -> *) e s (m :: * -> *).
    (Foldable f, MonadParsec e s m) =>
    f (Token s) -> m (Token s)
    noneOf FilePath
    [Token Text]
    "#\n"
      -- dp "arglinep4"
      restoflinep <|> whitespacep  -- whitespace / same-line comment, possibly with no newline
      commentlinesp
      return $ strip a

    -- initialiseAndParseJournal :: ErroringJournalParser IO ParsedJournal -> InputOpts
    --                           -> FilePath -> Text -> ExceptT String IO Journal
    -- initialiseAndParseJournal parser iopts f txt =
    --     prettyParseErrors $ runParserT (evalStateT parser initJournal) f txt
    --   where
    --     y = first3 . toGregorian $ _ioDay iopts
    --     initJournal = nulljournal{jparsedefaultyear = Just y, jincludefilestack = [f]}
    --     -- Flatten parse errors and final parse errors, and output each as a pretty String.
    --     prettyParseErrors :: ExceptT FinalParseError IO (Either (ParseErrorBundle Text HledgerParseErrorData) a)
    --                       -> ExceptT String IO a
    --     prettyParseErrors = withExceptT customErrorBundlePretty . liftEither
    --                     <=< withExceptT (finalErrorBundlePretty . attachSource f txt)