mirror of
https://github.com/simonmichael/hledger.git
synced 2024-09-20 02:37:12 +03:00
refactor/beef up readJournal/readJournalFile
This commit is contained in:
parent
4d7a809c4a
commit
6eb7ad28e1
@ -173,11 +173,14 @@ data Journal = Journal {
|
||||
-- raise an error.
|
||||
type JournalUpdate = ErrorT String IO (Journal -> Journal)
|
||||
|
||||
-- | The id of a data format understood by hledger, eg @journal@ or @csv@.
|
||||
type Format = String
|
||||
|
||||
-- | A hledger journal reader is a triple of format name, format-detecting
|
||||
-- predicate, and a parser to Journal.
|
||||
data Reader = Reader {
|
||||
-- name of the format this reader handles
|
||||
rFormat :: String
|
||||
rFormat :: Format
|
||||
-- quickly check if this reader can probably handle the given file path and file content
|
||||
,rDetector :: FilePath -> String -> Bool
|
||||
-- really parse the given file path and file content, returning a journal or error
|
||||
|
@ -1,27 +1,31 @@
|
||||
{-|
|
||||
|
||||
Read hledger data from various data formats, and related utilities.
|
||||
This is the entry point to hledger's reading system, which can read
|
||||
Journals from various data formats. Use this module if you want to
|
||||
parse journal data or read journal files; it should not be necessary
|
||||
to import modules below this one.
|
||||
|
||||
-}
|
||||
|
||||
module Hledger.Read (
|
||||
tests_Hledger_Read,
|
||||
readJournalFile,
|
||||
-- * Journal reading utilities
|
||||
defaultJournalPath,
|
||||
defaultJournal,
|
||||
readJournal,
|
||||
journalFromPathAndString,
|
||||
readJournalFile,
|
||||
requireJournalFileExists,
|
||||
ensureJournalFileExists,
|
||||
-- * Temporary parser exports for Convert
|
||||
ledgeraccountname,
|
||||
myJournalPath,
|
||||
myJournal,
|
||||
someamount,
|
||||
journalenvvar,
|
||||
journaldefaultfilename,
|
||||
requireJournalFile,
|
||||
ensureJournalFile,
|
||||
-- * Tests
|
||||
tests_Hledger_Read,
|
||||
)
|
||||
where
|
||||
import Control.Monad.Error
|
||||
import Data.Either (partitionEithers)
|
||||
import Data.List
|
||||
import Data.Maybe
|
||||
import Safe (headDef)
|
||||
import System.Directory (doesFileExist, getHomeDirectory)
|
||||
import System.Environment (getEnv)
|
||||
@ -32,7 +36,7 @@ import Test.HUnit
|
||||
import Text.Printf
|
||||
|
||||
import Hledger.Data.Dates (getCurrentDay)
|
||||
import Hledger.Data.Types (Journal(..), Reader(..))
|
||||
import Hledger.Data.Types (Journal(..), Reader(..), Format)
|
||||
import Hledger.Data.Journal (nullctx)
|
||||
import Hledger.Read.JournalReader as JournalReader
|
||||
import Hledger.Read.TimelogReader as TimelogReader
|
||||
@ -42,9 +46,9 @@ import Prelude hiding (getContents, writeFile)
|
||||
import Hledger.Utils.UTF8 (getContents, hGetContents, writeFile)
|
||||
|
||||
|
||||
journalenvvar = "LEDGER_FILE"
|
||||
journalenvvar2 = "LEDGER"
|
||||
journaldefaultfilename = ".hledger.journal"
|
||||
journalEnvVar = "LEDGER_FILE"
|
||||
journalEnvVar2 = "LEDGER"
|
||||
journalDefaultFilename = ".hledger.journal"
|
||||
|
||||
-- The available data file readers, each one handling a particular data
|
||||
-- format. The first is also used as the default for unknown formats.
|
||||
@ -58,54 +62,84 @@ readers = [
|
||||
-- | All the data formats we can read.
|
||||
formats = map rFormat readers
|
||||
|
||||
-- | Get the default journal file path specified by the environment.
|
||||
-- Like ledger, we look first for the LEDGER_FILE environment
|
||||
-- variable, and if that does not exist, for the legacy LEDGER
|
||||
-- environment variable. If neither is set, or the value is blank,
|
||||
-- return the hard-coded default, which is @.hledger.journal@ in the
|
||||
-- users's home directory (or in the current directory, if we cannot
|
||||
-- determine a home directory).
|
||||
defaultJournalPath :: IO String
|
||||
defaultJournalPath = do
|
||||
s <- envJournalPath
|
||||
if null s then defaultJournalPath else return s
|
||||
where
|
||||
envJournalPath = getEnv journalEnvVar `catch` (\_ -> getEnv journalEnvVar2 `catch` (\_ -> return ""))
|
||||
defaultJournalPath = do
|
||||
home <- getHomeDirectory `catch` (\_ -> return "")
|
||||
return $ home </> journalDefaultFilename
|
||||
|
||||
-- | Read the default journal file specified by the environment, or raise an error.
|
||||
defaultJournal :: IO Journal
|
||||
defaultJournal = defaultJournalPath >>= readJournalFile Nothing Nothing >>= either error' return
|
||||
|
||||
-- | Find the reader which can handle the given format, if any.
|
||||
-- Typically there is just one; only the first is returned.
|
||||
readerForFormat :: String -> Maybe Reader
|
||||
readerForFormat :: Format -> Maybe Reader
|
||||
readerForFormat s | null rs = Nothing
|
||||
| otherwise = Just $ head rs
|
||||
where
|
||||
rs = filter ((s==).rFormat) readers :: [Reader]
|
||||
|
||||
-- | Do our best to read a Journal from this string using the specified
|
||||
-- data format, or if unspecified, trying all supported formats until one
|
||||
-- succeeds. The file path is provided as an extra hint. Returns an error
|
||||
-- message if the format is unsupported or if it is supported but parsing
|
||||
-- fails.
|
||||
journalFromPathAndString :: Maybe String -> FilePath -> String -> IO (Either String Journal)
|
||||
journalFromPathAndString format fp s = do
|
||||
-- | Read a Journal from this string or give an error message, using
|
||||
-- the specified data format or trying all known formats. CSV
|
||||
-- conversion rules may be provided for better conversion of that
|
||||
-- format, and/or a file path for better error messages.
|
||||
readJournal :: Maybe Format -> Maybe CsvReader.CsvRules -> Maybe FilePath -> String -> IO (Either String Journal)
|
||||
readJournal format rules path s = do
|
||||
let readerstotry = case format of Nothing -> readers
|
||||
Just f -> case readerForFormat f of Just r -> [r]
|
||||
Nothing -> []
|
||||
(errors, journals) <- partitionEithers `fmap` mapM (tryReader fp s) readerstotry
|
||||
(errors, journals) <- partitionEithers `fmap` mapM (tryReader s path) readerstotry -- XXX lazify
|
||||
case journals of j:_ -> return $ Right j
|
||||
_ -> return $ Left $ bestErrorMsg errors fp s
|
||||
-- where
|
||||
|
||||
tryReader :: FilePath -> String -> Reader -> IO (Either String Journal)
|
||||
tryReader fp s r = do -- printf "trying to read %s format\n" (rFormat r)
|
||||
(runErrorT . (rParser r) fp) s
|
||||
_ -> return $ Left $ bestErrorMsg errors s path
|
||||
where
|
||||
path' = fromMaybe "(string)" path
|
||||
tryReader :: String -> Maybe FilePath -> Reader -> IO (Either String Journal)
|
||||
tryReader s path r = do -- printf "trying to read %s format\n" (rFormat r)
|
||||
(runErrorT . (rParser r) path') s
|
||||
|
||||
-- unknown format
|
||||
bestErrorMsg [] fp _ = printf "could not parse %sdata in %s" (fmt formats) fp
|
||||
where fmt [] = ""
|
||||
fmt [f] = f ++ " "
|
||||
fmt fs = intercalate ", " (init fs) ++ " or " ++ last fs ++ " "
|
||||
bestErrorMsg :: [String] -> String -> Maybe FilePath -> String
|
||||
bestErrorMsg [] _ path = printf "could not parse %sdata%s" fmts pathmsg
|
||||
where fmts = case formats of
|
||||
[] -> ""
|
||||
[f] -> f ++ " "
|
||||
fs -> intercalate ", " (init fs) ++ " or " ++ last fs ++ " "
|
||||
pathmsg = case path of
|
||||
Nothing -> ""
|
||||
Just p -> " in "++p
|
||||
-- one or more errors - report (the most appropriate ?) one
|
||||
bestErrorMsg es fp s = printf "could not parse %s data in %s\n%s" (rFormat r) fp e
|
||||
bestErrorMsg es s path = printf "could not parse %s data%s\n%s" (rFormat r) pathmsg e
|
||||
where (r,e) = headDef (head readers, head es) $ filter detects $ zip readers es
|
||||
detects (r,_) = (rDetector r) fp s
|
||||
detects (r,_) = (rDetector r) path' s
|
||||
pathmsg = case path of
|
||||
Nothing -> ""
|
||||
Just p -> " in "++p
|
||||
|
||||
-- | Read a journal from this file, using the specified data format or
|
||||
-- trying all known formats, or give an error string.
|
||||
readJournalFile :: Maybe String -> FilePath -> IO (Either String Journal)
|
||||
readJournalFile format "-" = getContents >>= journalFromPathAndString format "(stdin)"
|
||||
readJournalFile format f = do
|
||||
requireJournalFile f
|
||||
withFile f ReadMode $ \h -> hGetContents h >>= journalFromPathAndString format f
|
||||
-- | Read a Journal from this file (or stdin if the filename is -) or
|
||||
-- give an error message, using the specified data format or trying
|
||||
-- all known formats. CSV conversion rules may be provided for better
|
||||
-- conversion of that format.
|
||||
readJournalFile :: Maybe Format -> Maybe CsvReader.CsvRules -> FilePath -> IO (Either String Journal)
|
||||
readJournalFile format rules "-" = getContents >>= readJournal format rules (Just "(stdin)")
|
||||
readJournalFile format rules f = do
|
||||
requireJournalFileExists f
|
||||
withFile f ReadMode $ \h -> hGetContents h >>= readJournal format rules (Just f)
|
||||
|
||||
-- | If the specified journal file does not exist, give a helpful error and quit.
|
||||
requireJournalFile :: FilePath -> IO ()
|
||||
requireJournalFile f = do
|
||||
requireJournalFileExists :: FilePath -> IO ()
|
||||
requireJournalFileExists f = do
|
||||
exists <- doesFileExist f
|
||||
when (not exists) $ do
|
||||
hPrintf stderr "The hledger journal file \"%s\" was not found.\n" f
|
||||
@ -114,8 +148,8 @@ requireJournalFile f = do
|
||||
exitFailure
|
||||
|
||||
-- | Ensure there is a journal file at the given path, creating an empty one if needed.
|
||||
ensureJournalFile :: FilePath -> IO ()
|
||||
ensureJournalFile f = do
|
||||
ensureJournalFileExists :: FilePath -> IO ()
|
||||
ensureJournalFileExists f = do
|
||||
exists <- doesFileExist f
|
||||
when (not exists) $ do
|
||||
hPrintf stderr "Creating hledger journal file \"%s\".\n" f
|
||||
@ -129,31 +163,6 @@ newJournalContent = do
|
||||
d <- getCurrentDay
|
||||
return $ printf "; journal created %s by hledger\n" (show d)
|
||||
|
||||
-- | Read a Journal from this string, using the specified data format or
|
||||
-- trying all known formats, or give an error string.
|
||||
readJournal :: Maybe String -> String -> IO (Either String Journal)
|
||||
readJournal format s = journalFromPathAndString format "(string)" s
|
||||
|
||||
-- | Get the user's journal file path. Like ledger, we look first for the
|
||||
-- LEDGER_FILE environment variable, and if that does not exist, for the
|
||||
-- legacy LEDGER environment variable. If neither is set, or the value is
|
||||
-- blank, return the default journal file path, which is
|
||||
-- ".hledger.journal" in the users's home directory, or if we cannot
|
||||
-- determine that, in the current directory.
|
||||
myJournalPath :: IO String
|
||||
myJournalPath = do
|
||||
s <- envJournalPath
|
||||
if null s then defaultJournalPath else return s
|
||||
where
|
||||
envJournalPath = getEnv journalenvvar `catch` (\_ -> getEnv journalenvvar2 `catch` (\_ -> return ""))
|
||||
defaultJournalPath = do
|
||||
home <- getHomeDirectory `catch` (\_ -> return "")
|
||||
return $ home </> journaldefaultfilename
|
||||
|
||||
-- | Read the user's default journal file, or give an error.
|
||||
myJournal :: IO Journal
|
||||
myJournal = myJournalPath >>= readJournalFile Nothing >>= either error' return
|
||||
|
||||
tests_Hledger_Read = TestList
|
||||
[
|
||||
tests_Hledger_Read_JournalReader,
|
||||
@ -162,7 +171,7 @@ tests_Hledger_Read = TestList
|
||||
|
||||
"journalFile" ~: do
|
||||
assertBool "journalFile should parse an empty file" (isRight $ parseWithCtx nullctx JournalReader.journalFile "")
|
||||
jE <- readJournal Nothing "" -- don't know how to get it from journalFile
|
||||
jE <- readJournal Nothing Nothing Nothing "" -- don't know how to get it from journalFile
|
||||
either error' (assertBool "journalFile parsing an empty file should give an empty journal" . null . jtxns) jE
|
||||
|
||||
]
|
||||
|
@ -6,8 +6,10 @@ data, like the convert command.
|
||||
-}
|
||||
|
||||
module Hledger.Read.CsvReader (
|
||||
reader,
|
||||
tests_Hledger_Read_CsvReader
|
||||
CsvRules(..),
|
||||
nullrules,
|
||||
reader,
|
||||
tests_Hledger_Read_CsvReader
|
||||
)
|
||||
where
|
||||
import Control.Monad
|
||||
|
@ -1,6 +1,8 @@
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
{-|
|
||||
Utilities common to hledger journal readers.
|
||||
|
||||
Utilities used throughout hledger's read system.
|
||||
|
||||
-}
|
||||
|
||||
module Hledger.Read.Utils
|
||||
|
@ -516,7 +516,7 @@ handleAdd = do
|
||||
|]
|
||||
Right t -> do
|
||||
let t' = txnTieKnot t -- XXX move into balanceTransaction
|
||||
liftIO $ do ensureJournalFile journalpath
|
||||
liftIO $ do ensureJournalFileExists journalpath
|
||||
appendToJournalFileOrStdout journalpath $ showTransaction t'
|
||||
-- setMessage $ toHtml $ (printf "Added transaction:\n%s" (show t') :: String)
|
||||
setMessage [$shamlet|<span>Added transaction:<small><pre>#{chomp $ show t'}</pre></small>|]
|
||||
@ -561,7 +561,7 @@ handleEdit = do
|
||||
setMessage "No change"
|
||||
redirect RedirectTemporary JournalR
|
||||
else do
|
||||
jE <- liftIO $ journalFromPathAndString Nothing journalpath tnew
|
||||
jE <- liftIO $ readJournal Nothing Nothing (Just journalpath) tnew
|
||||
either
|
||||
(\e -> do
|
||||
setMessage $ toHtml e
|
||||
|
@ -43,11 +43,11 @@ runWith opts = run opts
|
||||
| "help" `in_` (rawopts_ $ cliopts_ opts) = putStr (showModeHelp webmode) >> exitSuccess
|
||||
| "version" `in_` (rawopts_ $ cliopts_ opts) = putStrLn prognameandversion >> exitSuccess
|
||||
| "binary-filename" `in_` (rawopts_ $ cliopts_ opts) = putStrLn (binaryfilename progname)
|
||||
| otherwise = journalFilePathFromOpts (cliopts_ opts) >>= ensureJournalFile >> withJournalDo' opts web
|
||||
| otherwise = journalFilePathFromOpts (cliopts_ opts) >>= ensureJournalFileExists >> withJournalDo' opts web
|
||||
|
||||
withJournalDo' :: WebOpts -> (WebOpts -> Journal -> IO ()) -> IO ()
|
||||
withJournalDo' opts cmd = do
|
||||
journalFilePathFromOpts (cliopts_ opts) >>= readJournalFile Nothing >>=
|
||||
journalFilePathFromOpts (cliopts_ opts) >>= readJournalFile Nothing Nothing >>=
|
||||
either error' (cmd opts . journalApplyAliases (aliasesFromOpts $ cliopts_ opts))
|
||||
|
||||
-- | The web command.
|
||||
|
@ -53,8 +53,8 @@ tests_Hledger_Cli = TestList
|
||||
|
||||
|
||||
,"account directive" ~:
|
||||
let sameParse str1 str2 = do j1 <- readJournal Nothing str1 >>= either error' return
|
||||
j2 <- readJournal Nothing str2 >>= either error' return
|
||||
let sameParse str1 str2 = do j1 <- readJournal Nothing Nothing Nothing str1 >>= either error' return
|
||||
j2 <- readJournal Nothing Nothing Nothing str2 >>= either error' return
|
||||
j1 `is` j2{filereadtime=filereadtime j1, files=files j1, jContext=jContext j1}
|
||||
in TestList
|
||||
[
|
||||
@ -85,7 +85,7 @@ tests_Hledger_Cli = TestList
|
||||
)
|
||||
|
||||
,"account directive should preserve \"virtual\" posting type" ~: do
|
||||
j <- readJournal Nothing "!account test\n2008/12/07 One\n (from) $-1\n (to) $1\n" >>= either error' return
|
||||
j <- readJournal Nothing Nothing Nothing "!account test\n2008/12/07 One\n (from) $-1\n (to) $1\n" >>= either error' return
|
||||
let p = head $ tpostings $ head $ jtxns j
|
||||
assertBool "" $ (paccount p) == "test:from"
|
||||
assertBool "" $ (ptype p) == VirtualPosting
|
||||
@ -93,7 +93,7 @@ tests_Hledger_Cli = TestList
|
||||
]
|
||||
|
||||
,"account aliases" ~: do
|
||||
Right j <- readJournal Nothing "!alias expenses = equity:draw:personal\n1/1\n (expenses:food) 1\n"
|
||||
Right j <- readJournal Nothing Nothing Nothing "!alias expenses = equity:draw:personal\n1/1\n (expenses:food) 1\n"
|
||||
let p = head $ tpostings $ head $ jtxns j
|
||||
assertBool "" $ paccount p == "equity:draw:personal:food"
|
||||
|
||||
@ -235,7 +235,7 @@ tests_Hledger_Cli = TestList
|
||||
]
|
||||
|
||||
,"balance report with cost basis" ~: do
|
||||
j <- (readJournal Nothing $ unlines
|
||||
j <- (readJournal Nothing Nothing Nothing $ unlines
|
||||
[""
|
||||
,"2008/1/1 test "
|
||||
," a:b 10h @ $50"
|
||||
@ -266,7 +266,7 @@ tests_Hledger_Cli = TestList
|
||||
-- `is` "aa:aa:aaaaaaaaaaaaaa")
|
||||
|
||||
,"default year" ~: do
|
||||
j <- readJournal Nothing defaultyear_journal_str >>= either error' return
|
||||
j <- readJournal Nothing Nothing Nothing defaultyear_journal_str >>= either error' return
|
||||
tdate (head $ jtxns j) `is` fromGregorian 2009 1 1
|
||||
return ()
|
||||
|
||||
|
@ -19,7 +19,7 @@ You can use the command line:
|
||||
or ghci:
|
||||
|
||||
> $ ghci hledger
|
||||
> > j <- readJournalFile "data/sample.journal"
|
||||
> > j <- readJournalFile Nothing Nothing "data/sample.journal"
|
||||
> > register [] ["income","expenses"] j
|
||||
> 2008/01/01 income income:salary $-1 $-1
|
||||
> 2008/06/01 gift income:gifts $-1 $-2
|
||||
@ -46,7 +46,7 @@ import System.Exit
|
||||
import System.Process
|
||||
import Text.Printf
|
||||
|
||||
import Hledger (ensureJournalFile)
|
||||
import Hledger (ensureJournalFileExists)
|
||||
import Hledger.Cli.Add
|
||||
import Hledger.Cli.Balance
|
||||
import Hledger.Cli.Convert
|
||||
@ -73,7 +73,7 @@ main = do
|
||||
| (null matchedaddon) && "version" `in_` (rawopts_ opts) = putStrLn prognameandversion
|
||||
| (null matchedaddon) && "binary-filename" `in_` (rawopts_ opts) = putStrLn $ binaryfilename progname
|
||||
| null cmd = putStr $ showModeHelp mainmode'
|
||||
| cmd `isPrefixOf` "add" = showModeHelpOr addmode $ journalFilePathFromOpts opts >>= ensureJournalFile >> withJournalDo opts add
|
||||
| cmd `isPrefixOf` "add" = showModeHelpOr addmode $ journalFilePathFromOpts opts >>= ensureJournalFileExists >> withJournalDo opts add
|
||||
| cmd `isPrefixOf` "convert" = showModeHelpOr convertmode $ convert opts
|
||||
| cmd `isPrefixOf` "test" = showModeHelpOr testmode $ runtests opts
|
||||
| any (cmd `isPrefixOf`) ["accounts","balance"] = showModeHelpOr accountsmode $ withJournalDo opts balance
|
||||
|
@ -422,10 +422,10 @@ defaultBalanceFormatString = [
|
||||
]
|
||||
|
||||
-- | Get the journal file path from options, an environment variable, or a default.
|
||||
-- If the path contains a literal tilde raise an error to avoid confusion.
|
||||
-- If the path contains a literal tilde raise an error to avoid confusion. XXX
|
||||
journalFilePathFromOpts :: CliOpts -> IO String
|
||||
journalFilePathFromOpts opts = do
|
||||
f <- myJournalPath
|
||||
f <- defaultJournalPath
|
||||
let f' = fromMaybe f $ file_ opts
|
||||
if '~' `elem` f'
|
||||
then error' $ printf "~ in the journal file path is not supported, please adjust (%s)" f'
|
||||
|
@ -48,20 +48,20 @@ withJournalDo opts cmd = do
|
||||
-- We kludgily read the file before parsing to grab the full text, unless
|
||||
-- it's stdin, or it doesn't exist and we are adding. We read it strictly
|
||||
-- to let the add command work.
|
||||
journalFilePathFromOpts opts >>= readJournalFile Nothing >>=
|
||||
journalFilePathFromOpts opts >>= readJournalFile Nothing Nothing >>=
|
||||
either error' (cmd opts . journalApplyAliases (aliasesFromOpts opts))
|
||||
|
||||
-- -- | Get a journal from the given string and options, or throw an error.
|
||||
-- readJournalWithOpts :: CliOpts -> String -> IO Journal
|
||||
-- readJournalWithOpts opts s = readJournal Nothing s >>= either error' return
|
||||
-- readJournalWithOpts opts s = readJournal Nothing Nothing Nothing s >>= either error' return
|
||||
|
||||
-- | Get a journal from the given string, or throw an error.
|
||||
readJournal' :: String -> IO Journal
|
||||
readJournal' s = readJournal Nothing s >>= either error' return
|
||||
readJournal' s = readJournal Nothing Nothing Nothing s >>= either error' return
|
||||
|
||||
-- | Re-read a journal from its data file, or return an error string.
|
||||
journalReload :: Journal -> IO (Either String Journal)
|
||||
journalReload j = readJournalFile Nothing $ journalFilePath j
|
||||
journalReload j = readJournalFile Nothing Nothing $ journalFilePath j
|
||||
|
||||
-- | Re-read a journal from its data file mostly, only if the file has
|
||||
-- changed since last read (or if there is no file, ie data read from
|
||||
|
Loading…
Reference in New Issue
Block a user