diff --git a/hledger-lib/Hledger/Read/Common.hs b/hledger-lib/Hledger/Read/Common.hs index d0e0383da..569f504ba 100644 --- a/hledger-lib/Hledger/Read/Common.hs +++ b/hledger-lib/Hledger/Read/Common.hs @@ -50,6 +50,7 @@ module Hledger.Read.Common ( getAccountAliases, clearAccountAliases, journalAddFile, + getSpecialSeparators, -- * parsers -- ** transaction bits @@ -162,7 +163,7 @@ data InputOpts = InputOpts { mformat_ :: Maybe StorageFormat -- ^ a file/storage format to try, unless overridden -- by a filename prefix. Nothing means try all. ,mrules_file_ :: Maybe FilePath -- ^ a conversion rules file to use (when reading CSV) - ,separator_ :: Char -- ^ the separator to use (when reading CSV) + ,separator_ :: Maybe Char -- ^ the separator to use (when reading CSV) ,aliases_ :: [String] -- ^ account name aliases to apply ,anon_ :: Bool -- ^ do light anonymisation/obfuscation of the data ,ignore_assertions_ :: Bool -- ^ don't check balance assertions @@ -175,14 +176,14 @@ data InputOpts = InputOpts { instance Default InputOpts where def = definputopts definputopts :: InputOpts -definputopts = InputOpts def def ',' def def def def True def def +definputopts = InputOpts def def def def def def def True def def rawOptsToInputOpts :: RawOpts -> InputOpts rawOptsToInputOpts rawopts = InputOpts{ -- files_ = listofstringopt "file" rawopts mformat_ = Nothing ,mrules_file_ = maybestringopt "rules-file" rawopts - ,separator_ = fromMaybe ',' (maybecharopt "separator" rawopts) + ,separator_ = maybestringopt "separator" rawopts >>= getSpecialSeparators ,aliases_ = listofstringopt "alias" rawopts ,anon_ = boolopt "anon" rawopts ,ignore_assertions_ = boolopt "ignore-assertions" rawopts @@ -194,6 +195,14 @@ rawOptsToInputOpts rawopts = InputOpts{ --- * parsing utilities +-- | Parse special separator names TAB and SPACE, or return the first +-- character. Return Nothing on empty string +getSpecialSeparators :: String -> Maybe Char +getSpecialSeparators "SPACE" = Just ' ' +getSpecialSeparators "TAB" = Just '\t' +getSpecialSeparators (x:_) = Just x +getSpecialSeparators [] = Nothing + -- | Run a text parser in the identity monad. See also: parseWithState. runTextParser, rtp :: TextParser Identity a -> Text -> Either (ParseErrorBundle Text CustomErr) a diff --git a/hledger-lib/Hledger/Read/CsvReader.hs b/hledger-lib/Hledger/Read/CsvReader.hs index 4dd8abc28..8f03e80c9 100644 --- a/hledger-lib/Hledger/Read/CsvReader.hs +++ b/hledger-lib/Hledger/Read/CsvReader.hs @@ -71,7 +71,7 @@ import Text.Printf (printf) import Hledger.Data import Hledger.Utils -import Hledger.Read.Common (Reader(..),InputOpts(..),amountp, statusp, genericSourcePos, finaliseJournal) +import Hledger.Read.Common (Reader(..),InputOpts(..),amountp, statusp, genericSourcePos, getSpecialSeparators, finaliseJournal) type CSV = [Record] @@ -104,13 +104,13 @@ parse iopts f t = do -- better preemptively reverse them once more. XXX inefficient pj' = journalReverse pj -getSeparatorFromRules :: Char -> CsvRules -> Char -getSeparatorFromRules defaultSeparator rules = - maybe defaultSeparator id (getSeparator <$> getDirective "separator" rules) - where getSeparator :: String -> Char - getSeparator "SPACE" = ' ' - getSeparator "TAB" = '\t' - getSeparator x = head x +-- | Decide which separator to get. +-- If the external separator is provided, take it. Otherwise, look at the rules. Finally, return ','. +getSeparator :: Maybe Char -> CsvRules -> Char +getSeparator externalSeparator rules = head $ + catMaybes [ externalSeparator + , getDirective "separator" rules >>= getSpecialSeparators + , Just ','] -- | Read a Journal from the given CSV data (and filename, used for error -- messages), or return an error. Proceed as follows: @@ -123,9 +123,9 @@ getSeparatorFromRules defaultSeparator rules = -- 4. if the rules file didn't exist, create it with the default rules and filename -- 5. return the transactions as a Journal -- @ -readJournalFromCsv :: Char -> Maybe FilePath -> FilePath -> Text -> IO (Either String Journal) +readJournalFromCsv :: Maybe Char -> Maybe FilePath -> FilePath -> Text -> IO (Either String Journal) readJournalFromCsv _ Nothing "-" _ = return $ Left "please use --rules-file when reading CSV from stdin" -readJournalFromCsv separator mrulesfile csvfile csvdata = +readJournalFromCsv commandLineSeparator mrulesfile csvfile csvdata = handle (\(e::IOException) -> return $ Left $ show e) $ do -- make and throw an IO exception.. which we catch and convert to an Either above ? @@ -156,7 +156,7 @@ readJournalFromCsv separator mrulesfile csvfile csvdata = records <- (either throwerr id . dbg2 "validateCsv" . validateCsv rules skiplines . dbg2 "parseCsv") - `fmap` parseCsv (getSeparatorFromRules separator rules) parsecfilename csvdata + `fmap` parseCsv (getSeparator commandLineSeparator rules) parsecfilename csvdata dbg1IO "first 3 csv records" $ take 3 records -- identify header lines diff --git a/tests/csv.test b/tests/csv.test index 51804fdb8..a4b932d6f 100644 --- a/tests/csv.test +++ b/tests/csv.test @@ -527,7 +527,7 @@ $ ./hledger-csv >=0 -# 25. specify alternative delimiter in rules +# 25. specify alternative whitespace delimiter in rules < 2009/10/01 Flubber Co 50 123 @@ -539,7 +539,40 @@ separator TAB $ ./hledger-csv 2009/10/01 Flubber Co - (assets:myacct) $50 = $123 + (assets:myacct) $50 = $123 + +>=0 + +# 26. specify char delimiter in rules +< +2009/10/01;Flubber Co;50;123 + +RULES +fields date, description, amount, balance +currency $ +account1 (assets:myacct) +separator ; + +$ ./hledger-csv +2009/10/01 Flubber Co + (assets:myacct) $50 = $123 + +>=0 + +# 27. command line delimiter overrides configuration file +< +2009/10/01 Flubber Co 50 123 + +RULES +fields date, description, amount, balance +currency $ +account1 (assets:myacct) +separator ; + +$ ./hledger-csv --separator 'TAB' +2009/10/01 Flubber Co + (assets:myacct) $50 = $123 + >=0 ## 25. A single unbalanced posting with number other than 1 also should not generate a balancing posting.