hledger-lib 1.51.1 → 1.51.2
raw patch · 8 files changed
+120/−52 lines, 8 filesdep ~basePVP: major bump suggested
API removals or changes: PVP suggests a major version bump
Dependency ranges changed: base
API changes (from Hackage documentation)
+ Hledger.Read: defaultExistingJournalPath :: IO String
+ Hledger.Read: defaultExistingJournalPathSafely :: IO (Either String String)
+ Hledger.Read: readPossibleJournalFile :: InputOpts -> PrefixedFilePath -> ExceptT String IO Journal
+ Hledger.Utils.IO: expandPathOrGlob :: FilePath -> FilePath -> IO FilePath
- Hledger.Data: Journal :: Maybe Year -> Maybe (CommoditySymbol, AmountStyle) -> Maybe DecimalMark -> [AccountName] -> [AccountAlias] -> [TimeclockEntry] -> [FilePath] -> [(Payee, PayeeDeclarationInfo)] -> [(TagName, TagDeclarationInfo)] -> [(AccountName, AccountDeclarationInfo)] -> Map AccountName [Tag] -> Map AccountType [AccountName] -> Map AccountName AccountType -> Map CommoditySymbol Commodity -> Map CommoditySymbol AmountStyle -> Map CommoditySymbol AmountStyle -> [PriceDirective] -> [MarketPrice] -> [TransactionModifier] -> [PeriodicTransaction] -> [Transaction] -> Text -> [(FilePath, Text)] -> POSIXTime -> Journal
+ Hledger.Data: Journal :: Maybe Year -> Maybe (CommoditySymbol, AmountStyle) -> Maybe DecimalMark -> [AccountName] -> [AccountAlias] -> [TimeclockEntry] -> [(FilePath, FilePath)] -> [(Payee, PayeeDeclarationInfo)] -> [(TagName, TagDeclarationInfo)] -> [(AccountName, AccountDeclarationInfo)] -> Map AccountName [Tag] -> Map AccountType [AccountName] -> Map AccountName AccountType -> Map CommoditySymbol Commodity -> Map CommoditySymbol AmountStyle -> Map CommoditySymbol AmountStyle -> [PriceDirective] -> [MarketPrice] -> [TransactionModifier] -> [PeriodicTransaction] -> [Transaction] -> Text -> [(FilePath, Text)] -> POSIXTime -> Journal
- Hledger.Data: [jincludefilestack] :: Journal -> [FilePath]
+ Hledger.Data: [jincludefilestack] :: Journal -> [(FilePath, FilePath)]
- Hledger.Data.Types: Journal :: Maybe Year -> Maybe (CommoditySymbol, AmountStyle) -> Maybe DecimalMark -> [AccountName] -> [AccountAlias] -> [TimeclockEntry] -> [FilePath] -> [(Payee, PayeeDeclarationInfo)] -> [(TagName, TagDeclarationInfo)] -> [(AccountName, AccountDeclarationInfo)] -> Map AccountName [Tag] -> Map AccountType [AccountName] -> Map AccountName AccountType -> Map CommoditySymbol Commodity -> Map CommoditySymbol AmountStyle -> Map CommoditySymbol AmountStyle -> [PriceDirective] -> [MarketPrice] -> [TransactionModifier] -> [PeriodicTransaction] -> [Transaction] -> Text -> [(FilePath, Text)] -> POSIXTime -> Journal
+ Hledger.Data.Types: Journal :: Maybe Year -> Maybe (CommoditySymbol, AmountStyle) -> Maybe DecimalMark -> [AccountName] -> [AccountAlias] -> [TimeclockEntry] -> [(FilePath, FilePath)] -> [(Payee, PayeeDeclarationInfo)] -> [(TagName, TagDeclarationInfo)] -> [(AccountName, AccountDeclarationInfo)] -> Map AccountName [Tag] -> Map AccountType [AccountName] -> Map AccountName AccountType -> Map CommoditySymbol Commodity -> Map CommoditySymbol AmountStyle -> Map CommoditySymbol AmountStyle -> [PriceDirective] -> [MarketPrice] -> [TransactionModifier] -> [PeriodicTransaction] -> [Transaction] -> Text -> [(FilePath, Text)] -> POSIXTime -> Journal
- Hledger.Data.Types: [jincludefilestack] :: Journal -> [FilePath]
+ Hledger.Data.Types: [jincludefilestack] :: Journal -> [(FilePath, FilePath)]
Files
- CHANGES.md +15/−0
- Hledger/Data/Journal.hs +1/−1
- Hledger/Data/Types.hs +2/−2
- Hledger/Read.hs +51/−27
- Hledger/Read/Common.hs +5/−3
- Hledger/Read/JournalReader.hs +18/−12
- Hledger/Utils/IO.hs +21/−0
- hledger-lib.cabal +7/−7
CHANGES.md view
@@ -17,6 +17,11 @@ For user-visible changes, see the hledger package changelog. +# 1.51.2 2026-01-08++- Allow base 4.22 / ghc 9.14.++ # 1.51.1 2025-12-08 @@ -34,6 +39,16 @@ - Hledger.Utils.String: quoteForCommandLine now quotes some additional problem characters, and no longer quotes "7". [#2468]++API++- Hledger.Read:+ defaultExistingJournalPath+ defaultExistingJournalPathSafely+ readPossibleJournalFile++- Hledger.Utils.IO:+ expandPathOrGlob # 1.50.4 2025-12-04
Hledger/Data/Journal.hs view
@@ -245,7 +245,7 @@ journalConcat j1 j2 = let f1 = takeFileName $ journalFilePath j1- f2 = maybe "(unknown)" takeFileName $ headMay $ jincludefilestack j2 -- XXX more accurate than journalFilePath for some reason+ f2 = maybe "(unknown)" takeFileName $ fmap fst $ headMay $ jincludefilestack j2 -- XXX more accurate than journalFilePath for some reason in dbgJournalAcctDeclOrder ("journalConcat: " <> f1 <> " <> " <> f2 <> ", acct decls renumbered: ") $ journalRenumberAccountDeclarations $
Hledger/Data/Types.hs view
@@ -633,8 +633,8 @@ ,jparsealiases :: [AccountAlias] -- ^ the current account name aliases in effect, specified by alias directives (& options ?) -- ,jparsetransactioncount :: Integer -- ^ the current count of transactions parsed so far (only journal format txns, currently) ,jparsetimeclockentries :: [TimeclockEntry] -- ^ timeclock sessions which have not been clocked out- ,jincludefilestack :: [FilePath]- -- principal data+ ,jincludefilestack :: [(FilePath, FilePath)] -- ^ (absolute path, canonical path) of included files, most recent first+-- principal data ,jdeclaredpayees :: [(Payee,PayeeDeclarationInfo)] -- ^ Payees declared by payee directives, in parse order. ,jdeclaredtags :: [(TagName,TagDeclarationInfo)] -- ^ Tags declared by tag directives, in parse order. ,jdeclaredaccounts :: [(AccountName,AccountDeclarationInfo)] -- ^ Accounts declared by account directives, in parse order.
Hledger/Read.hs view
@@ -93,6 +93,8 @@ defaultJournalWithSafely, defaultJournalPath, defaultJournalPathSafely,+ defaultExistingJournalPath,+ defaultExistingJournalPathSafely, requireJournalFileExists, ensureJournalFileExists, journalEnvVar,@@ -103,6 +105,7 @@ runExceptT, readJournal, readJournalFile,+ readPossibleJournalFile, readJournalFiles, readJournalFilesAndLatestDates, @@ -147,27 +150,24 @@ import Data.Text qualified as T import Data.Text.IO qualified as T import Data.Time (Day)-import Safe (headDef, headMay)+import Safe (headDef) import System.Directory (doesFileExist) import System.Environment (getEnv) import System.FilePath ((<.>), (</>), splitDirectories, splitFileName, takeFileName) import System.Info (os) import System.IO (Handle, hPutStrLn, stderr)+import Text.Printf (printf) import Hledger.Data.Dates (getCurrentDay, parsedate, showDate)+import Hledger.Data.Journal (journalNumberTransactions, nulljournal)+import Hledger.Data.JournalChecks (journalStrictChecks) import Hledger.Data.Types import Hledger.Read.Common import Hledger.Read.InputOptions import Hledger.Read.JournalReader as JournalReader import Hledger.Read.CsvReader (tests_CsvReader) import Hledger.Read.RulesReader (tests_RulesReader)--- import Hledger.Read.TimedotReader (tests_TimedotReader)--- import Hledger.Read.TimeclockReader (tests_TimeclockReader) import Hledger.Utils-import Prelude hiding (getContents, writeFile)-import Hledger.Data.JournalChecks (journalStrictChecks)-import Text.Printf (printf)-import Hledger.Data.Journal (journalNumberTransactions) --- ** doctest setup -- $setup@@ -202,34 +202,29 @@ ,C.Handler (\(e :: C.IOException) -> return $ Left $ show e) ] --- | Get the default journal file path, and check that it exists; or raise an error.+-- | Get the default journal file path - either $LEDGER_FILE or $HOME/.hledger.journal file. -- -- This looks for the LEDGER_FILE environment variable, like Ledger.--- The value should be a file path; ~ at the start is supported, meaning user's home directory.--- The value can also be a glob pattern, for convenience; if so we consider only the first matched file.--- If no such file exists, an error is raised.+-- The value should be a file path, possibly with ~ at the start meaning the current user's home directory.+-- Or the value can be a glob pattern (containing *, ?, [ or {) ), in which case the first matching file path is used.+-- When it's a glob pattern that matches no existing files, an error is raised. ----- If LEDGER_FILE is unset or set to the empty string, we return a default file path:--- @.hledger.journal@ in the user's home directory.--- Or if we can't find the user's home directory, in the current directory.--- If this default file doesn't exist, an error is raised.+-- If LEDGER_FILE is unset or set to the empty string, this returns a default file path:+-- @.hledger.journal@ in the user's home directory,+-- or if we can't find the user's home directory, in the current directory. --+-- The referenced file can be nonexistent.+-- defaultJournalPath :: IO String defaultJournalPath = do- ledgerfile <- getEnv journalEnvVar `C.catch` (\(_::C.IOException) -> return "")- if null ledgerfile+ p <- getEnv journalEnvVar `C.catch` (\(_::C.IOException) -> return "")+ if null p then do homedir <- fromMaybe "" <$> getHomeSafe let defaultfile = homedir </> journalDefaultFilename- exists <- doesFileExist defaultfile- if exists then return defaultfile- -- else error' $ "LEDGER_FILE is unset and \"" <> defaultfile <> "\" was not found"- else error' $ "neither LEDGER_FILE nor \"" <> defaultfile <> "\" was found"- else do- mf <- headMay <$> expandGlob "." ledgerfile `C.catch` (\(_::C.IOException) -> return [])- case mf of- Just f -> return f- Nothing -> error' $ "LEDGER_FILE points to nonexistent \"" <> ledgerfile <> "\""+ return defaultfile+ else+ expandPathOrGlob "." p -- | Like defaultJournalPath, but return an error message instead of raising an error. defaultJournalPathSafely :: IO (Either String String)@@ -242,6 +237,24 @@ ,C.Handler (\(e :: C.IOException) -> return $ Left $ show e) ] +-- | Like defaultJournalPath, but also checks that the file exists, and raises an error if it doesn't.+defaultExistingJournalPath :: IO String+defaultExistingJournalPath = do+ f <- defaultJournalPath+ requireJournalFileExists f+ return f++-- | Like defaultExistingJournalPath, but returns an error message instead of raising an error.+defaultExistingJournalPathSafely :: IO (Either String String)+defaultExistingJournalPathSafely = (do+ f <- defaultExistingJournalPath+ return $ Right f+ )+ `C.catches` [+ C.Handler (\(e :: C.ErrorCall) -> return $ Left $ show e)+ ,C.Handler (\(e :: C.IOException) -> return $ Left $ show e)+ ]+ -- | @readJournal iopts mfile txt@ -- -- Read a Journal from some handle, with strict checks if enabled,@@ -322,7 +335,18 @@ else return (j, Nothing) --- | Read a Journal from each specified file path (using @readJournalFile@) +-- | Like readJournalFile, but if the file does not exist, returns an empty journal+-- with the file path set. This is useful for commands like add and import that+-- need to work with a potentially non-existent journal file.+readPossibleJournalFile :: InputOpts -> PrefixedFilePath -> ExceptT String IO Journal+readPossibleJournalFile iopts prefixedfile = do+ let (_, f) = splitReaderPrefix prefixedfile+ exists <- liftIO $ doesFileExist f+ if exists+ then readJournalFile iopts prefixedfile+ else return $ nulljournal{jfiles = [(f, "")]}++-- | Read a Journal from each specified file path (using @readJournalFile@) -- and combine them into one; or return the first error message. -- -- Combining Journals means concatenating them, basically.
Hledger/Read/Common.hs view
@@ -151,6 +151,7 @@ import Data.Time.Clock.POSIX (getPOSIXTime) import Data.Time.LocalTime (LocalTime(..), TimeOfDay(..)) import Data.Word (Word8)+import System.Directory (canonicalizePath) import System.FilePath (takeFileName) import System.IO (Handle) import Text.Megaparsec@@ -314,11 +315,12 @@ -- Timeclock and Timedot files. initialiseAndParseJournal :: ErroringJournalParser IO ParsedJournal -> InputOpts -> FilePath -> Text -> ExceptT String IO Journal-initialiseAndParseJournal parser iopts f txt =- prettyParseErrors $ runParserT (evalStateT parser initJournal) f txt+initialiseAndParseJournal parser iopts f txt = do+ cf <- liftIO $ canonicalizePath f+ prettyParseErrors $ runParserT (evalStateT parser (initJournal cf)) f txt where y = first3 . toGregorian $ _ioDay iopts- initJournal = nulljournal{jparsedefaultyear = Just y, jincludefilestack = [f]}+ initJournal cf = nulljournal{jparsedefaultyear = Just y, jincludefilestack = [(f, cf)]} -- 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
Hledger/Read/JournalReader.hs view
@@ -388,19 +388,25 @@ -- Make ** also match file name parts like zsh's GLOB_STAR_SHORT. let- expandedglob' =+ finalglob = -- ** without a slash is equivalent to **/* case regexReplace (toRegex' $ T.pack "\\*\\*([^/\\])") "**/*\\1" expandedglob of Right s -> s Left _ -> expandedglob -- ignore any error, there should be none -- Compile as a Pattern. Can throw an error.- g <- case tryCompileWith compDefault{errorRecovery=False} expandedglob' of+ pat <- case tryCompileWith compDefault{errorRecovery=False} finalglob of Left e -> customFailure $ parseErrorAt off $ "Invalid glob pattern: " ++ e Right x -> pure x -- Find all matched paths. These might include directories or the current file.- paths <- liftIO $ globDir1 g cwd+ -- Glob seems to get attributes of all files in a directory, which disturbs build systems+ -- which detect dependencies based on filesystem operations (eg tup).+ -- So avoid using it if not needed.+ paths <- liftIO $+ if isLiteral pat+ then return $ if isAbsolute finalglob then [finalglob] else [cwd </> finalglob]+ else globDir1 pat cwd -- Exclude any directories or symlinks to directories, and canonicalise, and sort. files <- liftIO $@@ -410,12 +416,11 @@ -- Throw an error if one of these files is among the grandparent files, forming a cycle. -- Though, ignore the immediate parent file for convenience. XXX inconsistent - should it ignore all cyclic includes ?- -- We used to store the canonical paths, then switched to non-canonical paths for more useful output,- -- which means for each include directive we must re-canonicalise everything here; noticeable ? XXX+ -- Use canonical paths for cycle detection, show nominal absolute paths in error messages. parentj <- get let parentfiles = jincludefilestack parentj- cparentfiles <- liftIO $ mapM canonicalizePath parentfiles- let cparentf = take 1 parentfiles+ cparentfiles = map snd parentfiles+ cparentf = take 1 cparentfiles files2 <- forM files $ \f -> do cf <- liftIO $ canonicalizePath f if@@ -442,8 +447,9 @@ -- Read the file's content, or throw an error childInput <- lift $ readFilePortably filepath & handleIOError off "failed to read a file"+ cfilepath <- liftIO $ canonicalizePath filepath parentj <- get- let initChildj = newJournalWithParseStateFrom filepath parentj+ let initChildj = newJournalWithParseStateFrom filepath cfilepath parentj -- Choose a reader based on the file path prefix or file extension, -- defaulting to JournalReader. Duplicating readJournal a bit here.@@ -470,14 +476,14 @@ where childfilename = takeFileName filepath- parentfilename = maybe "(unknown)" takeFileName $ headMay $ jincludefilestack parentj -- XXX more accurate than journalFilePath for some reason+ parentfilename = maybe "(unknown)" takeFileName $ fmap fst $ headMay $ jincludefilestack parentj -- XXX more accurate than journalFilePath for some reason -- And update the current parse state. put parentj' where- newJournalWithParseStateFrom :: FilePath -> Journal -> Journal- newJournalWithParseStateFrom filepath j = nulljournal{+ newJournalWithParseStateFrom :: FilePath -> FilePath -> Journal -> Journal+ newJournalWithParseStateFrom filepath cfilepath j = nulljournal{ jparsedefaultyear = jparsedefaultyear j ,jparsedefaultcommodity = jparsedefaultcommodity j ,jparseparentaccounts = jparseparentaccounts j@@ -486,7 +492,7 @@ ,jdeclaredcommodities = jdeclaredcommodities j -- ,jparsetransactioncount = jparsetransactioncount j ,jparsetimeclockentries = jparsetimeclockentries j- ,jincludefilestack = filepath : jincludefilestack j+ ,jincludefilestack = (filepath, cfilepath) : jincludefilestack j } -- Get the absolute path of the file referenced by this parse position.
Hledger/Utils/IO.hs view
@@ -40,6 +40,7 @@ expandHomePath, expandPath, expandGlob,+ expandPathOrGlob, sortByModTime, openFileOrStdin, readFileOrStdinPortably,@@ -419,6 +420,26 @@ -- For a more elaborate glob expander, see 'findMatchedFiles' (used by the include directive). expandGlob :: FilePath -> FilePath -> IO [FilePath] expandGlob curdir p = expandPath curdir p >>= glob <&> sort -- PARTIAL:++-- | Like expandPath, but if the path contains glob metacharacters (* ? [ {),+-- treats it as a glob pattern and expands it, returning the first match.+-- Raises an error if the glob pattern matches no files.+-- If the path contains no glob metacharacters, just expands ~ and returns the path,+-- even if the file doesn't exist yet.+-- This is useful for options like -f and LEDGER_FILE that should:+-- - accept non-existent files (for commands like add/import that create them)+-- - expand glob patterns and error if they don't match anything+expandPathOrGlob :: FilePath -> FilePath -> IO FilePath+expandPathOrGlob curdir p = do+ let hasGlobChars = any (`elem` p) ("*?[{" :: [Char])+ if hasGlobChars+ then do+ matches <- expandGlob curdir p `catch` (\(_::IOException) -> return [])+ case headMay matches of+ Just f -> return f+ Nothing -> error' $ "glob pattern \"" <> p <> "\" matched no files"+ else+ expandPath curdir p -- | Given a list of existing file paths, sort them by modification time (from oldest to newest). sortByModTime :: [FilePath] -> IO [FilePath]
hledger-lib.cabal view
@@ -5,7 +5,7 @@ -- see: https://github.com/sol/hpack name: hledger-lib-version: 1.51.1+version: 1.51.2 synopsis: A library providing the core functionality of hledger description: This library contains hledger's core functionality. It is used by most hledger* packages so that they support the same@@ -124,7 +124,7 @@ hs-source-dirs: ./ ghc-options: -Wall -Wno-incomplete-uni-patterns -Wno-missing-signatures -Wno-orphans -Wno-type-defaults -Wno-unused-do-bind- cpp-options: -DVERSION="1.51.1"+ cpp-options: -DVERSION="1.51.2" build-depends: Decimal >=0.5.1 , Glob >=0.9@@ -132,7 +132,7 @@ , aeson-pretty , ansi-terminal >=0.9 , array- , base >=4.18 && <4.22+ , base >=4.18 && <4.23 , blaze-html , blaze-markup >=0.5.1 , bytestring@@ -184,7 +184,7 @@ hs-source-dirs: test ghc-options: -Wall -Wno-incomplete-uni-patterns -Wno-missing-signatures -Wno-orphans -Wno-type-defaults -Wno-unused-do-bind- cpp-options: -DVERSION="1.51.1"+ cpp-options: -DVERSION="1.51.2" build-depends: Decimal >=0.5.1 , Glob >=0.7@@ -192,7 +192,7 @@ , aeson-pretty , ansi-terminal >=0.9 , array- , base >=4.18 && <4.22+ , base >=4.18 && <4.23 , blaze-html , blaze-markup >=0.5.1 , bytestring@@ -245,7 +245,7 @@ hs-source-dirs: test ghc-options: -Wall -Wno-incomplete-uni-patterns -Wno-missing-signatures -Wno-orphans -Wno-type-defaults -Wno-unused-do-bind- cpp-options: -DVERSION="1.51.1"+ cpp-options: -DVERSION="1.51.2" build-depends: Decimal >=0.5.1 , Glob >=0.9@@ -253,7 +253,7 @@ , aeson-pretty , ansi-terminal >=0.9 , array- , base >=4.18 && <4.22+ , base >=4.18 && <4.23 , blaze-html , blaze-markup >=0.5.1 , bytestring