fix!: utf-8: Use with-utf8 to ensure all files are read and written with utf8 encoding. (#1619)
May also fix #1154, #1033, #708, #536, #73: testing is needed. This aims to solve all problems where misconfigured locales lead to parsers failing on utf8-encoded data. This should hopefully avoid encoding issues, but since it fundamentally alters how encoding is dealt with it may lead to unexpected outcomes. Widespread testing on a number of different platforms would be useful.
This commit is contained in:
		
							parent
							
								
									db26456e1c
								
							
						
					
					
						commit
						e233f001c5
					
				| @ -8,7 +8,7 @@ | |||||||
| {-| Construct two balance reports for two different time periods and use one of the as "budget" for | {-| Construct two balance reports for two different time periods and use one of the as "budget" for | ||||||
|     the other, thus comparing them |     the other, thus comparing them | ||||||
| -} | -} | ||||||
| import Data.Text.Lazy.IO as TL | import Data.Text.Lazy.IO as TL (putStrLn)  -- Only putStr and friends are safe | ||||||
| import System.Environment (getArgs) | import System.Environment (getArgs) | ||||||
| import Hledger.Cli | import Hledger.Cli | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -112,7 +112,7 @@ import Data.Time.Calendar (toGregorian) | |||||||
| import Data.Time.Calendar.OrdinalDate (mondayStartWeek, sundayStartWeek, toOrdinalDate) | import Data.Time.Calendar.OrdinalDate (mondayStartWeek, sundayStartWeek, toOrdinalDate) | ||||||
| import Data.Text (Text, isPrefixOf, pack, unpack) | import Data.Text (Text, isPrefixOf, pack, unpack) | ||||||
| import qualified Data.Text as T | import qualified Data.Text as T | ||||||
| import qualified Data.Text.IO as T | import qualified Data.Text.IO as T (putStrLn)  -- Only putStr and friends are safe | ||||||
| import qualified Hledger.Data as H | import qualified Hledger.Data as H | ||||||
| import qualified Hledger.Query as H | import qualified Hledger.Query as H | ||||||
| import qualified Hledger.Read as H | import qualified Hledger.Read as H | ||||||
|  | |||||||
| @ -11,7 +11,7 @@ import System.Environment (getArgs) | |||||||
| import Hledger.Cli | import Hledger.Cli | ||||||
| import qualified Data.Map as M | import qualified Data.Map as M | ||||||
| import Data.Map.Merge.Strict | import Data.Map.Merge.Strict | ||||||
| import qualified Data.Text.Lazy.IO as TL | import qualified Data.Text.Lazy.IO as TL (putStrLn)  -- Only putStr and friends are safe | ||||||
| 
 | 
 | ||||||
| appendReports :: MultiBalanceReport -> MultiBalanceReport -> MultiBalanceReport | appendReports :: MultiBalanceReport -> MultiBalanceReport -> MultiBalanceReport | ||||||
| appendReports r1 r2 = | appendReports r1 r2 = | ||||||
|  | |||||||
| @ -9,7 +9,7 @@ | |||||||
| {-# LANGUAGE RecordWildCards #-} | {-# LANGUAGE RecordWildCards #-} | ||||||
| 
 | 
 | ||||||
| import Data.String.QQ (s) | import Data.String.QQ (s) | ||||||
| import qualified Data.Text.IO as T | import qualified Data.Text.IO as T (putStrLn)  -- Only putStr and friends are safe | ||||||
| import Hledger | import Hledger | ||||||
| import Hledger.Cli | import Hledger.Cli | ||||||
| 
 | 
 | ||||||
|  | |||||||
							
								
								
									
										8
									
								
								examples/unicode-bom.journal
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								examples/unicode-bom.journal
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,8 @@ | |||||||
|  | ; unicode in description, account name and currency symbol | ||||||
|  | 2010/1/1 ß | ||||||
|  |   (ß)  10 ß | ||||||
|  | 
 | ||||||
|  | ; as above but with characters from code pages not installed on a western ms windows machine | ||||||
|  | 2010/1/1 проверка | ||||||
|  |   (проверка)  10 проверка | ||||||
|  | 
 | ||||||
| @ -12,7 +12,7 @@ module Hledger.Data.PeriodicTransaction ( | |||||||
| where | where | ||||||
| 
 | 
 | ||||||
| import qualified Data.Text as T | import qualified Data.Text as T | ||||||
| import qualified Data.Text.IO as T | import qualified Data.Text.IO as TIO (putStr)  -- Only putStr and friends are safe | ||||||
| import Text.Printf | import Text.Printf | ||||||
| 
 | 
 | ||||||
| import Hledger.Data.Types | import Hledger.Data.Types | ||||||
| @ -36,7 +36,7 @@ _ptgen str = do | |||||||
|   case checkPeriodicTransactionStartDate i s t of |   case checkPeriodicTransactionStartDate i s t of | ||||||
|     Just e  -> error' e  -- PARTIAL: |     Just e  -> error' e  -- PARTIAL: | ||||||
|     Nothing -> |     Nothing -> | ||||||
|       mapM_ (T.putStr . showTransaction) $ |       mapM_ (TIO.putStr . showTransaction) $ | ||||||
|         runPeriodicTransaction |         runPeriodicTransaction | ||||||
|           nullperiodictransaction{ ptperiodexpr=t , ptspan=s, ptinterval=i, ptpostings=["a" `post` usd 1] } |           nullperiodictransaction{ ptperiodexpr=t , ptspan=s, ptinterval=i, ptpostings=["a" `post` usd 1] } | ||||||
|           nulldatespan |           nulldatespan | ||||||
| @ -48,7 +48,7 @@ _ptgenspan str span = do | |||||||
|   case checkPeriodicTransactionStartDate i s t of |   case checkPeriodicTransactionStartDate i s t of | ||||||
|     Just e  -> error' e  -- PARTIAL: |     Just e  -> error' e  -- PARTIAL: | ||||||
|     Nothing -> |     Nothing -> | ||||||
|       mapM_ (T.putStr . showTransaction) $ |       mapM_ (TIO.putStr . showTransaction) $ | ||||||
|         runPeriodicTransaction |         runPeriodicTransaction | ||||||
|           nullperiodictransaction{ ptperiodexpr=t , ptspan=s, ptinterval=i, ptpostings=["a" `post` usd 1] } |           nullperiodictransaction{ ptperiodexpr=t , ptspan=s, ptinterval=i, ptpostings=["a" `post` usd 1] } | ||||||
|           span |           span | ||||||
|  | |||||||
| @ -63,10 +63,10 @@ modifyTransactions atypes atags styles d tmods ts = do | |||||||
| -- Currently the only kind of modification possible is adding automated | -- Currently the only kind of modification possible is adding automated | ||||||
| -- postings when certain other postings are present. | -- postings when certain other postings are present. | ||||||
| -- | -- | ||||||
| -- >>> import qualified Data.Text.IO as T | -- >>> import qualified Data.Text.IO as TIO (putStr)  -- Only putStr and friends are safe | ||||||
| -- >>> t = nulltransaction{tpostings=["ping" `post` usd 1]} | -- >>> t = nulltransaction{tpostings=["ping" `post` usd 1]} | ||||||
| -- >>> tmpost acc amt = TMPostingRule (acc `post` amt) False | -- >>> tmpost acc amt = TMPostingRule (acc `post` amt) False | ||||||
| -- >>> test = either putStr (T.putStr.showTransaction) . fmap ($ t) . transactionModifierToFunction (const Nothing) (const []) mempty nulldate | -- >>> test = either putStr (TIO.putStr.showTransaction) . fmap ($ t) . transactionModifierToFunction (const Nothing) (const []) mempty nulldate | ||||||
| -- >>> test $ TransactionModifier "" ["pong" `tmpost` usd 2] | -- >>> test $ TransactionModifier "" ["pong" `tmpost` usd 2] | ||||||
| -- 0000-01-01 | -- 0000-01-01 | ||||||
| --     ping           $1.00 | --     ping           $1.00 | ||||||
|  | |||||||
| @ -64,7 +64,7 @@ import Data.Ord (comparing) | |||||||
| import Data.Semigroup (sconcat) | import Data.Semigroup (sconcat) | ||||||
| import Data.Text (Text) | import Data.Text (Text) | ||||||
| import qualified Data.Text as T | import qualified Data.Text as T | ||||||
| import qualified Data.Text.IO as T | import Data.Text.IO.Utf8 (writeFile) | ||||||
| import Data.Time (Day) | import Data.Time (Day) | ||||||
| import Safe (headDef) | import Safe (headDef) | ||||||
| import System.Directory (doesFileExist, getHomeDirectory) | import System.Directory (doesFileExist, getHomeDirectory) | ||||||
| @ -232,7 +232,7 @@ ensureJournalFileExists f = do | |||||||
|     hPutStr stderr $ "Creating hledger journal file " <> show f <> ".\n" |     hPutStr stderr $ "Creating hledger journal file " <> show f <> ".\n" | ||||||
|     -- note Hledger.Utils.UTF8.* do no line ending conversion on windows, |     -- note Hledger.Utils.UTF8.* do no line ending conversion on windows, | ||||||
|     -- we currently require unix line endings on all platforms. |     -- we currently require unix line endings on all platforms. | ||||||
|     newJournalContent >>= T.writeFile f |     newJournalContent >>= writeFile f | ||||||
| 
 | 
 | ||||||
| -- | Does any part of this path contain non-. characters and end with a . ? | -- | Does any part of this path contain non-. characters and end with a . ? | ||||||
| -- Such paths are not safe to use on Windows (cf #1056). | -- Such paths are not safe to use on Windows (cf #1056). | ||||||
| @ -259,7 +259,7 @@ latestDates = headDef [] . take 1 . group . reverse . sort | |||||||
| -- | Remember that these transaction dates were the latest seen when | -- | Remember that these transaction dates were the latest seen when | ||||||
| -- reading this journal file. | -- reading this journal file. | ||||||
| saveLatestDates :: LatestDates -> FilePath -> IO () | saveLatestDates :: LatestDates -> FilePath -> IO () | ||||||
| saveLatestDates dates f = T.writeFile (latestDatesFileFor f) $ T.unlines $ map showDate dates | saveLatestDates dates f = writeFile (latestDatesFileFor f) $ T.unlines $ map showDate dates | ||||||
| 
 | 
 | ||||||
| -- | What were the latest transaction dates seen the last time this | -- | What were the latest transaction dates seen the last time this | ||||||
| -- journal file was read ? If there were multiple transactions on the | -- journal file was read ? If there were multiple transactions on the | ||||||
|  | |||||||
| @ -37,6 +37,7 @@ module Hledger.Read.CsvReader ( | |||||||
| where | where | ||||||
| 
 | 
 | ||||||
| --- ** imports | --- ** imports | ||||||
|  | import Prelude hiding (getContents, writeFile) | ||||||
| import Control.Applicative        (liftA2) | import Control.Applicative        (liftA2) | ||||||
| import Control.Monad              (unless, when) | import Control.Monad              (unless, when) | ||||||
| import Control.Monad.Except       (ExceptT(..), liftEither, throwError) | import Control.Monad.Except       (ExceptT(..), liftEither, throwError) | ||||||
| @ -44,9 +45,14 @@ import qualified Control.Monad.Fail as Fail | |||||||
| import Control.Monad.IO.Class     (MonadIO, liftIO) | import Control.Monad.IO.Class     (MonadIO, liftIO) | ||||||
| import Control.Monad.State.Strict (StateT, get, modify', evalStateT) | import Control.Monad.State.Strict (StateT, get, modify', evalStateT) | ||||||
| import Control.Monad.Trans.Class  (lift) | import Control.Monad.Trans.Class  (lift) | ||||||
| import Data.Char                  (toLower, isDigit, isSpace, isAlphaNum, ord) |  | ||||||
| import Data.Bifunctor             (first) | import Data.Bifunctor             (first) | ||||||
| import Data.Functor               ((<&>)) | import Data.Functor               ((<&>)) | ||||||
|  | import qualified Data.ByteString as B | ||||||
|  | import qualified Data.ByteString.Lazy as BL | ||||||
|  | import Data.Char (toLower, isDigit, isSpace, isAlphaNum, ord) | ||||||
|  | import qualified Data.Csv as Cassava | ||||||
|  | import qualified Data.Csv.Parser.Megaparsec as CassavaMP | ||||||
|  | import Data.Foldable (asum, toList) | ||||||
| import Data.List (elemIndex, foldl', intersperse, mapAccumL, nub, sortBy) | import Data.List (elemIndex, foldl', intersperse, mapAccumL, nub, sortBy) | ||||||
| import Data.Maybe (catMaybes, fromMaybe, isJust) | import Data.Maybe (catMaybes, fromMaybe, isJust) | ||||||
| import Data.MemoUgly (memo) | import Data.MemoUgly (memo) | ||||||
| @ -54,8 +60,9 @@ import Data.Ord (comparing) | |||||||
| import qualified Data.Set as S | import qualified Data.Set as S | ||||||
| import Data.Text (Text) | import Data.Text (Text) | ||||||
| import qualified Data.Text as T | import qualified Data.Text as T | ||||||
| import qualified Data.Text.Encoding as T | import Data.Text.Encoding (decodeUtf8, encodeUtf8) | ||||||
| import qualified Data.Text.IO as T | import Data.Text.IO (getContents)  -- Only putStr and friends are safe | ||||||
|  | import Data.Text.IO.Utf8 (writeFile) | ||||||
| import qualified Data.Text.Lazy as TL | import qualified Data.Text.Lazy as TL | ||||||
| import qualified Data.Text.Lazy.Builder as TB | import qualified Data.Text.Lazy.Builder as TB | ||||||
| import Data.Time.Calendar (Day) | import Data.Time.Calendar (Day) | ||||||
| @ -63,11 +70,6 @@ import Data.Time.Format (parseTimeM, defaultTimeLocale) | |||||||
| import Safe (atMay, headMay, lastMay, readMay) | import Safe (atMay, headMay, lastMay, readMay) | ||||||
| import System.Directory (doesFileExist) | import System.Directory (doesFileExist) | ||||||
| import System.FilePath ((</>), takeDirectory, takeExtension, takeFileName) | import System.FilePath ((</>), takeDirectory, takeExtension, takeFileName) | ||||||
| import qualified Data.Csv as Cassava |  | ||||||
| import qualified Data.Csv.Parser.Megaparsec as CassavaMP |  | ||||||
| import qualified Data.ByteString as B |  | ||||||
| import qualified Data.ByteString.Lazy as BL |  | ||||||
| import Data.Foldable (asum, toList) |  | ||||||
| import Text.Megaparsec hiding (match, parse) | import Text.Megaparsec hiding (match, parse) | ||||||
| import Text.Megaparsec.Char (char, newline, string) | import Text.Megaparsec.Char (char, newline, string) | ||||||
| import Text.Megaparsec.Custom (customErrorBundlePretty, parseErrorAt) | import Text.Megaparsec.Custom (customErrorBundlePretty, parseErrorAt) | ||||||
| @ -197,7 +199,7 @@ expandIncludes dir content = mapM (expandLine dir) (T.lines content) >>= return | |||||||
|   where |   where | ||||||
|     expandLine dir line = |     expandLine dir line = | ||||||
|       case line of |       case line of | ||||||
|         (T.stripPrefix "include " -> Just f) -> expandIncludes dir' =<< T.readFile f' |         (T.stripPrefix "include " -> Just f) -> expandIncludes dir' =<< readFilePortably f' | ||||||
|           where |           where | ||||||
|             f' = dir </> T.unpack (T.dropWhile isSpace f) |             f' = dir </> T.unpack (T.dropWhile isSpace f) | ||||||
|             dir' = takeDirectory f' |             dir' = takeDirectory f' | ||||||
| @ -745,8 +747,8 @@ readJournalFromCsv mrulesfile csvfile csvdata = do | |||||||
|       -- than one date and the first date is more recent than the last): |       -- than one date and the first date is more recent than the last): | ||||||
|       -- reverse them to get same-date transactions ordered chronologically. |       -- reverse them to get same-date transactions ordered chronologically. | ||||||
|       txns' = |       txns' = | ||||||
|         (if newestfirst || mdataseemsnewestfirst == Just True  |         (if newestfirst || mdataseemsnewestfirst == Just True | ||||||
|           then dbg7 "reversed csv txns" . reverse else id)  |           then dbg7 "reversed csv txns" . reverse else id) | ||||||
|           txns |           txns | ||||||
|         where |         where | ||||||
|           newestfirst = dbg6 "newestfirst" $ isJust $ getDirective "newest-first" rules |           newestfirst = dbg6 "newestfirst" $ isJust $ getDirective "newest-first" rules | ||||||
| @ -759,7 +761,7 @@ readJournalFromCsv mrulesfile csvfile csvdata = do | |||||||
| 
 | 
 | ||||||
|     liftIO $ when (not rulesfileexists) $ do |     liftIO $ when (not rulesfileexists) $ do | ||||||
|       dbg1IO "creating conversion rules file" rulesfile |       dbg1IO "creating conversion rules file" rulesfile | ||||||
|       T.writeFile rulesfile rulestext |       writeFile rulesfile rulestext | ||||||
| 
 | 
 | ||||||
|     return nulljournal{jtxns=txns''} |     return nulljournal{jtxns=txns''} | ||||||
| 
 | 
 | ||||||
| @ -774,14 +776,14 @@ parseSeparator = specials . T.toLower | |||||||
| parseCsv :: Char -> FilePath -> Text -> ExceptT String IO CSV | parseCsv :: Char -> FilePath -> Text -> ExceptT String IO CSV | ||||||
| parseCsv separator filePath csvdata = ExceptT $ | parseCsv separator filePath csvdata = ExceptT $ | ||||||
|   case filePath of |   case filePath of | ||||||
|     "-" -> parseCassava separator "(stdin)" <$> T.getContents |     "-" -> parseCassava separator "(stdin)" <$> getContents | ||||||
|     _   -> return $ if T.null csvdata then Right mempty else parseCassava separator filePath csvdata |     _   -> return $ if T.null csvdata then Right mempty else parseCassava separator filePath csvdata | ||||||
| 
 | 
 | ||||||
| parseCassava :: Char -> FilePath -> Text -> Either String CSV | parseCassava :: Char -> FilePath -> Text -> Either String CSV | ||||||
| parseCassava separator path content = | parseCassava separator path content = | ||||||
|   either (Left . errorBundlePretty) (Right . parseResultToCsv) <$> |   either (Left . errorBundlePretty) (Right . parseResultToCsv) <$> | ||||||
|   CassavaMP.decodeWith (decodeOptions separator) Cassava.NoHeader path $ |   CassavaMP.decodeWith (decodeOptions separator) Cassava.NoHeader path $ | ||||||
|   BL.fromStrict $ T.encodeUtf8 content |   BL.fromStrict $ encodeUtf8 content | ||||||
| 
 | 
 | ||||||
| decodeOptions :: Char -> Cassava.DecodeOptions | decodeOptions :: Char -> Cassava.DecodeOptions | ||||||
| decodeOptions separator = Cassava.defaultDecodeOptions { | decodeOptions separator = Cassava.defaultDecodeOptions { | ||||||
| @ -792,7 +794,7 @@ parseResultToCsv :: (Foldable t, Functor t) => t (t B.ByteString) -> CSV | |||||||
| parseResultToCsv = toListList . unpackFields | parseResultToCsv = toListList . unpackFields | ||||||
|     where |     where | ||||||
|         toListList = toList . fmap toList |         toListList = toList . fmap toList | ||||||
|         unpackFields  = (fmap . fmap) T.decodeUtf8 |         unpackFields  = (fmap . fmap) decodeUtf8 | ||||||
| 
 | 
 | ||||||
| printCSV :: CSV -> TL.Text | printCSV :: CSV -> TL.Text | ||||||
| printCSV = TB.toLazyText . unlinesB . map printRecord | printCSV = TB.toLazyText . unlinesB . map printRecord | ||||||
|  | |||||||
| @ -36,7 +36,7 @@ import Data.FileEmbed (makeRelativeToProject, embedStringFile) | |||||||
| import Data.List.Extra (foldl', foldl1', uncons, unsnoc) | import Data.List.Extra (foldl', foldl1', uncons, unsnoc) | ||||||
| import qualified Data.Set as Set | import qualified Data.Set as Set | ||||||
| import Data.Text (Text) | import Data.Text (Text) | ||||||
| import qualified Data.Text.IO as T | import qualified Data.Text.IO as TIO (hGetContents)  -- Only putStr and friends are safe | ||||||
| import qualified Data.Text.Lazy.Builder as TB | import qualified Data.Text.Lazy.Builder as TB | ||||||
| import Data.Time.Clock (getCurrentTime) | import Data.Time.Clock (getCurrentTime) | ||||||
| import Data.Time.LocalTime (LocalTime, ZonedTime, getCurrentTimeZone, | import Data.Time.LocalTime (LocalTime, ZonedTime, getCurrentTimeZone, | ||||||
| @ -48,10 +48,10 @@ import Lens.Micro ((&), (.~)) | |||||||
| import Lens.Micro.TH (DefName(TopName), lensClass, lensField, makeLensesWith, classyRules) | import Lens.Micro.TH (DefName(TopName), lensClass, lensField, makeLensesWith, classyRules) | ||||||
| import System.Console.ANSI (Color,ColorIntensity,ConsoleLayer(..), SGR(..), setSGRCode) | import System.Console.ANSI (Color,ColorIntensity,ConsoleLayer(..), SGR(..), setSGRCode) | ||||||
| import System.Directory (getHomeDirectory) | import System.Directory (getHomeDirectory) | ||||||
| import System.FilePath (isRelative, (</>)) | import System.FilePath ((</>), isRelative) | ||||||
| import System.IO | import System.IO (Handle, IOMode (..), hGetEncoding, hSetEncoding, | ||||||
|   (Handle, IOMode (..), hGetEncoding, hSetEncoding, hSetNewlineMode, |                   hSetNewlineMode, stdin, universalNewlineMode, utf8_bom) | ||||||
|    openFile, stdin, universalNewlineMode, utf8_bom) | import qualified System.IO.Utf8 as Utf8 | ||||||
| 
 | 
 | ||||||
| import Hledger.Utils.Debug | import Hledger.Utils.Debug | ||||||
| import Hledger.Utils.Parse | import Hledger.Utils.Parse | ||||||
| @ -175,7 +175,7 @@ expandHomePath = \case | |||||||
| -- using the system locale's text encoding, | -- using the system locale's text encoding, | ||||||
| -- ignoring any utf8 BOM prefix (as seen in paypal's 2018 CSV, eg) if that encoding is utf8. | -- ignoring any utf8 BOM prefix (as seen in paypal's 2018 CSV, eg) if that encoding is utf8. | ||||||
| readFilePortably :: FilePath -> IO Text | readFilePortably :: FilePath -> IO Text | ||||||
| readFilePortably f =  openFile f ReadMode >>= readHandlePortably | readFilePortably f =  Utf8.openFile f ReadMode >>= readHandlePortably | ||||||
| 
 | 
 | ||||||
| -- | Like readFilePortably, but read from standard input if the path is "-". | -- | Like readFilePortably, but read from standard input if the path is "-". | ||||||
| readFileOrStdinPortably :: String -> IO Text | readFileOrStdinPortably :: String -> IO Text | ||||||
| @ -183,15 +183,14 @@ readFileOrStdinPortably f = openFileOrStdin f ReadMode >>= readHandlePortably | |||||||
|   where |   where | ||||||
|     openFileOrStdin :: String -> IOMode -> IO Handle |     openFileOrStdin :: String -> IOMode -> IO Handle | ||||||
|     openFileOrStdin "-" _ = return stdin |     openFileOrStdin "-" _ = return stdin | ||||||
|     openFileOrStdin f m   = openFile f m |     openFileOrStdin f m   = Utf8.openFile f m | ||||||
| 
 | 
 | ||||||
| readHandlePortably :: Handle -> IO Text | readHandlePortably :: Handle -> IO Text | ||||||
| readHandlePortably h = do | readHandlePortably h = do | ||||||
|   hSetNewlineMode h universalNewlineMode |     hSetNewlineMode h universalNewlineMode | ||||||
|   menc <- hGetEncoding h |     menc <- hGetEncoding h | ||||||
|   when (fmap show menc == Just "UTF-8") $  -- XXX no Eq instance, rely on Show |     when (fmap show menc == Just "UTF-8") $ hSetEncoding h utf8_bom  -- No Eq instance, rely on Show | ||||||
|     hSetEncoding h utf8_bom |     TIO.hGetContents h | ||||||
|   T.hGetContents h |  | ||||||
| 
 | 
 | ||||||
| -- | Total version of maximum, for integral types, giving 0 for an empty list. | -- | Total version of maximum, for integral types, giving 0 for an empty list. | ||||||
| maximum' :: Integral a => [a] -> a | maximum' :: Integral a => [a] -> a | ||||||
|  | |||||||
| @ -1,6 +1,6 @@ | |||||||
| cabal-version: 1.12 | cabal-version: 1.12 | ||||||
| 
 | 
 | ||||||
| -- This file has been generated from package.yaml by hpack version 0.34.4. | -- This file has been generated from package.yaml by hpack version 0.34.7. | ||||||
| -- | -- | ||||||
| -- see: https://github.com/sol/hpack | -- see: https://github.com/sol/hpack | ||||||
| 
 | 
 | ||||||
| @ -137,6 +137,7 @@ library | |||||||
|     , uglymemo |     , uglymemo | ||||||
|     , unordered-containers >=0.2 |     , unordered-containers >=0.2 | ||||||
|     , utf8-string >=0.3.5 |     , utf8-string >=0.3.5 | ||||||
|  |     , with-utf8 >=1.0.0 | ||||||
|   default-language: Haskell2010 |   default-language: Haskell2010 | ||||||
| 
 | 
 | ||||||
| test-suite doctest | test-suite doctest | ||||||
| @ -188,6 +189,7 @@ test-suite doctest | |||||||
|     , uglymemo |     , uglymemo | ||||||
|     , unordered-containers >=0.2 |     , unordered-containers >=0.2 | ||||||
|     , utf8-string >=0.3.5 |     , utf8-string >=0.3.5 | ||||||
|  |     , with-utf8 >=1.0.0 | ||||||
|   if impl(ghc < 9.2) |   if impl(ghc < 9.2) | ||||||
|     buildable: False |     buildable: False | ||||||
|   default-language: Haskell2010 |   default-language: Haskell2010 | ||||||
| @ -241,5 +243,6 @@ test-suite unittest | |||||||
|     , uglymemo |     , uglymemo | ||||||
|     , unordered-containers >=0.2 |     , unordered-containers >=0.2 | ||||||
|     , utf8-string >=0.3.5 |     , utf8-string >=0.3.5 | ||||||
|  |     , with-utf8 >=1.0.0 | ||||||
|   buildable: True |   buildable: True | ||||||
|   default-language: Haskell2010 |   default-language: Haskell2010 | ||||||
|  | |||||||
| @ -48,8 +48,10 @@ dependencies: | |||||||
| - Decimal >=0.5.1 | - Decimal >=0.5.1 | ||||||
| - directory | - directory | ||||||
| - doclayout >=0.3 && <0.4 | - doclayout >=0.3 && <0.4 | ||||||
|  | - extra >=1.6.3 | ||||||
| - file-embed >=0.0.10 | - file-embed >=0.0.10 | ||||||
| - filepath | - filepath | ||||||
|  | - Glob >= 0.9 | ||||||
| - hashtables >=1.2.3.1 | - hashtables >=1.2.3.1 | ||||||
| - megaparsec >=7.0.0 && <9.3 | - megaparsec >=7.0.0 && <9.3 | ||||||
| - microlens >=0.4 | - microlens >=0.4 | ||||||
| @ -70,8 +72,7 @@ dependencies: | |||||||
| - unordered-containers >=0.2 | - unordered-containers >=0.2 | ||||||
| - uglymemo | - uglymemo | ||||||
| - utf8-string >=0.3.5 | - utf8-string >=0.3.5 | ||||||
| - extra >=1.6.3 | - with-utf8 >=1.0.0 | ||||||
| - Glob >= 0.9 |  | ||||||
| # for ledger-parse: | # for ledger-parse: | ||||||
| #- parsers >=0.5 | #- parsers >=0.5 | ||||||
| #- system-filepath | #- system-filepath | ||||||
|  | |||||||
| @ -22,7 +22,7 @@ module Hledger.Cli.Commands.Accounts ( | |||||||
| 
 | 
 | ||||||
| import Data.List | import Data.List | ||||||
| import qualified Data.Text as T | import qualified Data.Text as T | ||||||
| import qualified Data.Text.IO as T | import qualified Data.Text.IO as TIO (putStrLn)  -- Only putStr and friends are safe | ||||||
| import System.Console.CmdArgs.Explicit as C | import System.Console.CmdArgs.Explicit as C | ||||||
| 
 | 
 | ||||||
| import Hledger | import Hledger | ||||||
| @ -96,4 +96,4 @@ accounts CliOpts{rawopts_=rawopts, reportspec_=ReportSpec{_rsQuery=query,_rsRepo | |||||||
|       where |       where | ||||||
|         spacer = T.replicate (maxwidth - T.length (showName a)) " " |         spacer = T.replicate (maxwidth - T.length (showName a)) " " | ||||||
|     maxwidth = maximum $ map (T.length . showName) clippedaccts |     maxwidth = maximum $ map (T.length . showName) clippedaccts | ||||||
|   forM_ clippedaccts $ \a -> T.putStrLn $ showName a <> showType a |   forM_ clippedaccts $ \a -> TIO.putStrLn $ showName a <> showType a | ||||||
|  | |||||||
| @ -26,9 +26,9 @@ import Data.List (isPrefixOf) | |||||||
| import Data.Maybe (fromJust, fromMaybe, isJust) | import Data.Maybe (fromJust, fromMaybe, isJust) | ||||||
| import Data.Text (Text) | import Data.Text (Text) | ||||||
| import qualified Data.Text as T | import qualified Data.Text as T | ||||||
| import qualified Data.Text.IO as T | import qualified Data.Text.IO as TIO (hPutStr, putStr)  -- Only putStr and friends are safe | ||||||
| import qualified Data.Text.Lazy as TL | import qualified Data.Text.Lazy as TL | ||||||
| import qualified Data.Text.Lazy.IO as TL | import qualified Data.Text.Lazy.IO as TLIO (putStrLn)   -- Only putStr and friends are safe | ||||||
| import Data.Time.Calendar (Day) | import Data.Time.Calendar (Day) | ||||||
| import Data.Time.Format (formatTime, defaultTimeLocale) | import Data.Time.Format (formatTime, defaultTimeLocale) | ||||||
| import Lens.Micro ((^.)) | import Lens.Micro ((^.)) | ||||||
| @ -184,7 +184,7 @@ confirmedTransactionWizard prevInput es@EntryState{..} stack@(currentStage : _) | |||||||
|           prevInput' = prevInput{prevDescAndCmnt=Just descAndCommentString} |           prevInput' = prevInput{prevDescAndCmnt=Just descAndCommentString} | ||||||
|       when (isJust mbaset) . liftIO $ do |       when (isJust mbaset) . liftIO $ do | ||||||
|           hPutStrLn stderr "Using this similar transaction for defaults:" |           hPutStrLn stderr "Using this similar transaction for defaults:" | ||||||
|           T.hPutStr stderr $ showTransaction (fromJust mbaset) |           TIO.hPutStr stderr $ showTransaction (fromJust mbaset) | ||||||
|       confirmedTransactionWizard prevInput' es' ((EnterNewPosting TxnParams{txnDate=date, txnCode=code, txnDesc=desc, txnCmnt=comment} Nothing) : stack) |       confirmedTransactionWizard prevInput' es' ((EnterNewPosting TxnParams{txnDate=date, txnCode=code, txnDesc=desc, txnCmnt=comment} Nothing) : stack) | ||||||
|     Nothing -> |     Nothing -> | ||||||
|       confirmedTransactionWizard prevInput es (drop 1 stack) |       confirmedTransactionWizard prevInput es (drop 1 stack) | ||||||
| @ -435,7 +435,7 @@ journalAddTransaction j@Journal{jtxns=ts} opts t = do | |||||||
|     -- unelided shows all amounts explicitly, in case there's a price, cf #283 |     -- unelided shows all amounts explicitly, in case there's a price, cf #283 | ||||||
|   when (debug_ opts > 0) $ do |   when (debug_ opts > 0) $ do | ||||||
|     putStrLn $ printf "\nAdded transaction to %s:" f |     putStrLn $ printf "\nAdded transaction to %s:" f | ||||||
|     TL.putStrLn =<< registerFromString (showTransaction t) |     TLIO.putStrLn =<< registerFromString (showTransaction t) | ||||||
|   return j{jtxns=ts++[t]} |   return j{jtxns=ts++[t]} | ||||||
| 
 | 
 | ||||||
| -- | Append a string, typically one or more transactions, to a journal | -- | Append a string, typically one or more transactions, to a journal | ||||||
| @ -448,7 +448,7 @@ journalAddTransaction j@Journal{jtxns=ts} opts t = do | |||||||
| -- | -- | ||||||
| appendToJournalFileOrStdout :: FilePath -> Text -> IO () | appendToJournalFileOrStdout :: FilePath -> Text -> IO () | ||||||
| appendToJournalFileOrStdout f s | appendToJournalFileOrStdout f s | ||||||
|   | f == "-"  = T.putStr s' |   | f == "-"  = TIO.putStr s' | ||||||
|   | otherwise = appendFile f $ T.unpack s' |   | otherwise = appendFile f $ T.unpack s' | ||||||
|   where s' = "\n" <> ensureOneNewlineTerminated s |   where s' = "\n" <> ensureOneNewlineTerminated s | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -7,7 +7,7 @@ module Hledger.Cli.Commands.Checkdates ( | |||||||
| ) where | ) where | ||||||
| 
 | 
 | ||||||
| import qualified Data.Text as T | import qualified Data.Text as T | ||||||
| import qualified Data.Text.IO as T | import qualified Data.Text.IO as TIO (putStrLn)  -- Only putStr and friends are safe | ||||||
| import Hledger | import Hledger | ||||||
| import Hledger.Cli.CliOptions | import Hledger.Cli.CliOptions | ||||||
| import System.Console.CmdArgs.Explicit | import System.Console.CmdArgs.Explicit | ||||||
| @ -43,7 +43,7 @@ checkdates CliOpts{rawopts_=rawopts,reportspec_=rspec} j = do | |||||||
|         positionstr = T.pack . showGenericSourcePos $ tsourcepos error |         positionstr = T.pack . showGenericSourcePos $ tsourcepos error | ||||||
|         txn1str = linesPrepend  (T.pack "  ")               $ showTransaction previous |         txn1str = linesPrepend  (T.pack "  ")               $ showTransaction previous | ||||||
|         txn2str = linesPrepend2 (T.pack "> ") (T.pack "  ") $ showTransaction error |         txn2str = linesPrepend2 (T.pack "> ") (T.pack "  ") $ showTransaction error | ||||||
|       T.putStrLn $ |       TIO.putStrLn $ | ||||||
|         T.pack "Error: transaction date is out of order" |         T.pack "Error: transaction date is out of order" | ||||||
|         <> uniquestr <> T.pack "\nat " <> positionstr <> T.pack ":\n\n" |         <> uniquestr <> T.pack "\nat " <> positionstr <> T.pack ":\n\n" | ||||||
|         <> txn1str <> txn2str |         <> txn1str <> txn2str | ||||||
|  | |||||||
| @ -12,7 +12,7 @@ import Data.Function (on) | |||||||
| import Data.List (groupBy) | import Data.List (groupBy) | ||||||
| import Data.Maybe (fromMaybe) | import Data.Maybe (fromMaybe) | ||||||
| import qualified Data.Text as T | import qualified Data.Text as T | ||||||
| import qualified Data.Text.IO as T | import qualified Data.Text.IO as TIO (putStr)  -- Only putStr and friends are safe | ||||||
| import Data.Time.Calendar (addDays) | import Data.Time.Calendar (addDays) | ||||||
| import System.Console.CmdArgs.Explicit as C | import System.Console.CmdArgs.Explicit as C | ||||||
| 
 | 
 | ||||||
| @ -169,5 +169,5 @@ close CliOpts{rawopts_=rawopts, reportspec_=rspec'} j = do | |||||||
|       ++ [posting{paccount=openingacct, pamount=if explicit then mixedAmountSetFullPrecision (maNegate totalamt) else missingmixedamt} | not interleaved] |       ++ [posting{paccount=openingacct, pamount=if explicit then mixedAmountSetFullPrecision (maNegate totalamt) else missingmixedamt} | not interleaved] | ||||||
| 
 | 
 | ||||||
|   -- print them |   -- print them | ||||||
|   when closing . T.putStr $ showTransaction closingtxn |   when closing . TIO.putStr $ showTransaction closingtxn | ||||||
|   when opening . T.putStr $ showTransaction openingtxn |   when opening . TIO.putStr $ showTransaction openingtxn | ||||||
|  | |||||||
| @ -16,7 +16,7 @@ module Hledger.Cli.Commands.Codes ( | |||||||
| ) where | ) where | ||||||
| 
 | 
 | ||||||
| import qualified Data.Text as T | import qualified Data.Text as T | ||||||
| import qualified Data.Text.IO as T | import qualified Data.Text.IO as TIO (putStrLn)  -- Only putStr and friends are safe | ||||||
| 
 | 
 | ||||||
| import Hledger | import Hledger | ||||||
| import Hledger.Cli.CliOptions | import Hledger.Cli.CliOptions | ||||||
| @ -36,4 +36,4 @@ codes CliOpts{reportspec_=rspec} j = do | |||||||
|   let ts = entriesReport rspec j |   let ts = entriesReport rspec j | ||||||
|       codes = (if empty_ (_rsReportOpts rspec) then id else filter (not . T.null)) $ |       codes = (if empty_ (_rsReportOpts rspec) then id else filter (not . T.null)) $ | ||||||
|               map tcode ts |               map tcode ts | ||||||
|   mapM_ T.putStrLn codes |   mapM_ TIO.putStrLn codes | ||||||
|  | |||||||
| @ -13,7 +13,7 @@ module Hledger.Cli.Commands.Commodities ( | |||||||
| ) where | ) where | ||||||
| 
 | 
 | ||||||
| import qualified Data.Set as S | import qualified Data.Set as S | ||||||
| import qualified Data.Text.IO as T | import qualified Data.Text.IO as TIO (putStrLn)  -- Only putStr and friends are safe | ||||||
| 
 | 
 | ||||||
| import Hledger | import Hledger | ||||||
| import Hledger.Cli.CliOptions | import Hledger.Cli.CliOptions | ||||||
| @ -30,4 +30,4 @@ commoditiesmode = hledgerCommandMode | |||||||
| commodities :: CliOpts -> Journal -> IO () | commodities :: CliOpts -> Journal -> IO () | ||||||
| commodities _copts = | commodities _copts = | ||||||
|   -- TODO support --declared/--used like accounts, payees |   -- TODO support --declared/--used like accounts, payees | ||||||
|   mapM_ T.putStrLn . S.filter (/= "AUTO") . journalCommodities |   mapM_ TIO.putStrLn . S.filter (/= "AUTO") . journalCommodities | ||||||
|  | |||||||
| @ -15,7 +15,7 @@ module Hledger.Cli.Commands.Descriptions ( | |||||||
| ) where | ) where | ||||||
| 
 | 
 | ||||||
| import Data.List.Extra (nubSort) | import Data.List.Extra (nubSort) | ||||||
| import qualified Data.Text.IO as T | import qualified Data.Text.IO as TIO (putStrLn)  -- Only putStr and friends are safe | ||||||
| 
 | 
 | ||||||
| import Hledger | import Hledger | ||||||
| import Hledger.Cli.CliOptions | import Hledger.Cli.CliOptions | ||||||
| @ -35,4 +35,4 @@ descriptions CliOpts{reportspec_=rspec} j = do | |||||||
|   let ts = entriesReport rspec j |   let ts = entriesReport rspec j | ||||||
|       descriptions = nubSort $ map tdescription ts |       descriptions = nubSort $ map tdescription ts | ||||||
| 
 | 
 | ||||||
|   mapM_ T.putStrLn descriptions |   mapM_ TIO.putStrLn descriptions | ||||||
|  | |||||||
| @ -18,7 +18,7 @@ import Data.Ord (comparing) | |||||||
| import Data.Maybe (fromJust) | import Data.Maybe (fromJust) | ||||||
| import Data.Time (diffDays) | import Data.Time (diffDays) | ||||||
| import Data.Either (partitionEithers) | import Data.Either (partitionEithers) | ||||||
| import qualified Data.Text.IO as T | import qualified Data.Text.IO as TIO (putStr)  -- Only putStr and friends are safe | ||||||
| import Lens.Micro (set) | import Lens.Micro (set) | ||||||
| import System.Exit (exitFailure) | import System.Exit (exitFailure) | ||||||
| 
 | 
 | ||||||
| @ -108,10 +108,10 @@ diff CliOpts{file_=[f1, f2], reportspec_=ReportSpec{_rsQuery=Acct acctRe}} _ = d | |||||||
|   let unmatchedtxn2 = unmatchedtxns R pp2 m |   let unmatchedtxn2 = unmatchedtxns R pp2 m | ||||||
| 
 | 
 | ||||||
|   putStrLn "These transactions are in the first file only:\n" |   putStrLn "These transactions are in the first file only:\n" | ||||||
|   mapM_ (T.putStr . showTransaction) unmatchedtxn1 |   mapM_ (TIO.putStr . showTransaction) unmatchedtxn1 | ||||||
| 
 | 
 | ||||||
|   putStrLn "These transactions are in the second file only:\n" |   putStrLn "These transactions are in the second file only:\n" | ||||||
|   mapM_ (T.putStr . showTransaction) unmatchedtxn2 |   mapM_ (TIO.putStr . showTransaction) unmatchedtxn2 | ||||||
| 
 | 
 | ||||||
| diff _ _ = do | diff _ _ = do | ||||||
|   putStrLn "Please specify two input files. Usage: hledger diff -f FILE1 -f FILE2 FULLACCOUNTNAME" |   putStrLn "Please specify two input files. Usage: hledger diff -f FILE1 -f FILE2 FULLACCOUNTNAME" | ||||||
|  | |||||||
| @ -9,7 +9,7 @@ where | |||||||
| 
 | 
 | ||||||
| import Control.Monad | import Control.Monad | ||||||
| import Data.List | import Data.List | ||||||
| import qualified Data.Text.IO as T | import qualified Data.Text.IO as TIO (putStr)  -- Only putStr and friends are safe | ||||||
| import Hledger | import Hledger | ||||||
| import Hledger.Cli.CliOptions | import Hledger.Cli.CliOptions | ||||||
| import Hledger.Cli.Commands.Add (journalAddTransaction) | import Hledger.Cli.Commands.Add (journalAddTransaction) | ||||||
| @ -60,7 +60,7 @@ importcmd opts@CliOpts{rawopts_=rawopts,inputopts_=iopts} j = do | |||||||
|               printf "; would import %d new transactions from %s:\n\n" (length newts) inputstr |               printf "; would import %d new transactions from %s:\n\n" (length newts) inputstr | ||||||
|               -- TODO how to force output here ? |               -- TODO how to force output here ? | ||||||
|               -- length (jtxns newj) `seq` print' opts{rawopts_=("explicit",""):rawopts} newj |               -- length (jtxns newj) `seq` print' opts{rawopts_=("explicit",""):rawopts} newj | ||||||
|               mapM_ (T.putStr . showTransaction) newts |               mapM_ (TIO.putStr . showTransaction) newts | ||||||
|             newts | catchup -> do |             newts | catchup -> do | ||||||
|               printf "marked %s as caught up, skipping %d unimported transactions\n\n" inputstr (length newts) |               printf "marked %s as caught up, skipping %d unimported transactions\n\n" inputstr (length newts) | ||||||
|             newts -> do |             newts -> do | ||||||
|  | |||||||
| @ -16,7 +16,7 @@ module Hledger.Cli.Commands.Notes ( | |||||||
| ) where | ) where | ||||||
| 
 | 
 | ||||||
| import Data.List.Extra (nubSort) | import Data.List.Extra (nubSort) | ||||||
| import qualified Data.Text.IO as T | import qualified Data.Text.IO as TIO (putStrLn)  -- Only putStr and friends are safe | ||||||
| 
 | 
 | ||||||
| import Hledger | import Hledger | ||||||
| import Hledger.Cli.CliOptions | import Hledger.Cli.CliOptions | ||||||
| @ -35,4 +35,4 @@ notes :: CliOpts -> Journal -> IO () | |||||||
| notes CliOpts{reportspec_=rspec} j = do | notes CliOpts{reportspec_=rspec} j = do | ||||||
|   let ts = entriesReport rspec j |   let ts = entriesReport rspec j | ||||||
|       notes = nubSort $ map transactionNote ts |       notes = nubSort $ map transactionNote ts | ||||||
|   mapM_ T.putStrLn notes |   mapM_ TIO.putStrLn notes | ||||||
|  | |||||||
| @ -15,7 +15,7 @@ module Hledger.Cli.Commands.Payees ( | |||||||
| ) where | ) where | ||||||
| 
 | 
 | ||||||
| import qualified Data.Set as S | import qualified Data.Set as S | ||||||
| import qualified Data.Text.IO as T | import qualified Data.Text.IO as TIO (putStrLn)  -- Only putStr and friends are safe | ||||||
| import System.Console.CmdArgs.Explicit as C | import System.Console.CmdArgs.Explicit as C | ||||||
| 
 | 
 | ||||||
| import Hledger | import Hledger | ||||||
| @ -45,4 +45,4 @@ payees CliOpts{rawopts_=rawopts, reportspec_=ReportSpec{_rsQuery=query}} j = do | |||||||
|       if | declared     && not used -> matcheddeclaredpayees |       if | declared     && not used -> matcheddeclaredpayees | ||||||
|          | not declared && used     -> matchedusedpayees |          | not declared && used     -> matchedusedpayees | ||||||
|          | otherwise                -> matcheddeclaredpayees <> matchedusedpayees |          | otherwise                -> matcheddeclaredpayees <> matchedusedpayees | ||||||
|   mapM_ T.putStrLn payees |   mapM_ TIO.putStrLn payees | ||||||
|  | |||||||
| @ -10,7 +10,7 @@ where | |||||||
| import qualified Data.Map as M | import qualified Data.Map as M | ||||||
| import Data.List | import Data.List | ||||||
| import qualified Data.Text as T | import qualified Data.Text as T | ||||||
| import qualified Data.Text.IO as T | import qualified Data.Text.IO as TIO (putStrLn)  -- Only putStr and friends are safe | ||||||
| import Hledger | import Hledger | ||||||
| import Hledger.Cli.CliOptions | import Hledger.Cli.CliOptions | ||||||
| import System.Console.CmdArgs.Explicit | import System.Console.CmdArgs.Explicit | ||||||
| @ -45,7 +45,7 @@ prices opts j = do | |||||||
|       ++ ifBoolOpt "infer-market-prices" cprices |       ++ ifBoolOpt "infer-market-prices" cprices | ||||||
|       ++ ifBoolOpt "infer-reverse-prices" rcprices  -- TODO: shouldn't this show reversed P prices also ? valuation will use them |       ++ ifBoolOpt "infer-reverse-prices" rcprices  -- TODO: shouldn't this show reversed P prices also ? valuation will use them | ||||||
| 
 | 
 | ||||||
|   mapM_ (T.putStrLn . showPriceDirective) $ |   mapM_ (TIO.putStrLn . showPriceDirective) $ | ||||||
|     sortOn pddate $ |     sortOn pddate $ | ||||||
|     filter (matchesPriceDirective q) $ |     filter (matchesPriceDirective q) $ | ||||||
|     allprices |     allprices | ||||||
|  | |||||||
| @ -18,7 +18,7 @@ where | |||||||
| import Data.Text (Text) | import Data.Text (Text) | ||||||
| import Data.List (intersperse) | import Data.List (intersperse) | ||||||
| import qualified Data.Text as T | import qualified Data.Text as T | ||||||
| import qualified Data.Text.IO as T | import qualified Data.Text.IO as TIO (putStr, putStrLn)  -- Only putStr and friends are safe | ||||||
| import qualified Data.Text.Lazy as TL | import qualified Data.Text.Lazy as TL | ||||||
| import qualified Data.Text.Lazy.Builder as TB | import qualified Data.Text.Lazy.Builder as TB | ||||||
| import Lens.Micro ((^.), _Just, has) | import Lens.Micro ((^.), _Just, has) | ||||||
| @ -206,5 +206,5 @@ postingToCSV p = | |||||||
| printMatch :: CliOpts -> Journal -> Text -> IO () | printMatch :: CliOpts -> Journal -> Text -> IO () | ||||||
| printMatch opts j desc = do | printMatch opts j desc = do | ||||||
|   case journalSimilarTransaction opts j desc of |   case journalSimilarTransaction opts j desc of | ||||||
|     Nothing -> putStrLn "no matches found." |     Nothing -> TIO.putStrLn "no matches found." | ||||||
|     Just t  -> T.putStr $ showTransaction t |     Just t  -> TIO.putStr $ showTransaction t | ||||||
|  | |||||||
| @ -10,7 +10,7 @@ where | |||||||
| import Data.Char (toUpper) | import Data.Char (toUpper) | ||||||
| import Data.List | import Data.List | ||||||
| import qualified Data.Text as T | import qualified Data.Text as T | ||||||
| import qualified Data.Text.Lazy.IO as TL | import qualified Data.Text.Lazy.IO as TLIO (putStr, putStrLn)  -- Only putStr and friends are safe | ||||||
| import Hledger | import Hledger | ||||||
| import Hledger.Cli.CliOptions | import Hledger.Cli.CliOptions | ||||||
| import Hledger.Cli.Commands.Register | import Hledger.Cli.Commands.Register | ||||||
| @ -28,8 +28,8 @@ registermatch opts@CliOpts{rawopts_=rawopts,reportspec_=rspec} j = | |||||||
|     [desc] -> do |     [desc] -> do | ||||||
|         let ps = [p | (_,_,_,p,_) <- postingsReport rspec j] |         let ps = [p | (_,_,_,p,_) <- postingsReport rspec j] | ||||||
|         case similarPosting ps desc of |         case similarPosting ps desc of | ||||||
|           Nothing -> putStrLn "no matches found." |           Nothing -> TLIO.putStrLn "no matches found." | ||||||
|           Just p  -> TL.putStr $ postingsReportAsText opts [pri] |           Just p  -> TLIO.putStr $ postingsReportAsText opts [pri] | ||||||
|                      where pri = (Just (postingDate p) |                      where pri = (Just (postingDate p) | ||||||
|                                  ,Nothing |                                  ,Nothing | ||||||
|                                  ,tdescription <$> ptransaction p |                                  ,tdescription <$> ptransaction p | ||||||
|  | |||||||
| @ -14,7 +14,7 @@ import Data.Functor.Identity | |||||||
| import Data.List (sortOn, foldl') | import Data.List (sortOn, foldl') | ||||||
| import Data.Text (Text) | import Data.Text (Text) | ||||||
| import qualified Data.Text as T | import qualified Data.Text as T | ||||||
| import qualified Data.Text.IO as T | import qualified Data.Text.IO as TIO (putStr)  -- Only putStr and friends are safe | ||||||
| import Hledger | import Hledger | ||||||
| import Hledger.Cli.CliOptions | import Hledger.Cli.CliOptions | ||||||
| import Hledger.Cli.Commands.Print | import Hledger.Cli.Commands.Print | ||||||
| @ -66,7 +66,7 @@ printOrDiff opts | |||||||
| diffOutput :: Journal -> Journal -> IO () | diffOutput :: Journal -> Journal -> IO () | ||||||
| diffOutput j j' = do | diffOutput j j' = do | ||||||
|     let changed = [(originalTransaction t, originalTransaction t') | (t, t') <- zip (jtxns j) (jtxns j'), t /= t'] |     let changed = [(originalTransaction t, originalTransaction t') | (t, t') <- zip (jtxns j) (jtxns j'), t /= t'] | ||||||
|     T.putStr $ renderPatch $ map (uncurry $ diffTxn j) changed |     TIO.putStr $ renderPatch $ map (uncurry $ diffTxn j) changed | ||||||
| 
 | 
 | ||||||
| type Chunk = (SourcePos, [DiffLine Text]) | type Chunk = (SourcePos, [DiffLine Text]) | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -25,7 +25,7 @@ import Data.List | |||||||
| import Numeric.RootFinding | import Numeric.RootFinding | ||||||
| import Data.Decimal | import Data.Decimal | ||||||
| import qualified Data.Text as T | import qualified Data.Text as T | ||||||
| import qualified Data.Text.Lazy.IO as TL | import qualified Data.Text.Lazy.IO as TLIO (putStr, putStrLn)  -- Only putStr and friends are safe | ||||||
| import System.Console.CmdArgs.Explicit as CmdArgs | import System.Console.CmdArgs.Explicit as CmdArgs | ||||||
| 
 | 
 | ||||||
| import Text.Tabular.AsciiWide as Tab | import Text.Tabular.AsciiWide as Tab | ||||||
| @ -85,7 +85,7 @@ roi CliOpts{rawopts_=rawopts, reportspec_=rspec@ReportSpec{_rsReportOpts=ReportO | |||||||
|     trans = dbg3 "investments" $ jtxns filteredj |     trans = dbg3 "investments" $ jtxns filteredj | ||||||
| 
 | 
 | ||||||
|   when (null trans) $ do |   when (null trans) $ do | ||||||
|     putStrLn "No relevant transactions found. Check your investments query" |     TLIO.putStrLn "No relevant transactions found. Check your investments query" | ||||||
|     exitFailure |     exitFailure | ||||||
| 
 | 
 | ||||||
|   let spans = snd $ reportSpan filteredj rspec |   let spans = snd $ reportSpan filteredj rspec | ||||||
| @ -146,7 +146,7 @@ roi CliOpts{rawopts_=rawopts, reportspec_=rspec@ReportSpec{_rsReportOpts=ReportO | |||||||
|                , Tab.Group Tab.SingleLine [Header "IRR", Header "TWR"]]) |                , Tab.Group Tab.SingleLine [Header "IRR", Header "TWR"]]) | ||||||
|               tableBody |               tableBody | ||||||
| 
 | 
 | ||||||
|   TL.putStrLn $ Tab.render prettyTables id id id table |   TLIO.putStrLn $ Tab.render prettyTables id id id table | ||||||
| 
 | 
 | ||||||
| timeWeightedReturn showCashFlow prettyTables investmentsQuery trans mixedAmountValue (OneSpan spanBegin spanEnd valueBeforeAmt valueAfter cashFlow pnl) = do | timeWeightedReturn showCashFlow prettyTables investmentsQuery trans mixedAmountValue (OneSpan spanBegin spanEnd valueBeforeAmt valueAfter cashFlow pnl) = do | ||||||
|   let valueBefore = unMix valueBeforeAmt |   let valueBefore = unMix valueBeforeAmt | ||||||
| @ -229,7 +229,7 @@ timeWeightedReturn showCashFlow prettyTables investmentsQuery trans mixedAmountV | |||||||
|         unitPrices = add initialUnitPrice unitPrices' |         unitPrices = add initialUnitPrice unitPrices' | ||||||
|         unitBalances = add initialUnits unitBalances' |         unitBalances = add initialUnits unitBalances' | ||||||
| 
 | 
 | ||||||
|     TL.putStr $ Tab.render prettyTables id id T.pack |     TLIO.putStr $ Tab.render prettyTables id id T.pack | ||||||
|       (Table |       (Table | ||||||
|        (Tab.Group NoLine (map (Header . showDate) dates)) |        (Tab.Group NoLine (map (Header . showDate) dates)) | ||||||
|        (Tab.Group DoubleLine [ Tab.Group Tab.SingleLine [Tab.Header "Portfolio value", Tab.Header "Unit balance"] |        (Tab.Group DoubleLine [ Tab.Group Tab.SingleLine [Tab.Header "Portfolio value", Tab.Header "Unit balance"] | ||||||
| @ -259,7 +259,7 @@ internalRateOfReturn showCashFlow prettyTables (OneSpan spanBegin spanEnd valueB | |||||||
|   when showCashFlow $ do |   when showCashFlow $ do | ||||||
|     printf "\nIRR cash flow for %s - %s\n" (showDate spanBegin) (showDate (addDays (-1) spanEnd)) |     printf "\nIRR cash flow for %s - %s\n" (showDate spanBegin) (showDate (addDays (-1) spanEnd)) | ||||||
|     let (dates, amounts) = unzip totalCF |     let (dates, amounts) = unzip totalCF | ||||||
|     TL.putStrLn $ Tab.render prettyTables id id id |     TLIO.putStrLn $ Tab.render prettyTables id id id | ||||||
|       (Table |       (Table | ||||||
|        (Tab.Group Tab.NoLine (map (Header . showDate) dates)) |        (Tab.Group Tab.NoLine (map (Header . showDate) dates)) | ||||||
|        (Tab.Group Tab.SingleLine [Header "Amount"]) |        (Tab.Group Tab.SingleLine [Header "Amount"]) | ||||||
|  | |||||||
| @ -10,7 +10,7 @@ where | |||||||
| import qualified Control.Monad.Fail as Fail | import qualified Control.Monad.Fail as Fail | ||||||
| import Data.List.Extra (nubSort) | import Data.List.Extra (nubSort) | ||||||
| import qualified Data.Text as T | import qualified Data.Text as T | ||||||
| import qualified Data.Text.IO as T | import qualified Data.Text.IO as TIO (putStrLn)  -- Only putStr and friends are safe | ||||||
| import Safe | import Safe | ||||||
| import System.Console.CmdArgs.Explicit as C | import System.Console.CmdArgs.Explicit as C | ||||||
| import Hledger | import Hledger | ||||||
| @ -55,4 +55,4 @@ tags CliOpts{rawopts_=rawopts,reportspec_=rspec} j = do | |||||||
|       , let r = if values then v else t |       , let r = if values then v else t | ||||||
|       , not (values && T.null v && not empty) |       , not (values && T.null v && not empty) | ||||||
|       ] |       ] | ||||||
|   mapM_ T.putStrLn tagsorvalues |   mapM_ TIO.putStrLn tagsorvalues | ||||||
|  | |||||||
| @ -41,17 +41,18 @@ etc. | |||||||
| module Hledger.Cli.Main where | module Hledger.Cli.Main where | ||||||
| 
 | 
 | ||||||
| import Data.Char (isDigit) | import Data.Char (isDigit) | ||||||
| import Data.List | import Data.List (isPrefixOf) | ||||||
| import Safe | import Data.Time.Clock.POSIX (getPOSIXTime) | ||||||
|  | import Main.Utf8 (withUtf8) | ||||||
|  | import Safe (headDef, headMay) | ||||||
| import qualified System.Console.CmdArgs.Explicit as C | import qualified System.Console.CmdArgs.Explicit as C | ||||||
| import System.Environment | import System.Environment (getArgs) | ||||||
| import System.Exit | import System.Exit (exitFailure, exitWith) | ||||||
| import System.FilePath | import System.FilePath (dropExtension) | ||||||
| import System.Process | import System.Process (system) | ||||||
| import Text.Printf | import Text.Printf (printf) | ||||||
| 
 | 
 | ||||||
| import Hledger.Cli | import Hledger.Cli | ||||||
| import Data.Time.Clock.POSIX (getPOSIXTime) |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| -- | The overall cmdargs mode describing hledger's command-line options and subcommands. | -- | The overall cmdargs mode describing hledger's command-line options and subcommands. | ||||||
| @ -96,7 +97,7 @@ mainmode addons = defMode { | |||||||
| 
 | 
 | ||||||
| -- | Let's go! | -- | Let's go! | ||||||
| main :: IO () | main :: IO () | ||||||
| main = do | main = withUtf8 $ do | ||||||
|   progstarttime <- getPOSIXTime |   progstarttime <- getPOSIXTime | ||||||
| 
 | 
 | ||||||
|   -- Choose and run the appropriate internal or external command based |   -- Choose and run the appropriate internal or external command based | ||||||
|  | |||||||
| @ -30,16 +30,19 @@ module Hledger.Cli.Utils | |||||||
|     ) |     ) | ||||||
| where | where | ||||||
| 
 | 
 | ||||||
|  | import Prelude hiding (putStr, putStrLn, writeFile) | ||||||
|  | 
 | ||||||
| import Control.Exception as C | import Control.Exception as C | ||||||
| import Control.Monad.Except (ExceptT, liftIO) | import Control.Monad.Except (ExceptT, liftIO) | ||||||
| 
 |  | ||||||
| import Data.List | import Data.List | ||||||
| import Data.Maybe | import Data.Maybe | ||||||
| import qualified Data.Text as T | import qualified Data.Text as T | ||||||
| import qualified Data.Text.IO as T |  | ||||||
| import qualified Data.Text.Lazy as TL | import qualified Data.Text.Lazy as TL | ||||||
| import qualified Data.Text.Lazy.Builder as TB | import qualified Data.Text.Lazy.Builder as TB | ||||||
| import qualified Data.Text.Lazy.IO as TL | import qualified Data.Text.IO as TIO (putStrLn)      -- Only putStr and friends are safe | ||||||
|  | import qualified Data.Text.IO.Utf8 as TIO | ||||||
|  | import qualified Data.Text.Lazy.IO as TLIO (putStr)  -- Only putStr and friends are safe | ||||||
|  | import qualified Data.Text.Lazy.IO.Utf8 as TLIO | ||||||
| import Data.Time (Day) | import Data.Time (Day) | ||||||
| import Data.Time.Clock.POSIX (POSIXTime, utcTimeToPOSIXSeconds) | import Data.Time.Clock.POSIX (POSIXTime, utcTimeToPOSIXSeconds) | ||||||
| import Lens.Micro ((^.)) | import Lens.Micro ((^.)) | ||||||
| @ -111,9 +114,7 @@ anonymiseByOpts opts = | |||||||
| -- | Write some output to stdout or to a file selected by --output-file. | -- | Write some output to stdout or to a file selected by --output-file. | ||||||
| -- If the file exists it will be overwritten. | -- If the file exists it will be overwritten. | ||||||
| writeOutput :: CliOpts -> String -> IO () | writeOutput :: CliOpts -> String -> IO () | ||||||
| writeOutput opts s = do | writeOutput opts = writeOutputLazyText opts . TL.pack | ||||||
|   f <- outputFileFromOpts opts |  | ||||||
|   (maybe putStr writeFile f) s |  | ||||||
| 
 | 
 | ||||||
| -- | Write some output to stdout or to a file selected by --output-file. | -- | Write some output to stdout or to a file selected by --output-file. | ||||||
| -- If the file exists it will be overwritten. This function operates on Lazy | -- If the file exists it will be overwritten. This function operates on Lazy | ||||||
| @ -121,7 +122,7 @@ writeOutput opts s = do | |||||||
| writeOutputLazyText :: CliOpts -> TL.Text -> IO () | writeOutputLazyText :: CliOpts -> TL.Text -> IO () | ||||||
| writeOutputLazyText opts s = do | writeOutputLazyText opts s = do | ||||||
|   f <- outputFileFromOpts opts |   f <- outputFileFromOpts opts | ||||||
|   (maybe TL.putStr TL.writeFile f) s |   (maybe TLIO.putStr TLIO.writeFile f) s | ||||||
| 
 | 
 | ||||||
| -- -- | Get a journal from the given string and options, or throw an error. | -- -- | Get a journal from the given string and options, or throw an error. | ||||||
| -- readJournal :: CliOpts -> String -> IO Journal | -- readJournal :: CliOpts -> String -> IO Journal | ||||||
| @ -189,8 +190,8 @@ openBrowserOn u = trybrowsers browsers u | |||||||
|           ExitSuccess -> return ExitSuccess |           ExitSuccess -> return ExitSuccess | ||||||
|           ExitFailure _ -> trybrowsers bs u |           ExitFailure _ -> trybrowsers bs u | ||||||
|       trybrowsers [] u = do |       trybrowsers [] u = do | ||||||
|         putStrLn $ printf "Could not start a web browser (tried: %s)" $ intercalate ", " browsers |         TIO.putStrLn . T.pack $ "Could not start a web browser (tried: " <> intercalate ", " browsers <> ")" | ||||||
|         putStrLn $ printf "Please open your browser and visit %s" u |         TIO.putStrLn . T.pack $ "Please open your browser and visit " <> u | ||||||
|         return $ ExitFailure 127 |         return $ ExitFailure 127 | ||||||
|       browsers | os=="darwin"  = ["open"] |       browsers | os=="darwin"  = ["open"] | ||||||
|                | os=="mingw32" = ["c:/Program Files/Mozilla Firefox/firefox.exe"] |                | os=="mingw32" = ["c:/Program Files/Mozilla Firefox/firefox.exe"] | ||||||
| @ -217,12 +218,12 @@ writeFileWithBackupIfChanged :: FilePath -> T.Text -> IO Bool | |||||||
| writeFileWithBackupIfChanged f t = do | writeFileWithBackupIfChanged f t = do | ||||||
|   s <- readFilePortably f |   s <- readFilePortably f | ||||||
|   if t == s then return False |   if t == s then return False | ||||||
|             else backUpFile f >> T.writeFile f t >> return True |             else backUpFile f >> TIO.writeFile f t >> return True | ||||||
| 
 | 
 | ||||||
| -- | Back up this file with a (incrementing) numbered suffix, then | -- | Back up this file with a (incrementing) numbered suffix, then | ||||||
| -- overwrite it with this new text, or give an error. | -- overwrite it with this new text, or give an error. | ||||||
| writeFileWithBackup :: FilePath -> String -> IO () | writeFileWithBackup :: FilePath -> String -> IO () | ||||||
| writeFileWithBackup f t = backUpFile f >> writeFile f t | writeFileWithBackup f t = backUpFile f >> TIO.writeFile f (T.pack t) | ||||||
| 
 | 
 | ||||||
| readFileStrictly :: FilePath -> IO T.Text | readFileStrictly :: FilePath -> IO T.Text | ||||||
| readFileStrictly f = readFilePortably f >>= \s -> C.evaluate (T.length s) >> return s | readFileStrictly f = readFilePortably f >>= \s -> C.evaluate (T.length s) >> return s | ||||||
|  | |||||||
| @ -1,6 +1,6 @@ | |||||||
| cabal-version: 1.12 | cabal-version: 1.12 | ||||||
| 
 | 
 | ||||||
| -- This file has been generated from package.yaml by hpack version 0.34.4. | -- This file has been generated from package.yaml by hpack version 0.34.6. | ||||||
| -- | -- | ||||||
| -- see: https://github.com/sol/hpack | -- see: https://github.com/sol/hpack | ||||||
| 
 | 
 | ||||||
| @ -172,6 +172,7 @@ library | |||||||
|     , unordered-containers |     , unordered-containers | ||||||
|     , utf8-string >=0.3.5 |     , utf8-string >=0.3.5 | ||||||
|     , utility-ht >=0.0.13 |     , utility-ht >=0.0.13 | ||||||
|  |     , with-utf8 >=1.0.0 | ||||||
|     , wizards >=1.0 |     , wizards >=1.0 | ||||||
|   if (!(os(windows))) && (flag(terminfo)) |   if (!(os(windows))) && (flag(terminfo)) | ||||||
|     build-depends: |     build-depends: | ||||||
| @ -221,6 +222,7 @@ executable hledger | |||||||
|     , unordered-containers |     , unordered-containers | ||||||
|     , utf8-string >=0.3.5 |     , utf8-string >=0.3.5 | ||||||
|     , utility-ht >=0.0.13 |     , utility-ht >=0.0.13 | ||||||
|  |     , with-utf8 >=1.0.0 | ||||||
|     , wizards >=1.0 |     , wizards >=1.0 | ||||||
|   if (!(os(windows))) && (flag(terminfo)) |   if (!(os(windows))) && (flag(terminfo)) | ||||||
|     build-depends: |     build-depends: | ||||||
| @ -271,6 +273,7 @@ test-suite unittest | |||||||
|     , unordered-containers |     , unordered-containers | ||||||
|     , utf8-string >=0.3.5 |     , utf8-string >=0.3.5 | ||||||
|     , utility-ht >=0.0.13 |     , utility-ht >=0.0.13 | ||||||
|  |     , with-utf8 >=1.0.0 | ||||||
|     , wizards >=1.0 |     , wizards >=1.0 | ||||||
|   if (!(os(windows))) && (flag(terminfo)) |   if (!(os(windows))) && (flag(terminfo)) | ||||||
|     build-depends: |     build-depends: | ||||||
| @ -320,6 +323,7 @@ benchmark bench | |||||||
|     , unordered-containers |     , unordered-containers | ||||||
|     , utf8-string >=0.3.5 |     , utf8-string >=0.3.5 | ||||||
|     , utility-ht >=0.0.13 |     , utility-ht >=0.0.13 | ||||||
|  |     , with-utf8 >=1.0.0 | ||||||
|     , wizards >=1.0 |     , wizards >=1.0 | ||||||
|   buildable: False |   buildable: False | ||||||
|   if (!(os(windows))) && (flag(terminfo)) |   if (!(os(windows))) && (flag(terminfo)) | ||||||
|  | |||||||
| @ -130,6 +130,7 @@ dependencies: | |||||||
| - unordered-containers | - unordered-containers | ||||||
| - utf8-string >=0.3.5 | - utf8-string >=0.3.5 | ||||||
| - utility-ht >=0.0.13 | - utility-ht >=0.0.13 | ||||||
|  | - with-utf8 >=1.0.0 | ||||||
| - wizards >=1.0 | - wizards >=1.0 | ||||||
| 
 | 
 | ||||||
| when: | when: | ||||||
|  | |||||||
| @ -1,11 +1,17 @@ | |||||||
| hledger -f - balance | # 1. Works with unicode input. | ||||||
| <<< | $hledger -f unicode.journal balance | ||||||
| 2009-01-01 проверка |                 10 ß  ß | ||||||
|   τράπεζα  10 руб |          10 проверка  проверка | ||||||
|   नकद |  | ||||||
| >>> |  | ||||||
|               10 руб  τράπεζα |  | ||||||
|              -10 руб  नकद |  | ||||||
| -------------------- | -------------------- | ||||||
|                    0   |                 10 ß | ||||||
| >>>=0 |          10 проверка   | ||||||
|  | >=0 | ||||||
|  | 
 | ||||||
|  | # 2. Handles a byte order mark. | ||||||
|  | $ hledger -f unicode-bom.journal balance | ||||||
|  |                 10 ß  ß | ||||||
|  |          10 проверка  проверка | ||||||
|  | -------------------- | ||||||
|  |                 10 ß | ||||||
|  |          10 проверка   | ||||||
|  | >=0 | ||||||
|  | |||||||
							
								
								
									
										1
									
								
								hledger/test/i18n/unicode-bom.journal
									
									
									
									
									
										Symbolic link
									
								
							
							
						
						
									
										1
									
								
								hledger/test/i18n/unicode-bom.journal
									
									
									
									
									
										Symbolic link
									
								
							| @ -0,0 +1 @@ | |||||||
|  | ../../../examples/unicode-bom.journal | ||||||
							
								
								
									
										1
									
								
								hledger/test/i18n/unicode.journal
									
									
									
									
									
										Symbolic link
									
								
							
							
						
						
									
										1
									
								
								hledger/test/i18n/unicode.journal
									
									
									
									
									
										Symbolic link
									
								
							| @ -0,0 +1 @@ | |||||||
|  | ../../../examples/unicode.journal | ||||||
| @ -15,8 +15,6 @@ packages: | |||||||
| extra-deps: | extra-deps: | ||||||
| # for Shake.hs (regex doesn't support base-compat-0.11): | # for Shake.hs (regex doesn't support base-compat-0.11): | ||||||
| - regex-1.0.2.0@rev:1 | - regex-1.0.2.0@rev:1 | ||||||
| - doclayout-0.3.1.1 |  | ||||||
| - emojis-0.1.2 |  | ||||||
| # for testing base-compat 0.11 compatibility (mutually exclusive with the above): | # for testing base-compat 0.11 compatibility (mutually exclusive with the above): | ||||||
| # - aeson-1.4.6.0 | # - aeson-1.4.6.0 | ||||||
| # - aeson-compat-0.3.9 | # - aeson-compat-0.3.9 | ||||||
| @ -29,6 +27,11 @@ extra-deps: | |||||||
| - prettyprinter-1.7.0 | - prettyprinter-1.7.0 | ||||||
| - prettyprinter-ansi-terminal-1.1.2 | - prettyprinter-ansi-terminal-1.1.2 | ||||||
| - doctest-0.18.1 | - doctest-0.18.1 | ||||||
|  | - doclayout-0.3.1.1 | ||||||
|  | - emojis-0.1.2 | ||||||
|  | - with-utf8-1.0.2.3 | ||||||
|  | - th-compat-0.1.3 | ||||||
|  | - th-env-0.1.0.3 | ||||||
| # for hledger: | # for hledger: | ||||||
| - githash-0.1.4.0 | - githash-0.1.4.0 | ||||||
| # for hledger-ui: | # for hledger-ui: | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user