packages feed

hledger-flow 0.13.2.0 → 0.14.0.0

raw patch · 20 files changed

+826/−566 lines, 20 filesdep +exceptionsdep +pathdep +path-ioPVP ok

version bump matches the API change (PVP)

Dependencies added: exceptions, path, path-io

API changes (from Hackage documentation)

- Hledger.Flow.Common: determineBaseDir :: Maybe FilePath -> IO (FilePath, FilePath)
- Hledger.Flow.Common: determineBaseDir' :: FilePath -> IO (FilePath, FilePath)
- Hledger.Flow.Common: determineBaseDirFromAbsoluteStartDir :: FilePath -> IO (Either Text (FilePath, FilePath))
- Hledger.Flow.Common: determineBaseDirFromAbsoluteStartDir' :: FilePath -> FilePath -> IO (Either Text (FilePath, FilePath))
- Hledger.Flow.Common: dirOrPwd :: Maybe FilePath -> IO FilePath
- Hledger.Flow.Common: docURL :: Line -> Text
- Hledger.Flow.Common: errorMessageBaseDir :: FilePath -> Text
- Hledger.Flow.Common: forceTrailingSlash :: FilePath -> FilePath
- Hledger.Flow.Common: relativeToBase :: HasBaseDir o => o -> FilePath -> FilePath
- Hledger.Flow.Common: relativeToBase' :: FilePath -> FilePath -> FilePath
+ Hledger.Flow.BaseDir: determineBaseDir :: Maybe TurtlePath -> IO (BaseDir, RunDir)
+ Hledger.Flow.BaseDir: determineBaseDir' :: AbsDir -> Maybe TurtlePath -> IO (BaseDir, RunDir)
+ Hledger.Flow.BaseDir: determineBaseDirFromStartDir :: AbsDir -> IO (BaseDir, RunDir)
+ Hledger.Flow.BaseDir: determineBaseDirFromStartDir' :: (MonadIO m, MonadThrow m) => AbsDir -> AbsDir -> m (BaseDir, RunDir)
+ Hledger.Flow.BaseDir: effectiveRunDir :: BaseDir -> RunDir -> Bool -> AbsDir
+ Hledger.Flow.BaseDir: relativeToBase :: HasBaseDir o => o -> TurtlePath -> TurtlePath
+ Hledger.Flow.BaseDir: relativeToBase' :: TurtlePath -> TurtlePath -> TurtlePath
+ Hledger.Flow.BaseDir: turtleBaseDir :: HasBaseDir o => o -> TurtlePath
+ Hledger.Flow.DocHelpers: docURL :: Line -> Text
+ Hledger.Flow.PathHelpers: InvalidTurtleDir :: TurtlePath -> PathException
+ Hledger.Flow.PathHelpers: MissingBaseDir :: AbsDir -> PathException
+ Hledger.Flow.PathHelpers: data PathException
+ Hledger.Flow.PathHelpers: forceTrailingSlash :: TurtlePath -> TurtlePath
+ Hledger.Flow.PathHelpers: fromTurtleAbsDir :: MonadThrow m => TurtlePath -> m AbsDir
+ Hledger.Flow.PathHelpers: fromTurtleAbsFile :: MonadThrow m => TurtlePath -> m AbsFile
+ Hledger.Flow.PathHelpers: fromTurtleRelDir :: MonadThrow m => TurtlePath -> m RelDir
+ Hledger.Flow.PathHelpers: fromTurtleRelFile :: MonadThrow m => TurtlePath -> m RelFile
+ Hledger.Flow.PathHelpers: instance GHC.Classes.Eq Hledger.Flow.PathHelpers.PathException
+ Hledger.Flow.PathHelpers: instance GHC.Exception.Type.Exception Hledger.Flow.PathHelpers.PathException
+ Hledger.Flow.PathHelpers: instance GHC.Show.Show Hledger.Flow.PathHelpers.PathException
+ Hledger.Flow.PathHelpers: pathToTurtle :: Path b t -> TurtlePath
+ Hledger.Flow.PathHelpers: turtleToAbsDir :: (MonadIO m, MonadThrow m) => AbsDir -> TurtlePath -> m AbsDir
+ Hledger.Flow.PathHelpers: type AbsDir = Path Abs Dir
+ Hledger.Flow.PathHelpers: type AbsFile = Path Abs File
+ Hledger.Flow.PathHelpers: type RelDir = Path Rel Dir
+ Hledger.Flow.PathHelpers: type RelFile = Path Rel File
+ Hledger.Flow.PathHelpers: type TurtlePath = FilePath
+ Hledger.Flow.RuntimeOptions: [onlyNewFiles] :: RuntimeOptions -> Bool
+ Hledger.Flow.RuntimeOptions: instance Hledger.Flow.Types.HasRunDir Hledger.Flow.RuntimeOptions.RuntimeOptions
+ Hledger.Flow.Types: class HasRunDir a
+ Hledger.Flow.Types: importRunDir :: HasRunDir a => a -> RunDir
+ Hledger.Flow.Types: type BaseDir = AbsDir
+ Hledger.Flow.Types: type RunDir = RelDir
- Hledger.Flow.Common: addPreamble :: Map FilePath Text -> Map FilePath Text
+ Hledger.Flow.Common: addPreamble :: Map TurtlePath Text -> Map TurtlePath Text
- Hledger.Flow.Common: allYearsFileName :: FilePath
+ Hledger.Flow.Common: allYearsFileName :: TurtlePath
- Hledger.Flow.Common: allYearsPath :: FilePath -> FilePath
+ Hledger.Flow.Common: allYearsPath :: TurtlePath -> TurtlePath
- Hledger.Flow.Common: allYearsPath' :: (FilePath -> FilePath) -> FilePath -> FilePath
+ Hledger.Flow.Common: allYearsPath' :: (TurtlePath -> TurtlePath) -> TurtlePath -> TurtlePath
- Hledger.Flow.Common: basenameLine :: FilePath -> Shell Line
+ Hledger.Flow.Common: basenameLine :: TurtlePath -> Shell Line
- Hledger.Flow.Common: buildFilename :: [Line] -> Text -> FilePath
+ Hledger.Flow.Common: buildFilename :: [Line] -> Text -> TurtlePath
- Hledger.Flow.Common: changeExtension :: Text -> FilePath -> FilePath
+ Hledger.Flow.Common: changeExtension :: Text -> TurtlePath -> TurtlePath
- Hledger.Flow.Common: changeOutputPath :: FilePath -> FilePath -> FilePath
+ Hledger.Flow.Common: changeOutputPath :: TurtlePath -> TurtlePath -> TurtlePath
- Hledger.Flow.Common: changePathAndExtension :: FilePath -> Text -> FilePath -> FilePath
+ Hledger.Flow.Common: changePathAndExtension :: TurtlePath -> Text -> TurtlePath -> TurtlePath
- Hledger.Flow.Common: excludeHiddenFiles :: Shell FilePath -> Shell FilePath
+ Hledger.Flow.Common: excludeHiddenFiles :: Shell TurtlePath -> Shell TurtlePath
- Hledger.Flow.Common: excludeWeirdPaths :: Shell FilePath -> Shell FilePath
+ Hledger.Flow.Common: excludeWeirdPaths :: Shell TurtlePath -> Shell TurtlePath
- Hledger.Flow.Common: extraIncludes :: (HasBaseDir o, HasVerbosity o) => o -> TChan LogMessage -> [FilePath] -> [Text] -> [FilePath] -> [FilePath] -> IO InputFileBundle
+ Hledger.Flow.Common: extraIncludes :: (HasBaseDir o, HasVerbosity o) => o -> TChan LogMessage -> [TurtlePath] -> [Text] -> [TurtlePath] -> [TurtlePath] -> IO InputFileBundle
- Hledger.Flow.Common: extraIncludes' :: (HasBaseDir o, HasVerbosity o) => o -> TChan LogMessage -> InputFileBundle -> [FilePath] -> [Text] -> [FilePath] -> [FilePath] -> IO InputFileBundle
+ Hledger.Flow.Common: extraIncludes' :: (HasBaseDir o, HasVerbosity o) => o -> TChan LogMessage -> InputFileBundle -> [TurtlePath] -> [Text] -> [TurtlePath] -> [TurtlePath] -> IO InputFileBundle
- Hledger.Flow.Common: extraIncludesForFile :: (HasVerbosity o, HasBaseDir o) => o -> TChan LogMessage -> FilePath -> [Text] -> [FilePath] -> [FilePath] -> IO InputFileBundle
+ Hledger.Flow.Common: extraIncludesForFile :: (HasVerbosity o, HasBaseDir o) => o -> TChan LogMessage -> TurtlePath -> [Text] -> [TurtlePath] -> [TurtlePath] -> IO InputFileBundle
- Hledger.Flow.Common: extractImportDirs :: FilePath -> Either Text ImportDirs
+ Hledger.Flow.Common: extractImportDirs :: TurtlePath -> Either Text ImportDirs
- Hledger.Flow.Common: filterPaths :: (FilePath -> IO Bool) -> [FilePath] -> Shell [FilePath]
+ Hledger.Flow.Common: filterPaths :: (TurtlePath -> IO Bool) -> [TurtlePath] -> Shell [TurtlePath]
- Hledger.Flow.Common: filterPaths' :: [FilePath] -> (FilePath -> IO Bool) -> [FilePath] -> Shell [FilePath]
+ Hledger.Flow.Common: filterPaths' :: [TurtlePath] -> (TurtlePath -> IO Bool) -> [TurtlePath] -> Shell [TurtlePath]
- Hledger.Flow.Common: filterPathsByFileStatus :: (FileStatus -> Bool) -> Shell FilePath -> Shell FilePath
+ Hledger.Flow.Common: filterPathsByFileStatus :: (FileStatus -> Bool) -> Shell TurtlePath -> Shell TurtlePath
- Hledger.Flow.Common: filterPathsByFileStatus' :: (FileStatus -> Bool) -> [FilePath] -> [FilePath] -> Shell [FilePath]
+ Hledger.Flow.Common: filterPathsByFileStatus' :: (FileStatus -> Bool) -> [TurtlePath] -> [TurtlePath] -> Shell [TurtlePath]
- Hledger.Flow.Common: firstExistingFile :: [FilePath] -> IO (Maybe FilePath)
+ Hledger.Flow.Common: firstExistingFile :: [TurtlePath] -> IO (Maybe TurtlePath)
- Hledger.Flow.Common: generatedIncludeText :: InputFileBundle -> InputFileBundle -> FilePath -> [FilePath] -> Text
+ Hledger.Flow.Common: generatedIncludeText :: InputFileBundle -> InputFileBundle -> TurtlePath -> [TurtlePath] -> Text
- Hledger.Flow.Common: groupAndWriteIncludeFiles :: (HasBaseDir o, HasVerbosity o) => o -> TChan LogMessage -> [FilePath] -> IO [FilePath]
+ Hledger.Flow.Common: groupAndWriteIncludeFiles :: (HasBaseDir o, HasVerbosity o) => o -> TChan LogMessage -> [TurtlePath] -> IO [TurtlePath]
- Hledger.Flow.Common: groupIncludeFiles :: [FilePath] -> (InputFileBundle, InputFileBundle)
+ Hledger.Flow.Common: groupIncludeFiles :: [TurtlePath] -> (InputFileBundle, InputFileBundle)
- Hledger.Flow.Common: groupIncludeFilesPerYear :: [FilePath] -> InputFileBundle
+ Hledger.Flow.Common: groupIncludeFilesPerYear :: [TurtlePath] -> InputFileBundle
- Hledger.Flow.Common: hledgerInfoFromPath :: Maybe FilePath -> IO HledgerInfo
+ Hledger.Flow.Common: hledgerInfoFromPath :: Maybe TurtlePath -> IO HledgerInfo
- Hledger.Flow.Common: hledgerPathFromOption :: Maybe FilePath -> IO FilePath
+ Hledger.Flow.Common: hledgerPathFromOption :: Maybe TurtlePath -> IO TurtlePath
- Hledger.Flow.Common: hledgerVersionFromPath :: FilePath -> IO Text
+ Hledger.Flow.Common: hledgerVersionFromPath :: TurtlePath -> IO Text
- Hledger.Flow.Common: importDirBreakdown :: FilePath -> [FilePath]
+ Hledger.Flow.Common: importDirBreakdown :: TurtlePath -> [TurtlePath]
- Hledger.Flow.Common: importDirBreakdown' :: [FilePath] -> FilePath -> [FilePath]
+ Hledger.Flow.Common: importDirBreakdown' :: [TurtlePath] -> TurtlePath -> [TurtlePath]
- Hledger.Flow.Common: includeFileName :: FilePath -> FilePath
+ Hledger.Flow.Common: includeFileName :: TurtlePath -> TurtlePath
- Hledger.Flow.Common: includeYears :: TChan LogMessage -> FilePath -> IO [Integer]
+ Hledger.Flow.Common: includeYears :: TChan LogMessage -> TurtlePath -> IO [Integer]
- Hledger.Flow.Common: initialIncludeFilePath :: FilePath -> FilePath
+ Hledger.Flow.Common: initialIncludeFilePath :: TurtlePath -> TurtlePath
- Hledger.Flow.Common: intPath :: Integer -> FilePath
+ Hledger.Flow.Common: intPath :: Integer -> TurtlePath
- Hledger.Flow.Common: listOwners :: HasBaseDir o => o -> Shell FilePath
+ Hledger.Flow.Common: listOwners :: HasBaseDir o => o -> Shell TurtlePath
- Hledger.Flow.Common: lsDirs :: FilePath -> Shell FilePath
+ Hledger.Flow.Common: lsDirs :: TurtlePath -> Shell TurtlePath
- Hledger.Flow.Common: onlyDirs :: Shell FilePath -> Shell FilePath
+ Hledger.Flow.Common: onlyDirs :: Shell TurtlePath -> Shell TurtlePath
- Hledger.Flow.Common: onlyFiles :: Shell FilePath -> Shell FilePath
+ Hledger.Flow.Common: onlyFiles :: Shell TurtlePath -> Shell TurtlePath
- Hledger.Flow.Common: parentIncludeFilePath :: FilePath -> FilePath
+ Hledger.Flow.Common: parentIncludeFilePath :: TurtlePath -> TurtlePath
- Hledger.Flow.Common: relativeFilesAsText :: HasBaseDir o => o -> [FilePath] -> [Text]
+ Hledger.Flow.Common: relativeFilesAsText :: HasBaseDir o => o -> [TurtlePath] -> [Text]
- Hledger.Flow.Common: toIncludeFiles :: (HasBaseDir o, HasVerbosity o) => o -> TChan LogMessage -> InputFileBundle -> IO (Map FilePath Text)
+ Hledger.Flow.Common: toIncludeFiles :: (HasBaseDir o, HasVerbosity o) => o -> TChan LogMessage -> InputFileBundle -> IO (Map TurtlePath Text)
- Hledger.Flow.Common: toIncludeFiles' :: InputFileBundle -> InputFileBundle -> InputFileBundle -> Map FilePath Text
+ Hledger.Flow.Common: toIncludeFiles' :: InputFileBundle -> InputFileBundle -> InputFileBundle -> Map TurtlePath Text
- Hledger.Flow.Common: toIncludeLine :: FilePath -> FilePath -> Text
+ Hledger.Flow.Common: toIncludeLine :: TurtlePath -> TurtlePath -> Text
- Hledger.Flow.Common: type InputFileBundle = Map FilePath [FilePath]
+ Hledger.Flow.Common: type InputFileBundle = Map TurtlePath [TurtlePath]
- Hledger.Flow.Common: verboseTestFile :: (HasVerbosity o, HasBaseDir o) => o -> TChan LogMessage -> FilePath -> IO Bool
+ Hledger.Flow.Common: verboseTestFile :: (HasVerbosity o, HasBaseDir o) => o -> TChan LogMessage -> TurtlePath -> IO Bool
- Hledger.Flow.Common: writeFileMap :: (HasBaseDir o, HasVerbosity o) => o -> TChan LogMessage -> (InputFileBundle, InputFileBundle) -> IO [FilePath]
+ Hledger.Flow.Common: writeFileMap :: (HasBaseDir o, HasVerbosity o) => o -> TChan LogMessage -> (InputFileBundle, InputFileBundle) -> IO [TurtlePath]
- Hledger.Flow.Common: writeFiles :: IO (Map FilePath Text) -> IO [FilePath]
+ Hledger.Flow.Common: writeFiles :: IO (Map TurtlePath Text) -> IO [TurtlePath]
- Hledger.Flow.Common: writeFiles' :: Map FilePath Text -> IO [FilePath]
+ Hledger.Flow.Common: writeFiles' :: Map TurtlePath Text -> IO [TurtlePath]
- Hledger.Flow.Common: writeIncludesUpTo :: (HasBaseDir o, HasVerbosity o) => o -> TChan LogMessage -> FilePath -> [FilePath] -> IO [FilePath]
+ Hledger.Flow.Common: writeIncludesUpTo :: (HasBaseDir o, HasVerbosity o) => o -> TChan LogMessage -> TurtlePath -> [TurtlePath] -> IO [TurtlePath]
- Hledger.Flow.Common: writeTextMap :: Map FilePath Text -> IO ()
+ Hledger.Flow.Common: writeTextMap :: Map TurtlePath Text -> IO ()
- Hledger.Flow.Common: writeToplevelAllYearsInclude :: (HasBaseDir o, HasVerbosity o) => o -> IO [FilePath]
+ Hledger.Flow.Common: writeToplevelAllYearsInclude :: (HasBaseDir o, HasVerbosity o) => o -> IO [TurtlePath]
- Hledger.Flow.Common: yearsIncludeMap :: [FilePath] -> InputFileBundle
+ Hledger.Flow.Common: yearsIncludeMap :: [TurtlePath] -> InputFileBundle
- Hledger.Flow.Import.Types: ImportDirs :: FilePath -> FilePath -> FilePath -> FilePath -> FilePath -> FilePath -> ImportDirs
+ Hledger.Flow.Import.Types: ImportDirs :: TurtlePath -> TurtlePath -> TurtlePath -> TurtlePath -> TurtlePath -> TurtlePath -> ImportDirs
- Hledger.Flow.Import.Types: [accountDir] :: ImportDirs -> FilePath
+ Hledger.Flow.Import.Types: [accountDir] :: ImportDirs -> TurtlePath
- Hledger.Flow.Import.Types: [bankDir] :: ImportDirs -> FilePath
+ Hledger.Flow.Import.Types: [bankDir] :: ImportDirs -> TurtlePath
- Hledger.Flow.Import.Types: [importDir] :: ImportDirs -> FilePath
+ Hledger.Flow.Import.Types: [importDir] :: ImportDirs -> TurtlePath
- Hledger.Flow.Import.Types: [ownerDir] :: ImportDirs -> FilePath
+ Hledger.Flow.Import.Types: [ownerDir] :: ImportDirs -> TurtlePath
- Hledger.Flow.Import.Types: [stateDir] :: ImportDirs -> FilePath
+ Hledger.Flow.Import.Types: [stateDir] :: ImportDirs -> TurtlePath
- Hledger.Flow.Import.Types: [yearDir] :: ImportDirs -> FilePath
+ Hledger.Flow.Import.Types: [yearDir] :: ImportDirs -> TurtlePath
- Hledger.Flow.RuntimeOptions: RuntimeOptions :: FilePath -> FilePath -> Bool -> Text -> HledgerInfo -> SystemInfo -> Bool -> Bool -> Bool -> RuntimeOptions
+ Hledger.Flow.RuntimeOptions: RuntimeOptions :: BaseDir -> RunDir -> Bool -> Bool -> Text -> HledgerInfo -> SystemInfo -> Bool -> Bool -> Bool -> RuntimeOptions
- Hledger.Flow.RuntimeOptions: [baseDir] :: RuntimeOptions -> FilePath
+ Hledger.Flow.RuntimeOptions: [baseDir] :: RuntimeOptions -> BaseDir
- Hledger.Flow.RuntimeOptions: [importRunDir] :: RuntimeOptions -> FilePath
+ Hledger.Flow.RuntimeOptions: [importRunDir] :: RuntimeOptions -> RunDir
- Hledger.Flow.Types: HledgerInfo :: FilePath -> Text -> HledgerInfo
+ Hledger.Flow.Types: HledgerInfo :: TurtlePath -> Text -> HledgerInfo
- Hledger.Flow.Types: [hlPath] :: HledgerInfo -> FilePath
+ Hledger.Flow.Types: [hlPath] :: HledgerInfo -> TurtlePath
- Hledger.Flow.Types: baseDir :: HasBaseDir a => a -> FilePath
+ Hledger.Flow.Types: baseDir :: HasBaseDir a => a -> BaseDir

Files

ChangeLog.md view
@@ -1,5 +1,30 @@ # Changelog for [hledger-flow](https://github.com/apauley/hledger-flow) +## 0.14.0++- Add a new performance-related command-line option to import: `--new-files-only`. [PR #89](https://github.com/apauley/hledger-flow/pull/89)++  Don't regenerate transaction files if they are+  already present. This applies to hledger journal+  files as well as files produced by the preprocess and+  construct scripts.++- Generate monthly versions of the income statement in reports. A contribution by [Max Linke](https://github.com/apauley/hledger-flow/pull/88)++- Switch some usages of system-filepath over to [path](https://github.com/apauley/hledger-flow/pull/87)++  hledger-flow started as a collection of bash scripts that I translated into Haskell with the help of [Turtle](https://hackage.haskell.org/package/turtle).++  Turtle uses the now deprecated [system-filepath](https://hackage.haskell.org/package/system-filepath) to represent all paths.++  I've had many filepath-related issues in hledger-flow.+  They were related to issues such as that 2 instances of the same directory would not be treated as equal, because one could have a trailing slash and the other not.+  Another issue that popped up was knowing wether a path is a file or a directory, and if it is absolute or relative.++  All of these issues are articulated in the `path` library:+  https://github.com/commercialhaskell/path++ ## 0.13.2  Improve support for importing a subset of journals: start importing only from the directory given as argument,
README.org view
@@ -72,7 +72,7 @@ =hledger-flow= is intended for you if:  - You are interested in getting started with-  [[http://hledger.org/][hledger]] and you wouldn't mind pointers to the+  [[http://hledger.org/][hledger]] or [[https://www.ledger-cli.org/][ledger-cli]] and you wouldn't mind pointers to the   right docs along the way. - You want a way to organise your finances into a structure that will be   maintainable over the long term.@@ -85,17 +85,35 @@ - You appreciate the fact that all your financial information stays   within your control. +* About This Documentation++The rest of this file documents how to use =hledger-flow=, and it has probably outgrown what should be in a README file.++If you can spare some time to contribute to this project, please consider converting these docs to something more suitable,+such as [[https://github.com/apauley/hledger-flow/issues/47][Read the Docs]].+ * How do I install it?   :PROPERTIES:   :CUSTOM_ID: how-do-i-install-it   :END:  The easiest way to get it running is to download [[https://github.com/apauley/hledger-flow/releases][the latest release]]-for your OS, and copy the =hledger-flow= executable to a directory in your PATH.+for your OS (Linux or Mac OS X), and copy the =hledger-flow= executable to a directory in your PATH. Then just run it and see what it tells you to do.  You can also compile it yourself by following the [[https://github.com/apauley/hledger-flow/blob/master/CONTRIBUTING.org#build-the-project][build instructions]]. +** Windows Support++Currently =hledger-flow= does not work on Windows.++This [[https://github.com/apauley/hledger-flow/issues?q=is%3Aissue+is%3Aopen+label%3Awindows][list of issues]] describes some of the details of what doesn't work.++I believe it wouldn't take too much effort to fix those issues, but I'm going to leave Windows support+for other contributors.++Please send me some pull requests if you would like =hledger-flow= to work on Windows.+ * How Stable is it?   :PROPERTIES:   :CUSTOM_ID: how-stable-is-it@@ -104,6 +122,10 @@ We're not close to a 1.0 release yet, which means that we can still make changes if needed. +As an example, the command-line switches we use will probably change over time.+Some switches change the behaviour of the program - the default behaviour will probably change between releases.+The names of these command-line options can change, or they can be removed when it is no longer needed.+ That being said, some parts have been used and tested extensively and are likely to remain stable. Have a look at the "Stability of this Feature" sections in the feature reference below.@@ -267,17 +289,17 @@ A checking account at mybank:  #+BEGIN_EXAMPLE-    # Saved as: import/john/mybank/checking/mybank-checking.rules-    include ../../../mybank-shared.rules-    account1 Assets:Current:John:MyBank:Checking+# Saved as: import/john/mybank/checking/mybank-checking.rules+include ../../../mybank-shared.rules+account1 Assets:Current:John:MyBank:Checking #+END_EXAMPLE  Another account at mybank:  #+BEGIN_EXAMPLE-    # Saved as: import/alice/mybank/savings/mybank-savings.rules-    include ../../../mybank-shared.rules-    account1 Assets:Current:Alice:MyBank:Savings+# Saved as: import/alice/mybank/savings/mybank-savings.rules+include ../../../mybank-shared.rules+account1 Assets:Current:Alice:MyBank:Savings #+END_EXAMPLE  Where =import/mybank-shared.rules= may define some shared attributes:@@ -299,7 +321,7 @@ with the fields defined more or less like this:  #+BEGIN_EXAMPLE-    fields date, description, amount, balance, account1, account2+fields date, description, amount, balance, account1, account2 #+END_EXAMPLE  *** Stability of this Feature@@ -341,11 +363,21 @@ An opening balance may look something like this:  #+BEGIN_EXAMPLE-    2018-06-01 Savings Account Opening Balance+2018-06-01 Savings Account Opening Balance     assets:Current:MyBank:Savings               $102.01     equity:Opening Balances:MyBank:Savings #+END_EXAMPLE ++*** A Note of Caution Regarding Closing Balances++When closing your balances it may result in some =hledger= queries showing zero-values, or there could be issues with balance assertions.++Please have a look at the upstream =hledger= documentation on closing balances, e.g here:+https://hledger.org/hledger.html#close-usage++Some of the gotchas you may run into are also described in [[https://github.com/apauley/hledger-flow/issues/79][this hledger-flow issue]].+ *** Closing Balances     :PROPERTIES:     :CUSTOM_ID: closing-balances@@ -361,7 +393,7 @@ A closing balance may look something like this:  #+BEGIN_EXAMPLE-    2018-06-01 Savings Account Closing Balance+2018-06-01 Savings Account Closing Balance     assets:Current:MyBank:Savings               $-234.56 = $0.00     equity:Closing Balances:MyBank:Savings #+END_EXAMPLE@@ -402,10 +434,14 @@     :CUSTOM_ID: stability-of-this-feature-2     :END: -The opening balances file works well in my opinion, I don't expect it to-change. I'm only using closing balances in one or two places, so maybe-that could do with some suggestions from people who use this more than-myself.+Closing balances sometimes result in [[https://github.com/apauley/hledger-flow/issues/79][unexpected query results]].+In future we may change how/where the generated files include the closing journal.++We may also need to suggest some naming conventions for opening and closing balances so that reports can exclude+some of these transactions.++It is also possible that we might want to change the name/location of the closing journal,+but we'll try to avoid this if possible, because that would require users to rename their existing files.  ** Price Files    :PROPERTIES:
app/Main.hs view
@@ -1,26 +1,32 @@ {-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}  module Main where -import Turtle hiding (switch)-import Prelude hiding (FilePath, putStrLn)+import Path+import qualified Turtle as Turtle hiding (switch)+import Prelude hiding (putStrLn)  import Options.Applicative +import Hledger.Flow.PathHelpers (TurtlePath) import Hledger.Flow.Common+import Hledger.Flow.BaseDir import qualified Hledger.Flow.RuntimeOptions as RT import Hledger.Flow.Reports import Hledger.Flow.CSVImport -data ImportParams = ImportParams { maybeImportBaseDir :: Maybe FilePath-                                 , importUseRunDir :: Bool } deriving (Show)+data ImportParams = ImportParams { maybeImportBaseDir :: Maybe TurtlePath+                                 , importUseRunDir :: Bool+                                 , onlyNewFiles :: Bool+                                 } deriving (Show) -data ReportParams = ReportParams { maybeReportBaseDir :: Maybe FilePath } deriving (Show)+data ReportParams = ReportParams { maybeReportBaseDir :: Maybe TurtlePath } deriving (Show)  data Command = Import ImportParams | Report ReportParams deriving (Show)  data MainParams = MainParams { verbosity :: Int-                             , hledgerPathOpt :: Maybe FilePath+                             , hledgerPathOpt :: Maybe TurtlePath                              , showOpts :: Bool                              , sequential :: Bool                              } deriving (Show)@@ -28,19 +34,21 @@  main :: IO () main = do-  cmd <- options "An hledger workflow focusing on automated statement import and classification:\nhttps://github.com/apauley/hledger-flow#readme" baseCommandParser+  cmd <- Turtle.options "An hledger workflow focusing on automated statement import and classification:\nhttps://github.com/apauley/hledger-flow#readme" baseCommandParser   case cmd of-    Version                                -> stdout $ select versionInfo+    Version                                -> Turtle.stdout $ Turtle.select versionInfo     Command mainParams' (Import subParams) -> toRuntimeOptionsImport mainParams' subParams >>= importCSVs     Command mainParams' (Report subParams) -> toRuntimeOptionsReport mainParams' subParams >>= generateReports  toRuntimeOptionsImport :: MainParams -> ImportParams -> IO RT.RuntimeOptions toRuntimeOptionsImport mainParams' subParams' = do-  (bd, runDir) <- determineBaseDir $ maybeImportBaseDir subParams'+  let maybeBD = maybeImportBaseDir subParams' :: Maybe TurtlePath+  (bd, runDir) <- determineBaseDir maybeBD   hli <- hledgerInfoFromPath $ hledgerPathOpt mainParams'   return RT.RuntimeOptions { RT.baseDir = bd                            , RT.importRunDir = runDir                            , RT.useRunDir = importUseRunDir subParams'+                           , RT.onlyNewFiles = onlyNewFiles subParams'                            , RT.hfVersion = versionInfo'                            , RT.hledgerInfo = hli                            , RT.sysInfo = systemInfo@@ -50,11 +58,13 @@  toRuntimeOptionsReport :: MainParams -> ReportParams -> IO RT.RuntimeOptions toRuntimeOptionsReport mainParams' subParams' = do-  (bd, _) <- determineBaseDir $ maybeReportBaseDir subParams'+  let maybeBD = maybeReportBaseDir subParams' :: Maybe TurtlePath+  (bd, _) <- determineBaseDir maybeBD   hli <- hledgerInfoFromPath $ hledgerPathOpt mainParams'   return RT.RuntimeOptions { RT.baseDir = bd-                           , RT.importRunDir = "./"+                           , RT.importRunDir = [reldir|.|]                            , RT.useRunDir = False+                           , RT.onlyNewFiles = False                            , RT.hfVersion = versionInfo'                            , RT.hledgerInfo = hli                            , RT.sysInfo = systemInfo@@ -67,21 +77,22 @@   <|> flag' Version (long "version" <> short 'V' <> help "Display version information")  commandParser :: Parser Command-commandParser = fmap Import (subcommand "import" "Uses hledger with your own rules and/or scripts to convert electronic statements into categorised journal files" subcommandParserImport)-  <|> fmap Report (subcommand "report" "Generate Reports" subcommandParserReport)+commandParser = fmap Import (Turtle.subcommand "import" "Uses hledger with your own rules and/or scripts to convert electronic statements into categorised journal files" subcommandParserImport)+  <|> fmap Report (Turtle.subcommand "report" "Generate Reports" subcommandParserReport)  verboseParser :: Parser MainParams verboseParser = MainParams   <$> (length <$> many (flag' () (long "verbose" <> short 'v' <> help "Print more verbose output")))-  <*> optional (optPath "hledger-path" 'H' "The full path to an hledger executable")+  <*> optional (Turtle.optPath "hledger-path" 'H' "The full path to an hledger executable")   <*> switch (long "show-options" <> help "Print the options this program will run with")   <*> switch (long "sequential" <> help "Disable parallel processing")  subcommandParserImport :: Parser ImportParams subcommandParserImport = ImportParams-  <$> optional (argPath "dir" "The directory to import. Use the base directory for a full import or a sub-directory for a partial import. Defaults to the current directory. This behaviour is changing: see --enable-future-rundir")+  <$> optional (Turtle.argPath "dir" "The directory to import. Use the base directory for a full import or a sub-directory for a partial import. Defaults to the current directory. This behaviour is changing: see --enable-future-rundir")   <*> switch (long "enable-future-rundir" <> help "Enable the future (0.14.x) default behaviour now: start importing only from the directory that was given as an argument, or the currect directory. Previously a full import was always done. This switch will be removed in 0.14.x")+  <*> switch (long "new-files-only" <> help "Don't regenerate transaction files if they are already present. This applies to hledger journal files as well as files produced by the preprocess and construct scripts.")  subcommandParserReport :: Parser ReportParams subcommandParserReport = ReportParams-  <$> optional (argPath "basedir" "The hledger-flow base directory")+  <$> optional (Turtle.argPath "basedir" "The hledger-flow base directory")
hledger-flow.cabal view
@@ -1,13 +1,13 @@ cabal-version: 1.12 --- This file has been generated from package.yaml by hpack version 0.31.2.+-- This file has been generated from package.yaml by hpack version 0.33.0. -- -- see: https://github.com/sol/hpack ----- hash: 735099a4aed6ae3be8f511959f660740e93a66852c0a4aeb4b1d3b29e7a951c8+-- hash: 7b720ae822cc4e9d6e1e1b3e96ab162130e20c432bfd91e151bd2355f271ac54  name:           hledger-flow-version:        0.13.2.0+version:        0.14.0.0 synopsis:       An hledger workflow focusing on automated statement import and classification. description:    Please see the README on GitHub at <https://github.com/apauley/hledger-flow#readme> category:       Finance, Console@@ -29,9 +29,12 @@  library   exposed-modules:+      Hledger.Flow.BaseDir       Hledger.Flow.Common       Hledger.Flow.CSVImport+      Hledger.Flow.DocHelpers       Hledger.Flow.Import.Types+      Hledger.Flow.PathHelpers       Hledger.Flow.Reports       Hledger.Flow.RuntimeOptions       Hledger.Flow.Types@@ -43,7 +46,10 @@   build-depends:       base >=4.7 && <5     , containers+    , exceptions     , foldl+    , path+    , path-io     , stm     , text     , time@@ -62,6 +68,7 @@       base >=4.7 && <5     , hledger-flow     , optparse-applicative+    , path     , text     , turtle   default-language: Haskell2010@@ -70,6 +77,7 @@   type: exitcode-stdio-1.0   main-is: Spec.hs   other-modules:+      BaseDir.Integration       Common.Integration       Common.Unit       CSVImport.Integration@@ -85,6 +93,8 @@     , containers     , foldl     , hledger-flow+    , path+    , path-io     , stm     , text     , turtle
+ src/Hledger/Flow/BaseDir.hs view
@@ -0,0 +1,59 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}++module Hledger.Flow.BaseDir where++import Path+import Path.IO+import Hledger.Flow.Types (HasBaseDir, BaseDir, RunDir, baseDir)+import Hledger.Flow.PathHelpers++import Data.Maybe++import Control.Monad.Catch (MonadThrow, throwM)+import Control.Monad.IO.Class (MonadIO)+++import qualified Turtle as Turtle (stripPrefix)++determineBaseDir :: Maybe TurtlePath -> IO (BaseDir, RunDir)+determineBaseDir suppliedDir = do+  pwd <- getCurrentDir+  determineBaseDir' pwd suppliedDir++determineBaseDir' :: AbsDir -> Maybe TurtlePath -> IO (BaseDir, RunDir)+determineBaseDir' pwd (Just suppliedDir) = do+  absDir <- turtleToAbsDir pwd suppliedDir+  determineBaseDirFromStartDir absDir+determineBaseDir' pwd Nothing = determineBaseDirFromStartDir pwd++determineBaseDirFromStartDir ::  AbsDir -> IO (BaseDir, RunDir)+determineBaseDirFromStartDir startDir = determineBaseDirFromStartDir' startDir startDir++determineBaseDirFromStartDir' :: (MonadIO m, MonadThrow m) => AbsDir -> AbsDir -> m (BaseDir, RunDir)+determineBaseDirFromStartDir' startDir possibleBaseDir = do+  _ <- if (parent possibleBaseDir == possibleBaseDir) then throwM (MissingBaseDir startDir) else return ()+  foundBaseDir <- doesDirExist $ possibleBaseDir </> [reldir|import|]+  if foundBaseDir then+    do+      runDir <- makeRelative possibleBaseDir startDir+      return (possibleBaseDir, runDir)+    else determineBaseDirFromStartDir' startDir $ parent possibleBaseDir++relativeToBase :: HasBaseDir o => o -> TurtlePath -> TurtlePath+relativeToBase opts = relativeToBase' $ pathToTurtle (baseDir opts)++relativeToBase' :: TurtlePath -> TurtlePath -> TurtlePath+relativeToBase' bd p = if forceTrailingSlash bd == forceTrailingSlash p then "./" else+  fromMaybe p $ Turtle.stripPrefix (forceTrailingSlash bd) p++turtleBaseDir :: HasBaseDir o => o -> TurtlePath+turtleBaseDir opts = pathToTurtle $ baseDir opts++effectiveRunDir :: BaseDir -> RunDir -> Bool -> AbsDir+effectiveRunDir bd rd useRunDir = do+  let baseImportDir = bd </> [Path.reldir|import|]+  let absRunDir = bd </> rd+  if useRunDir+    then if absRunDir == bd then baseImportDir else absRunDir+    else baseImportDir
src/Hledger/Flow/CSVImport.hs view
@@ -4,109 +4,146 @@     ( importCSVs     ) where -import Turtle hiding (stdout, stderr, proc, procStrictWithErr)-import Prelude hiding (FilePath, putStrLn, take)+import qualified Turtle as Turtle hiding (stdout, stderr, proc, procStrictWithErr)+import Turtle ((%), (</>), (<.>))+import Prelude hiding (putStrLn, take) import qualified Data.Text as T import qualified Data.List.NonEmpty as NonEmpty import qualified Hledger.Flow.Types as FlowTypes import Hledger.Flow.Import.Types+import Hledger.Flow.BaseDir (relativeToBase, effectiveRunDir)+import Hledger.Flow.PathHelpers (TurtlePath, pathToTurtle)+import Hledger.Flow.DocHelpers (docURL) import Hledger.Flow.Common import Hledger.Flow.RuntimeOptions import Control.Concurrent.STM+import Control.Monad +type FileWasGenerated = Bool+ importCSVs :: RuntimeOptions -> IO ()-importCSVs opts = sh (+importCSVs opts = Turtle.sh (   do-    ch <- liftIO newTChanIO-    logHandle <- fork $ consoleChannelLoop ch-    liftIO $ if (showOptions opts) then channelOutLn ch (repr opts) else return ()-    liftIO $ logVerbose opts ch "Starting import"-    (journals, diff) <- time $ liftIO $ importCSVs' opts ch-    liftIO $ channelOutLn ch $ format ("Imported "%d%" journals in "%s) (length journals) $ repr diff-    liftIO $ terminateChannelLoop ch-    wait logHandle+    ch <- Turtle.liftIO newTChanIO+    logHandle <- Turtle.fork $ consoleChannelLoop ch+    Turtle.liftIO $ if (showOptions opts) then channelOutLn ch (Turtle.repr opts) else return ()+    Turtle.liftIO $ logVerbose opts ch "Starting import"+    (journals, diff) <- Turtle.time $ Turtle.liftIO $ importCSVs' opts ch+    let generatedJournals = filter snd journals+    Turtle.liftIO $ channelOutLn ch $ Turtle.format ("Imported "%Turtle.d%"/"%Turtle.d%" journals in "%Turtle.s) (length generatedJournals) (length journals) $ Turtle.repr diff+    Turtle.liftIO $ terminateChannelLoop ch+    Turtle.wait logHandle   )  pathSeparators :: [Char] pathSeparators = ['/', '\\', ':'] -inputFilePattern :: Pattern Text-inputFilePattern = contains (once (oneOf pathSeparators) <> asciiCI "1-in" <> once (oneOf pathSeparators) <> plus digit <> once (oneOf pathSeparators))+inputFilePattern :: Turtle.Pattern T.Text+inputFilePattern = Turtle.contains (Turtle.once (Turtle.oneOf pathSeparators) <> Turtle.asciiCI "1-in" <> Turtle.once (Turtle.oneOf pathSeparators) <> Turtle.plus Turtle.digit <> Turtle.once (Turtle.oneOf pathSeparators)) -importCSVs' :: RuntimeOptions -> TChan FlowTypes.LogMessage -> IO [FilePath]+importCSVs' :: RuntimeOptions -> TChan FlowTypes.LogMessage -> IO [(TurtlePath, FileWasGenerated)] importCSVs' opts ch = do-  let baseImportDir = forceTrailingSlash $ (baseDir opts) </> "import"-  let runDir = forceTrailingSlash $ collapse $ (baseDir opts) </> (importRunDir opts)-  let effectiveDir = if useRunDir opts-        then if (forceTrailingSlash $ runDir </> "import") == baseImportDir then baseImportDir else runDir-        else baseImportDir-  channelOutLn ch $ format ("Collecting input files from "%fp) effectiveDir-  (inputFiles, diff) <- time $ single . shellToList . onlyFiles $ find inputFilePattern effectiveDir+  let effectiveDir = effectiveRunDir (baseDir opts) (importRunDir opts) (useRunDir opts)+  channelOutLn ch $ Turtle.format ("Collecting input files from "%Turtle.fp) $ pathToTurtle effectiveDir+  (inputFiles, diff) <- Turtle.time $ Turtle.single . shellToList . onlyFiles $ Turtle.find inputFilePattern (pathToTurtle effectiveDir)   let fileCount = length inputFiles   if (fileCount == 0) then     do-      let msg = format ("I couldn't find any input files underneath "%fp+      let msg = Turtle.format ("I couldn't find any input files underneath "%Turtle.fp                         %"\n\nhledger-flow expects to find its input files in specifically\nnamed directories.\n\n"%-                        "Have a look at the documentation for a detailed explanation:\n"%s) effectiveDir (docURL "input-files")+                        "Have a look at the documentation for a detailed explanation:\n"%Turtle.s) (pathToTurtle effectiveDir) (docURL "input-files")       errExit 1 ch msg []     else     do-      channelOutLn ch $ format ("Found "%d%" input files in "%s%". Proceeding with import...") fileCount (repr diff)-      let actions = map (extractAndImport opts ch) inputFiles :: [IO FilePath]+      channelOutLn ch $ Turtle.format ("Found "%Turtle.d%" input files in "%Turtle.s%". Proceeding with import...") fileCount (Turtle.repr diff)+      let actions = map (extractAndImport opts ch) inputFiles :: [IO (TurtlePath, FileWasGenerated)]       importedJournals <- parAwareActions opts actions-      _ <- writeIncludesUpTo opts ch effectiveDir importedJournals+      _ <- writeIncludesUpTo opts ch (pathToTurtle effectiveDir) $ fmap fst importedJournals       _ <- writeToplevelAllYearsInclude opts       return importedJournals -extractAndImport :: RuntimeOptions -> TChan FlowTypes.LogMessage -> FilePath -> IO FilePath+extractAndImport :: RuntimeOptions -> TChan FlowTypes.LogMessage -> TurtlePath -> IO (TurtlePath, FileWasGenerated) extractAndImport opts ch inputFile = do   case extractImportDirs inputFile of     Right importDirs -> importCSV opts ch importDirs inputFile     Left errorMessage -> do-      errExit 1 ch errorMessage inputFile+      errExit 1 ch errorMessage (inputFile, False) -importCSV :: RuntimeOptions -> TChan FlowTypes.LogMessage -> ImportDirs -> FilePath -> IO FilePath+importCSV :: RuntimeOptions -> TChan FlowTypes.LogMessage -> ImportDirs -> TurtlePath -> IO (TurtlePath, FileWasGenerated) importCSV opts ch importDirs srcFile = do   let preprocessScript = accountDir importDirs </> "preprocess"   let constructScript = accountDir importDirs </> "construct"   let bankName = importDirLine bankDir importDirs   let accountName = importDirLine accountDir importDirs   let ownerName = importDirLine ownerDir importDirs-  csvFile <- preprocessIfNeeded opts ch preprocessScript bankName accountName ownerName srcFile-  doCustomConstruct <- verboseTestFile opts ch constructScript-  let importFun = if doCustomConstruct-        then customConstruct opts ch constructScript bankName accountName ownerName-        else hledgerImport opts ch+  (csvFile, preprocessHappened) <- preprocessIfNeeded opts ch preprocessScript bankName accountName ownerName srcFile   let journalOut = changePathAndExtension "3-journal" "journal" csvFile-  mktree $ directory journalOut-  importFun csvFile journalOut+  shouldImport <- if onlyNewFiles opts && not preprocessHappened+    then not <$> verboseTestFile opts ch journalOut+    else return True -preprocessIfNeeded :: RuntimeOptions -> TChan FlowTypes.LogMessage -> FilePath -> Line -> Line -> Line -> FilePath -> IO FilePath-preprocessIfNeeded opts ch script bank account owner src = do-  shouldPreprocess <- verboseTestFile opts ch script-  if shouldPreprocess-    then preprocess opts ch script bank account owner src-    else return src+  importFun <- if shouldImport+    then constructOrImport opts ch constructScript bankName accountName ownerName+    else do+      _ <- logNewFileSkip opts ch "import" journalOut+      return $ \_p1 _p2 -> return journalOut+  Turtle.mktree $ Turtle.directory journalOut+  out <- importFun csvFile journalOut+  return (out, shouldImport) -preprocess :: RuntimeOptions -> TChan FlowTypes.LogMessage -> FilePath -> Line -> Line -> Line -> FilePath -> IO FilePath-preprocess opts ch script bank account owner src = do+constructOrImport :: RuntimeOptions -> TChan FlowTypes.LogMessage -> TurtlePath -> Turtle.Line -> Turtle.Line -> Turtle.Line -> IO (TurtlePath -> TurtlePath -> IO TurtlePath)+constructOrImport opts ch constructScript bankName accountName ownerName = do+  constructScriptExists <- verboseTestFile opts ch constructScript+  if constructScriptExists+    then return $ customConstruct opts ch constructScript bankName accountName ownerName+    else return $ hledgerImport opts ch++preprocessIfNeeded :: RuntimeOptions -> TChan FlowTypes.LogMessage -> TurtlePath -> Turtle.Line -> Turtle.Line -> Turtle.Line -> TurtlePath -> IO (TurtlePath, Bool)+preprocessIfNeeded opts ch script bank account owner src = do   let csvOut = changePathAndExtension "2-preprocessed" "csv" src-  mktree $ directory csvOut-  let args = [format fp src, format fp csvOut, lineToText bank, lineToText account, lineToText owner]+  scriptExists <- verboseTestFile opts ch script+  shouldProceed <- if onlyNewFiles opts +    then do+      targetExists <- verboseTestFile opts ch csvOut+      return $ scriptExists && not targetExists+    else return scriptExists+  if shouldProceed+    then do+     out <- preprocess opts ch script bank account owner src csvOut+     return (out, True)+    else do+      _ <- logNewFileSkip opts ch "preprocess" csvOut+      return (src, False)++logNewFileSkip :: RuntimeOptions -> TChan FlowTypes.LogMessage -> T.Text -> TurtlePath -> IO ()+logNewFileSkip opts ch logIdentifier absTarget =+  Control.Monad.when (onlyNewFiles opts) $ do+   let relativeTarget = relativeToBase opts absTarget+   logVerbose opts ch+     $ Turtle.format+        ("Skipping " % Turtle.s+         % " - only creating new files and this output file already exists: '"+         % Turtle.fp+         % "'") logIdentifier relativeTarget++preprocess :: RuntimeOptions -> TChan FlowTypes.LogMessage -> TurtlePath -> Turtle.Line -> Turtle.Line -> Turtle.Line -> TurtlePath -> TurtlePath -> IO TurtlePath+preprocess opts ch script bank account owner src csvOut = do+  Turtle.mktree $ Turtle.directory csvOut+  let args = [Turtle.format Turtle.fp src, Turtle.format Turtle.fp csvOut, Turtle.lineToText bank, Turtle.lineToText account, Turtle.lineToText owner]   let relScript = relativeToBase opts script   let relSrc = relativeToBase opts src-  let cmdLabel = format ("executing '"%fp%"' on '"%fp%"'") relScript relSrc-  _ <- timeAndExitOnErr opts ch cmdLabel channelOut channelErr (parAwareProc opts) (format fp script, args, empty)+  let cmdLabel = Turtle.format ("executing '"%Turtle.fp%"' on '"%Turtle.fp%"'") relScript relSrc+  _ <- timeAndExitOnErr opts ch cmdLabel channelOut channelErr (parAwareProc opts) (Turtle.format Turtle.fp script, args, Turtle.empty)   return csvOut -hledgerImport :: RuntimeOptions -> TChan FlowTypes.LogMessage -> FilePath -> FilePath -> IO FilePath+hledgerImport :: RuntimeOptions -> TChan FlowTypes.LogMessage -> TurtlePath -> TurtlePath -> IO TurtlePath hledgerImport opts ch csvSrc journalOut = do   case extractImportDirs csvSrc of     Right importDirs -> hledgerImport' opts ch importDirs csvSrc journalOut     Left errorMessage -> do       errExit 1 ch errorMessage csvSrc -hledgerImport' :: RuntimeOptions -> TChan FlowTypes.LogMessage -> ImportDirs -> FilePath -> FilePath -> IO FilePath+hledgerImport' :: RuntimeOptions -> TChan FlowTypes.LogMessage -> ImportDirs -> TurtlePath -> TurtlePath -> IO TurtlePath hledgerImport' opts ch importDirs csvSrc journalOut = do   let candidates = rulesFileCandidates csvSrc importDirs   maybeRulesFile <- firstExistingFile candidates@@ -114,31 +151,31 @@   case maybeRulesFile of     Just rf -> do       let relRules = relativeToBase opts rf-      let hledger = format fp $ FlowTypes.hlPath . hledgerInfo $ opts :: Text-      let args = ["print", "--rules-file", format fp rf, "--file", format fp csvSrc, "--output-file", format fp journalOut]-      let cmdLabel = format ("importing '"%fp%"' using rules file '"%fp%"'") relCSV relRules-      _ <- timeAndExitOnErr opts ch cmdLabel channelOut channelErr (parAwareProc opts) (hledger, args, empty)+      let hledger = Turtle.format Turtle.fp $ FlowTypes.hlPath . hledgerInfo $ opts :: T.Text+      let args = ["print", "--rules-file", Turtle.format Turtle.fp rf, "--file", Turtle.format Turtle.fp csvSrc, "--output-file", Turtle.format Turtle.fp journalOut]+      let cmdLabel = Turtle.format ("importing '"%Turtle.fp%"' using rules file '"%Turtle.fp%"'") relCSV relRules+      _ <- timeAndExitOnErr opts ch cmdLabel channelOut channelErr (parAwareProc opts) (hledger, args, Turtle.empty)       return journalOut     Nothing ->       do         let relativeCandidates = map (relativeToBase opts) candidates-        let candidatesTxt = T.intercalate "\n" $ map (format fp) relativeCandidates-        let msg = format ("I couldn't find an hledger rules file while trying to import\n"%fp-                          %"\n\nI will happily use the first rules file I can find from any one of these "%d%" files:\n"%s-                          %"\n\nHere is a bit of documentation about rules files that you may find helpful:\n"%s)+        let candidatesTxt = T.intercalate "\n" $ map (Turtle.format Turtle.fp) relativeCandidates+        let msg = Turtle.format ("I couldn't find an hledger rules file while trying to import\n"%Turtle.fp+                          %"\n\nI will happily use the first rules file I can find from any one of these "%Turtle.d%" files:\n"%Turtle.s+                          %"\n\nHere is a bit of documentation about rules files that you may find helpful:\n"%Turtle.s)                   relCSV (length candidates) candidatesTxt (docURL "rules-files")         errExit 1 ch msg csvSrc -rulesFileCandidates :: FilePath -> ImportDirs -> [FilePath]+rulesFileCandidates :: TurtlePath -> ImportDirs -> [TurtlePath] rulesFileCandidates csvSrc importDirs = statementSpecificRulesFiles csvSrc importDirs ++ generalRulesFiles importDirs -importDirLines :: (ImportDirs -> FilePath) -> ImportDirs -> [Line]-importDirLines dirFun importDirs = NonEmpty.toList $ textToLines $ format fp $ dirname $ dirFun importDirs+importDirLines :: (ImportDirs -> TurtlePath) -> ImportDirs -> [Turtle.Line]+importDirLines dirFun importDirs = NonEmpty.toList $ Turtle.textToLines $ Turtle.format Turtle.fp $ Turtle.dirname $ dirFun importDirs -importDirLine :: (ImportDirs -> FilePath) -> ImportDirs -> Line+importDirLine :: (ImportDirs -> TurtlePath) -> ImportDirs -> Turtle.Line importDirLine dirFun importDirs = foldl (<>) "" $ importDirLines dirFun importDirs -generalRulesFiles :: ImportDirs -> [FilePath]+generalRulesFiles :: ImportDirs -> [TurtlePath] generalRulesFiles importDirs = do   let bank = importDirLines bankDir importDirs   let account = importDirLines accountDir importDirs@@ -147,27 +184,27 @@   let bankRulesFile = importDir importDirs </> buildFilename bank "rules"   [accountRulesFile, bankRulesFile] -statementSpecificRulesFiles :: FilePath -> ImportDirs -> [FilePath]+statementSpecificRulesFiles :: TurtlePath -> ImportDirs -> [TurtlePath] statementSpecificRulesFiles csvSrc importDirs = do-  let srcSuffix = snd $ T.breakOnEnd "_" (format fp (basename csvSrc))+  let srcSuffix = snd $ T.breakOnEnd "_" (Turtle.format Turtle.fp (Turtle.basename csvSrc))    if ((T.take 3 srcSuffix) == "rfo")     then     do-      let srcSpecificFilename = fromText srcSuffix <.> "rules"+      let srcSpecificFilename = Turtle.fromText srcSuffix <.> "rules"       map (</> srcSpecificFilename) [accountDir importDirs, bankDir importDirs, importDir importDirs]     else [] -customConstruct :: RuntimeOptions -> TChan FlowTypes.LogMessage -> FilePath -> Line -> Line -> Line -> FilePath -> FilePath -> IO FilePath+customConstruct :: RuntimeOptions -> TChan FlowTypes.LogMessage -> TurtlePath -> Turtle.Line -> Turtle.Line -> Turtle.Line -> TurtlePath -> TurtlePath -> IO TurtlePath customConstruct opts ch constructScript bank account owner csvSrc journalOut = do-  let script = format fp constructScript :: Text+  let script = Turtle.format Turtle.fp constructScript :: T.Text   let relScript = relativeToBase opts constructScript-  let constructArgs = [format fp csvSrc, "-", lineToText bank, lineToText account, lineToText owner]-  let constructCmdText = format ("Running: "%fp%" "%s) relScript (showCmdArgs constructArgs)-  let stdLines = inprocWithErrFun (channelErrLn ch) (script, constructArgs, empty)-  let hledger = format fp $ FlowTypes.hlPath . hledgerInfo $ opts :: Text-  let args = ["print", "--ignore-assertions", "--file", "-", "--output-file", format fp journalOut]+  let constructArgs = [Turtle.format Turtle.fp csvSrc, "-", Turtle.lineToText bank, Turtle.lineToText account, Turtle.lineToText owner]+  let constructCmdText = Turtle.format ("Running: "%Turtle.fp%" "%Turtle.s) relScript (showCmdArgs constructArgs)+  let stdLines = inprocWithErrFun (channelErrLn ch) (script, constructArgs, Turtle.empty)+  let hledger = Turtle.format Turtle.fp $ FlowTypes.hlPath . hledgerInfo $ opts :: T.Text+  let args = ["print", "--ignore-assertions", "--file", "-", "--output-file", Turtle.format Turtle.fp journalOut]   let relSrc = relativeToBase opts csvSrc-  let cmdLabel = format ("executing '"%fp%"' on '"%fp%"'") relScript relSrc+  let cmdLabel = Turtle.format ("executing '"%Turtle.fp%"' on '"%Turtle.fp%"'") relScript relSrc   _ <- timeAndExitOnErr' opts ch cmdLabel [constructCmdText] channelOut channelErr (parAwareProc opts) (hledger, args, stdLines)   return journalOut
src/Hledger/Flow/Common.hs view
@@ -2,8 +2,10 @@  module Hledger.Flow.Common where -import Turtle-import Prelude hiding (FilePath, putStrLn)+import qualified Turtle as Turtle+import Turtle ((%), (</>), (<.>))++import Prelude hiding (putStrLn) import qualified Data.Text as T import qualified Data.Text.IO as T import qualified Data.Text.Read as T@@ -20,8 +22,14 @@ import Data.Function (on) import qualified Data.List as List (nub, null, sort, sortBy, groupBy) import Data.Ord (comparing)+ import Hledger.Flow.Types import qualified Hledger.Flow.Import.Types as IT++import Hledger.Flow.PathHelpers (TurtlePath)+import Hledger.Flow.BaseDir (turtleBaseDir, relativeToBase, relativeToBase')+import Hledger.Flow.DocHelpers (docURL)+ import Control.Concurrent.STM  import qualified Data.List.NonEmpty as NE@@ -29,12 +37,12 @@ import qualified Data.Version as Version (showVersion) import qualified System.Info as Sys -type InputFileBundle = Map.Map FilePath [FilePath]+type InputFileBundle = Map.Map TurtlePath [TurtlePath] -versionInfo :: NE.NonEmpty Line-versionInfo = textToLines versionInfo'+versionInfo :: NE.NonEmpty Turtle.Line+versionInfo = Turtle.textToLines versionInfo' -versionInfo' :: Text+versionInfo' :: T.Text versionInfo' = T.pack ("hledger-flow " ++ Version.showVersion version ++ " " ++                        os systemInfo ++ " " ++ arch systemInfo ++ " " ++                        compilerName systemInfo ++ " " ++@@ -47,16 +55,16 @@                         , compilerVersion = Sys.compilerVersion                         } -hledgerPathFromOption :: Maybe FilePath -> IO FilePath+hledgerPathFromOption :: Maybe TurtlePath -> IO TurtlePath hledgerPathFromOption pathOption = do   case pathOption of     Just h  -> do-      isOnDisk <- testfile h+      isOnDisk <- Turtle.testfile h       if isOnDisk then return h else do-        let msg = format ("Unable to find hledger at "%fp) h+        let msg = Turtle.format ("Unable to find hledger at "%Turtle.fp) h         errExit' 1 (T.hPutStrLn H.stderr) msg h     Nothing -> do-      maybeH <- which "hledger"+      maybeH <- Turtle.which "hledger"       case maybeH of         Just h  -> return h         Nothing -> do@@ -65,52 +73,52 @@                 <> "There are a number of installation options on the hledger website: https://hledger.org/download.html"           errExit' 1 (T.hPutStrLn H.stderr) msg "/" -hledgerVersionFromPath :: FilePath -> IO Text-hledgerVersionFromPath hlp = fmap (T.strip . linesToText) (single $ shellToList $ inproc (format fp hlp) ["--version"] empty)+hledgerVersionFromPath :: TurtlePath -> IO T.Text+hledgerVersionFromPath hlp = fmap (T.strip . Turtle.linesToText) (Turtle.single $ shellToList $ Turtle.inproc (Turtle.format Turtle.fp hlp) ["--version"] Turtle.empty) -hledgerInfoFromPath :: Maybe FilePath -> IO HledgerInfo+hledgerInfoFromPath :: Maybe TurtlePath -> IO HledgerInfo hledgerInfoFromPath pathOption = do   hlp <- hledgerPathFromOption pathOption   hlv <- hledgerVersionFromPath hlp   return $ HledgerInfo hlp hlv -showCmdArgs :: [Text] -> Text+showCmdArgs :: [T.Text] -> T.Text showCmdArgs args = T.intercalate " " (map escapeArg args) -escapeArg :: Text -> Text+escapeArg :: T.Text -> T.Text escapeArg a = if (T.count " " a > 0) then "'" <> a <> "'" else a -dummyLogger :: TChan LogMessage -> Text -> IO ()+dummyLogger :: TChan LogMessage -> T.Text -> IO () dummyLogger _ _ = return () -channelOut :: TChan LogMessage -> Text -> IO ()+channelOut :: TChan LogMessage -> T.Text -> IO () channelOut ch txt = atomically $ writeTChan ch $ StdOut txt -channelOutLn :: TChan LogMessage -> Text -> IO ()+channelOutLn :: TChan LogMessage -> T.Text -> IO () channelOutLn ch txt = channelOut ch (txt <> "\n") -channelErr :: TChan LogMessage -> Text -> IO ()+channelErr :: TChan LogMessage -> T.Text -> IO () channelErr ch txt = atomically $ writeTChan ch $ StdErr txt -channelErrLn :: TChan LogMessage -> Text -> IO ()+channelErrLn :: TChan LogMessage -> T.Text -> IO () channelErrLn ch txt = channelErr ch (txt <> "\n") -errExit :: Int -> TChan LogMessage -> Text -> a -> IO a+errExit :: Int -> TChan LogMessage -> T.Text -> a -> IO a errExit exitStatus ch = errExit' exitStatus (channelErrLn ch) -errExit' :: Int -> (Text -> IO ()) -> Text -> a -> IO a+errExit' :: Int -> (T.Text -> IO ()) -> T.Text -> a -> IO a errExit' exitStatus logFun errorMessage dummyReturnValue = do   logFun errorMessage-  sleep 0.1-  _ <- exit $ ExitFailure exitStatus+  Turtle.sleep 0.1+  _ <- Turtle.exit $ Turtle.ExitFailure exitStatus   return dummyReturnValue -timestampPrefix :: Text -> IO Text+timestampPrefix :: T.Text -> IO T.Text timestampPrefix txt = do   t <- getZonedTime-  return $ format (s%"\thledger-flow "%s) (repr t) txt+  return $ Turtle.format (Turtle.s%"\thledger-flow "%Turtle.s) (Turtle.repr t) txt -logToChannel :: TChan LogMessage -> Text -> IO ()+logToChannel :: TChan LogMessage -> T.Text -> IO () logToChannel ch msg = do   ts <- timestampPrefix msg   channelErrLn ch ts@@ -130,90 +138,83 @@ terminateChannelLoop :: TChan LogMessage -> IO () terminateChannelLoop ch = atomically $ writeTChan ch Terminate -logVerbose :: HasVerbosity o => o -> TChan LogMessage -> Text -> IO ()+logVerbose :: HasVerbosity o => o -> TChan LogMessage -> T.Text -> IO () logVerbose opts ch msg = if (verbose opts) then logToChannel ch msg else return () -descriptiveOutput :: Text -> Text -> Text+descriptiveOutput :: T.Text -> T.Text -> T.Text descriptiveOutput outputLabel outTxt = do   if not (T.null outTxt)-    then format (s%":\n"%s%"\n") outputLabel outTxt+    then Turtle.format (Turtle.s%":\n"%Turtle.s%"\n") outputLabel outTxt     else "" -logTimedAction :: HasVerbosity o => o -> TChan LogMessage -> Text -> [Text]-  -> (TChan LogMessage -> Text -> IO ()) -> (TChan LogMessage -> Text -> IO ())+logTimedAction :: HasVerbosity o => o -> TChan LogMessage -> T.Text -> [T.Text]+  -> (TChan LogMessage -> T.Text -> IO ()) -> (TChan LogMessage -> T.Text -> IO ())   -> IO FullOutput   -> IO FullTimedOutput logTimedAction opts ch cmdLabel extraCmdLabels stdoutLogger stderrLogger action = do-  logVerbose opts ch $ format ("Begin: "%s) cmdLabel+  logVerbose opts ch $ Turtle.format ("Begin: "%Turtle.s) cmdLabel   if (List.null extraCmdLabels) then return () else logVerbose opts ch $ T.intercalate "\n" extraCmdLabels-  timed@((ec, stdOut, stdErr), diff) <- time action+  timed@((ec, stdOut, stdErr), diff) <- Turtle.time action   stdoutLogger ch stdOut   stderrLogger ch stdErr-  logVerbose opts ch $ format ("End:   "%s%" "%s%" ("%s%")") cmdLabel (repr ec) (repr diff)+  logVerbose opts ch $ Turtle.format ("End:   "%Turtle.s%" "%Turtle.s%" ("%Turtle.s%")") cmdLabel (Turtle.repr ec) (Turtle.repr diff)   return timed -timeAndExitOnErr :: (HasSequential o, HasVerbosity o) => o -> TChan LogMessage -> Text-  -> (TChan LogMessage -> Text -> IO ()) -> (TChan LogMessage -> Text -> IO ())+timeAndExitOnErr :: (HasSequential o, HasVerbosity o) => o -> TChan LogMessage -> T.Text+  -> (TChan LogMessage -> T.Text -> IO ()) -> (TChan LogMessage -> T.Text -> IO ())   -> ProcFun -> ProcInput   -> IO FullTimedOutput timeAndExitOnErr opts ch cmdLabel = timeAndExitOnErr' opts ch cmdLabel [] -timeAndExitOnErr' :: (HasSequential o, HasVerbosity o) => o -> TChan LogMessage -> Text -> [Text]-  -> (TChan LogMessage -> Text -> IO ()) -> (TChan LogMessage -> Text -> IO ())+timeAndExitOnErr' :: (HasSequential o, HasVerbosity o) => o -> TChan LogMessage -> T.Text -> [T.Text]+  -> (TChan LogMessage -> T.Text -> IO ()) -> (TChan LogMessage -> T.Text -> IO ())   -> ProcFun -> ProcInput   -> IO FullTimedOutput timeAndExitOnErr' opts ch cmdLabel extraCmdLabels stdoutLogger stderrLogger procFun (cmd, args, stdInput) = do   let action = procFun cmd args stdInput   timed@((ec, stdOut, stdErr), _) <- logTimedAction opts ch cmdLabel extraCmdLabels stdoutLogger stderrLogger action   case ec of-    ExitFailure i -> do-      let cmdText = format (s%" "%s) cmd $ showCmdArgs args+    Turtle.ExitFailure i -> do+      let cmdText = Turtle.format (Turtle.s%" "%Turtle.s) cmd $ showCmdArgs args       let msgOut = descriptiveOutput "Standard output" stdOut       let msgErr = descriptiveOutput "Error output" stdErr -      let exitMsg = format ("\n=== Begin Error: "%s%" ===\nExternal command:\n"%s%"\nExit code "%d%"\n"-                            %s%s%"=== End Error: "%s%" ===\n") cmdLabel cmdText i msgOut msgErr cmdLabel+      let exitMsg = Turtle.format ("\n=== Begin Error: "%Turtle.s%" ===\nExternal command:\n"%Turtle.s%"\nExit code "%Turtle.d%"\n"+                            %Turtle.s%Turtle.s%"=== End Error: "%Turtle.s%" ===\n") cmdLabel cmdText i msgOut msgErr cmdLabel       errExit i ch exitMsg timed-    ExitSuccess -> return timed+    Turtle.ExitSuccess -> return timed  procWithEmptyOutput :: ProcFun procWithEmptyOutput cmd args stdinput = do-  ec <- proc cmd args stdinput+  ec <- Turtle.proc cmd args stdinput   return (ec, T.empty, T.empty)  parAwareProc :: HasSequential o => o -> ProcFun-parAwareProc opts = if (sequential opts) then procWithEmptyOutput else procStrictWithErr+parAwareProc opts = if (sequential opts) then procWithEmptyOutput else Turtle.procStrictWithErr  parAwareActions :: HasSequential o => o -> [IO a] -> IO [a] parAwareActions opts = parAwareFun opts   where-    parAwareFun op = if (sequential op) then sequence else single . shellToList . parallel+    parAwareFun op = if (sequential op) then sequence else Turtle.single . shellToList . Turtle.parallel -inprocWithErrFun :: (Text -> IO ()) -> ProcInput -> Shell Line+inprocWithErrFun :: (T.Text -> IO ()) -> ProcInput -> Turtle.Shell Turtle.Line inprocWithErrFun errFun (cmd, args, standardInput) = do-  result <- inprocWithErr cmd args standardInput+  result <- Turtle.inprocWithErr cmd args standardInput   case result of     Right ln -> return ln     Left  ln -> do-      (liftIO . errFun . lineToText) ln-      empty+      (Turtle.liftIO . errFun . Turtle.lineToText) ln+      Turtle.empty -verboseTestFile :: (HasVerbosity o, HasBaseDir o) => o -> TChan LogMessage -> FilePath -> IO Bool+verboseTestFile :: (HasVerbosity o, HasBaseDir o) => o -> TChan LogMessage -> TurtlePath -> IO Bool verboseTestFile opts ch p = do-  fileExists <- testfile p+  fileExists <- Turtle.testfile p   let rel = relativeToBase opts p   if fileExists-    then logVerbose opts ch $ format ("Found a "       %fp%" file at '"%fp%"'") (basename rel) rel-    else logVerbose opts ch $ format ("Did not find a "%fp%" file at '"%fp%"'") (basename rel) rel+    then logVerbose opts ch $ Turtle.format ("Found '"%Turtle.fp%"'") rel+    else logVerbose opts ch $ Turtle.format ("Looked for but did not find '"%Turtle.fp%"'") rel   return fileExists -relativeToBase :: HasBaseDir o => o -> FilePath -> FilePath-relativeToBase opts = relativeToBase' (baseDir opts)--relativeToBase' :: FilePath -> FilePath -> FilePath-relativeToBase' bd p = if forceTrailingSlash bd == forceTrailingSlash p then "./" else-  fromMaybe p $ stripPrefix (forceTrailingSlash bd) p- groupPairs' :: (Eq a, Ord a) => [(a, b)] -> [(a, [b])] groupPairs' = map (\ll -> (fst . head $ ll, map snd ll)) . List.groupBy ((==) `on` fst)               . List.sortBy (comparing fst)@@ -227,25 +228,25 @@ groupValuesBy :: (Ord k, Ord v) => (v -> k) -> [v] -> Map.Map k [v] groupValuesBy keyFun = groupPairs . pairBy keyFun -initialIncludeFilePath :: FilePath -> FilePath-initialIncludeFilePath p = (parent . parent . parent) p </> includeFileName p+initialIncludeFilePath :: TurtlePath -> TurtlePath+initialIncludeFilePath p = (Turtle.parent . Turtle.parent . Turtle.parent) p </> includeFileName p -parentIncludeFilePath :: FilePath -> FilePath-parentIncludeFilePath p = (parent . parent) p </> (filename p)+parentIncludeFilePath :: TurtlePath -> TurtlePath+parentIncludeFilePath p = (Turtle.parent . Turtle.parent) p </> (Turtle.filename p) -allYearsPath :: FilePath -> FilePath-allYearsPath = allYearsPath' directory+allYearsPath :: TurtlePath -> TurtlePath+allYearsPath = allYearsPath' Turtle.directory -allYearsPath' :: (FilePath -> FilePath) -> FilePath -> FilePath+allYearsPath' :: (TurtlePath -> TurtlePath) -> TurtlePath -> TurtlePath allYearsPath' dir p = dir p </> "all-years.journal" -allYearsFileName :: FilePath+allYearsFileName :: TurtlePath allYearsFileName = "all-years" <.> "journal" -groupIncludeFiles :: [FilePath] -> (InputFileBundle, InputFileBundle)+groupIncludeFiles :: [TurtlePath] -> (InputFileBundle, InputFileBundle) groupIncludeFiles = allYearIncludeFiles . groupIncludeFilesPerYear -groupIncludeFilesPerYear :: [FilePath] -> InputFileBundle+groupIncludeFilesPerYear :: [TurtlePath] -> InputFileBundle groupIncludeFilesPerYear [] = Map.empty groupIncludeFilesPerYear ps@(p:_) = case extractImportDirs p of     Right _ -> (groupValuesBy initialIncludeFilePath) ps@@ -254,268 +255,216 @@ allYearIncludeFiles :: InputFileBundle -> (InputFileBundle, InputFileBundle) allYearIncludeFiles m = (m, yearsIncludeMap $ Map.keys m) -yearsIncludeMap :: [FilePath] -> InputFileBundle+yearsIncludeMap :: [TurtlePath] -> InputFileBundle yearsIncludeMap = groupValuesBy allYearsPath -docURL :: Line -> Text-docURL = format ("https://github.com/apauley/hledger-flow#"%l)--lsDirs :: FilePath -> Shell FilePath-lsDirs = onlyDirs . ls+lsDirs :: TurtlePath -> Turtle.Shell TurtlePath+lsDirs = onlyDirs . Turtle.ls -onlyDirs :: Shell FilePath -> Shell FilePath-onlyDirs = excludeHiddenFiles . excludeWeirdPaths . filterPathsByFileStatus isDirectory+onlyDirs :: Turtle.Shell TurtlePath -> Turtle.Shell TurtlePath+onlyDirs = excludeHiddenFiles . excludeWeirdPaths . filterPathsByFileStatus Turtle.isDirectory -onlyFiles :: Shell FilePath -> Shell FilePath-onlyFiles = excludeHiddenFiles . filterPathsByFileStatus isRegularFile+onlyFiles :: Turtle.Shell TurtlePath -> Turtle.Shell TurtlePath+onlyFiles = excludeHiddenFiles . filterPathsByFileStatus Turtle.isRegularFile -filterPathsByFileStatus :: (FileStatus -> Bool) -> Shell FilePath -> Shell FilePath+filterPathsByFileStatus :: (Turtle.FileStatus -> Bool) -> Turtle.Shell TurtlePath -> Turtle.Shell TurtlePath filterPathsByFileStatus filepred files = do   files' <- shellToList files   filtered <- filterPathsByFileStatus' filepred [] files'-  select filtered+  Turtle.select filtered -filterPathsByFileStatus' :: (FileStatus -> Bool) -> [FilePath] -> [FilePath] -> Shell [FilePath]+filterPathsByFileStatus' :: (Turtle.FileStatus -> Bool) -> [TurtlePath] -> [TurtlePath] -> Turtle.Shell [TurtlePath] filterPathsByFileStatus' _ acc [] = return acc filterPathsByFileStatus' filepred acc (file:files) = do-  filestat <- stat file+  filestat <- Turtle.stat file   let filtered = if (filepred filestat) then file:acc else acc   filterPathsByFileStatus' filepred filtered files -filterPaths :: (FilePath -> IO Bool) -> [FilePath] -> Shell [FilePath]+filterPaths :: (TurtlePath -> IO Bool) -> [TurtlePath] -> Turtle.Shell [TurtlePath] filterPaths = filterPaths' [] -filterPaths' :: [FilePath] -> (FilePath -> IO Bool) -> [FilePath] -> Shell [FilePath]+filterPaths' :: [TurtlePath] -> (TurtlePath -> IO Bool) -> [TurtlePath] -> Turtle.Shell [TurtlePath] filterPaths' acc _ [] = return acc filterPaths' acc filepred (file:files) = do-  shouldInclude <- liftIO $ filepred file+  shouldInclude <- Turtle.liftIO $ filepred file   let filtered = if shouldInclude then file:acc else acc   filterPaths' filtered filepred files -excludeHiddenFiles :: Shell FilePath -> Shell FilePath+excludeHiddenFiles :: Turtle.Shell TurtlePath -> Turtle.Shell TurtlePath excludeHiddenFiles paths = do   p <- paths-  case (match (prefix ".") $ format fp $ filename p) of-    [] -> select [p]-    _  -> select []+  case (Turtle.match (Turtle.prefix ".") $ Turtle.format Turtle.fp $ Turtle.filename p) of+    [] -> Turtle.select [p]+    _  -> Turtle.select [] -excludeWeirdPaths :: Shell FilePath -> Shell FilePath-excludeWeirdPaths = findtree (suffix $ noneOf "_")+excludeWeirdPaths :: Turtle.Shell TurtlePath -> Turtle.Shell TurtlePath+excludeWeirdPaths = Turtle.findtree (Turtle.suffix $ Turtle.noneOf "_") -firstExistingFile :: [FilePath] -> IO (Maybe FilePath)+firstExistingFile :: [TurtlePath] -> IO (Maybe TurtlePath) firstExistingFile files = do   case files of     []   -> return Nothing     file:fs -> do-      exists <- testfile file+      exists <- Turtle.testfile file       if exists then return (Just file) else firstExistingFile fs -basenameLine :: FilePath -> Shell Line-basenameLine path = case (textToLine $ format fp $ basename path) of-  Nothing -> die $ format ("Unable to determine basename from path: "%fp%"\n") path+basenameLine :: TurtlePath -> Turtle.Shell Turtle.Line+basenameLine path = case (Turtle.textToLine $ Turtle.format Turtle.fp $ Turtle.basename path) of+  Nothing -> Turtle.die $ Turtle.format ("Unable to determine basename from path: "%Turtle.fp%"\n") path   Just bn -> return bn -buildFilename :: [Line] -> Text -> FilePath-buildFilename identifiers ext = fromText (T.intercalate "-" (map lineToText identifiers)) <.> ext+buildFilename :: [Turtle.Line] -> T.Text -> TurtlePath+buildFilename identifiers ext = Turtle.fromText (T.intercalate "-" (map Turtle.lineToText identifiers)) <.> ext -shellToList :: Shell a -> Shell [a]-shellToList files = fold files Fold.list+shellToList :: Turtle.Shell a -> Turtle.Shell [a]+shellToList files = Turtle.fold files Fold.list -includeFileName :: FilePath -> FilePath-includeFileName = (<.> "journal") . fromText . (format (fp%"-include")) . dirname+includeFileName :: TurtlePath -> TurtlePath+includeFileName = (<.> "journal") . Turtle.fromText . (Turtle.format (Turtle.fp%"-include")) . Turtle.dirname -toIncludeFiles :: (HasBaseDir o, HasVerbosity o) => o -> TChan LogMessage -> InputFileBundle -> IO (Map.Map FilePath Text)+toIncludeFiles :: (HasBaseDir o, HasVerbosity o) => o -> TChan LogMessage -> InputFileBundle -> IO (Map.Map TurtlePath T.Text) toIncludeFiles opts ch m = do   preMap  <- extraIncludes opts ch (Map.keys m) ["opening.journal"] ["pre-import.journal"] []   postMap <- extraIncludes opts ch (Map.keys m) ["closing.journal"] ["post-import.journal"] ["prices.journal"]   return $ (addPreamble . toIncludeFiles' preMap postMap) m -extraIncludes :: (HasBaseDir o, HasVerbosity o) => o -> TChan LogMessage -> [FilePath] -> [Text] -> [FilePath] -> [FilePath] -> IO InputFileBundle+extraIncludes :: (HasBaseDir o, HasVerbosity o) => o -> TChan LogMessage -> [TurtlePath] -> [T.Text] -> [TurtlePath] -> [TurtlePath] -> IO InputFileBundle extraIncludes opts ch = extraIncludes' opts ch Map.empty -extraIncludes' :: (HasBaseDir o, HasVerbosity o) => o -> TChan LogMessage -> InputFileBundle -> [FilePath] -> [Text] -> [FilePath] -> [FilePath] -> IO InputFileBundle+extraIncludes' :: (HasBaseDir o, HasVerbosity o) => o -> TChan LogMessage -> InputFileBundle -> [TurtlePath] -> [T.Text] -> [TurtlePath] -> [TurtlePath] -> IO InputFileBundle extraIncludes' _ _ acc [] _ _ _ = return acc extraIncludes' opts ch acc (file:files) extraSuffixes manualFiles prices = do   extra <- extraIncludesForFile opts ch file extraSuffixes manualFiles prices   extraIncludes' opts ch (Map.unionWith (++) acc extra) files extraSuffixes manualFiles prices -extraIncludesForFile :: (HasVerbosity o, HasBaseDir o) => o -> TChan LogMessage -> FilePath -> [Text] -> [FilePath] -> [FilePath] -> IO InputFileBundle+extraIncludesForFile :: (HasVerbosity o, HasBaseDir o) => o -> TChan LogMessage -> TurtlePath -> [T.Text] -> [TurtlePath] -> [TurtlePath] -> IO InputFileBundle extraIncludesForFile opts ch file extraSuffixes manualFiles prices = do-  let dirprefix = fromText $ fst $ T.breakOn "-" $ format fp $ basename file-  let fileNames = map (\suff -> fromText $ format (fp%"-"%s) dirprefix suff) extraSuffixes-  let suffixFiles = map (directory file </>) fileNames-  let suffixDirFiles = map (directory file </> "_manual_" </> dirprefix </>) manualFiles-  let priceFiles = map (directory file </> ".." </> "prices" </> dirprefix </>) prices+  let dirprefix = Turtle.fromText $ fst $ T.breakOn "-" $ Turtle.format Turtle.fp $ Turtle.basename file+  let fileNames = map (\suff -> Turtle.fromText $ Turtle.format (Turtle.fp%"-"%Turtle.s) dirprefix suff) extraSuffixes+  let suffixFiles = map (Turtle.directory file </>) fileNames+  let suffixDirFiles = map (Turtle.directory file </> "_manual_" </> dirprefix </>) manualFiles+  let priceFiles = map (Turtle.directory file </> ".." </> "prices" </> dirprefix </>) prices   let extraFiles = suffixFiles ++ suffixDirFiles ++ priceFiles-  filtered <- single $ filterPaths testfile extraFiles-  let logMsg = format ("Looking for possible extra include files for '"%fp%"' among these "%d%" options: "%s%". Found "%d%": "%s)-               (relativeToBase opts file) (length extraFiles) (repr $ relativeFilesAsText opts extraFiles)-               (length filtered) (repr $ relativeFilesAsText opts filtered)+  filtered <- Turtle.single $ filterPaths Turtle.testfile extraFiles+  let logMsg = Turtle.format ("Looking for possible extra include files for '"%Turtle.fp%"' among these "%Turtle.d%" options: "%Turtle.s%". Found "%Turtle.d%": "%Turtle.s)+               (relativeToBase opts file) (length extraFiles) (Turtle.repr $ relativeFilesAsText opts extraFiles)+               (length filtered) (Turtle.repr $ relativeFilesAsText opts filtered)   logVerbose opts ch logMsg   return $ Map.fromList [(file, filtered)] -relativeFilesAsText :: HasBaseDir o => o -> [FilePath] -> [Text]-relativeFilesAsText opts ps = map ((format fp) . (relativeToBase opts)) ps+relativeFilesAsText :: HasBaseDir o => o -> [TurtlePath] -> [T.Text]+relativeFilesAsText opts ps = map ((Turtle.format Turtle.fp) . (relativeToBase opts)) ps -toIncludeFiles' :: InputFileBundle -> InputFileBundle -> InputFileBundle -> Map.Map FilePath Text+toIncludeFiles' :: InputFileBundle -> InputFileBundle -> InputFileBundle -> Map.Map TurtlePath T.Text toIncludeFiles' preMap postMap = Map.mapWithKey $ generatedIncludeText preMap postMap -addPreamble :: Map.Map FilePath Text -> Map.Map FilePath Text+addPreamble :: Map.Map TurtlePath T.Text -> Map.Map TurtlePath T.Text addPreamble = Map.map (\txt -> includePreamble <> "\n" <> txt) -toIncludeLine :: FilePath -> FilePath -> Text-toIncludeLine base file = format ("!include "%fp) $ relativeToBase' base file+toIncludeLine :: TurtlePath -> TurtlePath -> T.Text+toIncludeLine base file = Turtle.format ("!include "%Turtle.fp) $ relativeToBase' base file -generatedIncludeText :: InputFileBundle -> InputFileBundle -> FilePath -> [FilePath] -> Text+generatedIncludeText :: InputFileBundle -> InputFileBundle -> TurtlePath -> [TurtlePath] -> T.Text generatedIncludeText preMap postMap outputFile fs = do   let preFiles = fromMaybe [] $ Map.lookup outputFile preMap   let files = List.nub . List.sort $ fs   let postFiles = fromMaybe [] $ Map.lookup outputFile postMap-  let lns = map (toIncludeLine $ directory outputFile) $ preFiles ++ files ++ postFiles+  let lns = map (toIncludeLine $ Turtle.directory outputFile) $ preFiles ++ files ++ postFiles   T.intercalate "\n" $ lns ++ [""] -includePreamble :: Text+includePreamble :: T.Text includePreamble = "### Generated by hledger-flow - DO NOT EDIT ###\n" -groupAndWriteIncludeFiles :: (HasBaseDir o, HasVerbosity o) => o -> TChan LogMessage -> [FilePath] -> IO [FilePath]+groupAndWriteIncludeFiles :: (HasBaseDir o, HasVerbosity o) => o -> TChan LogMessage -> [TurtlePath] -> IO [TurtlePath] groupAndWriteIncludeFiles opts ch = writeFileMap opts ch . groupIncludeFiles -writeFiles :: IO (Map.Map FilePath Text) -> IO [FilePath]+writeFiles :: IO (Map.Map TurtlePath T.Text) -> IO [TurtlePath] writeFiles fileMap = do   m <- fileMap   writeFiles' m -writeFiles' :: Map.Map FilePath Text -> IO [FilePath]+writeFiles' :: Map.Map TurtlePath T.Text -> IO [TurtlePath] writeFiles' fileMap = do   writeTextMap fileMap   return $ Map.keys fileMap -writeTextMap :: Map.Map FilePath Text -> IO ()-writeTextMap = Map.foldlWithKey (\a k v -> a <> writeTextFile k v) (return ())+writeTextMap :: Map.Map TurtlePath T.Text -> IO ()+writeTextMap = Map.foldlWithKey (\a k v -> a <> Turtle.writeTextFile k v) (return ()) -writeFileMap :: (HasBaseDir o, HasVerbosity o) => o -> TChan LogMessage -> (InputFileBundle, InputFileBundle) -> IO [FilePath]+writeFileMap :: (HasBaseDir o, HasVerbosity o) => o -> TChan LogMessage -> (InputFileBundle, InputFileBundle) -> IO [TurtlePath] writeFileMap opts ch (m, allYears) = do   _ <- writeFiles' $ (addPreamble . toIncludeFiles' Map.empty Map.empty) allYears   writeFiles . (toIncludeFiles opts ch) $ m -writeIncludesUpTo :: (HasBaseDir o, HasVerbosity o) => o -> TChan LogMessage -> FilePath -> [FilePath] -> IO [FilePath]+writeIncludesUpTo :: (HasBaseDir o, HasVerbosity o) => o -> TChan LogMessage -> TurtlePath -> [TurtlePath] -> IO [TurtlePath] writeIncludesUpTo _ _ _ [] = return [] writeIncludesUpTo opts ch stopAt journalFiles = do-  let shouldStop = any (\dir -> dir == stopAt) $ map parent journalFiles+  let shouldStop = any (\dir -> dir == stopAt) $ map Turtle.parent journalFiles   if shouldStop     then return journalFiles     else do       newJournalFiles <- groupAndWriteIncludeFiles opts ch journalFiles       writeIncludesUpTo opts ch stopAt newJournalFiles -writeToplevelAllYearsInclude :: (HasBaseDir o, HasVerbosity o) => o -> IO [FilePath]+writeToplevelAllYearsInclude :: (HasBaseDir o, HasVerbosity o) => o -> IO [TurtlePath] writeToplevelAllYearsInclude opts = do-  let allTop = Map.singleton (baseDir opts </> allYearsFileName) ["import" </> allYearsFileName]+  let allTop = Map.singleton (turtleBaseDir opts </> allYearsFileName) ["import" </> allYearsFileName]   writeFiles' $ (addPreamble . toIncludeFiles' Map.empty Map.empty) allTop -changeExtension :: Text -> FilePath -> FilePath-changeExtension ext path = (dropExtension path) <.> ext+changeExtension :: T.Text -> TurtlePath -> TurtlePath+changeExtension ext path = (Turtle.dropExtension path) <.> ext -changePathAndExtension :: FilePath -> Text -> FilePath -> FilePath+changePathAndExtension :: TurtlePath -> T.Text -> TurtlePath -> TurtlePath changePathAndExtension newOutputLocation newExt = (changeOutputPath newOutputLocation) . (changeExtension newExt) -changeOutputPath :: FilePath -> FilePath -> FilePath-changeOutputPath newOutputLocation srcFile = mconcat $ map changeSrcDir $ splitDirectories srcFile+changeOutputPath :: TurtlePath -> TurtlePath -> TurtlePath+changeOutputPath newOutputLocation srcFile = mconcat $ map changeSrcDir $ Turtle.splitDirectories srcFile   where changeSrcDir file = if (file == "1-in/" || file == "2-preprocessed/") then newOutputLocation else file -errorMessageBaseDir :: FilePath -> Text-errorMessageBaseDir startDir = format ("\nUnable to find an hledger-flow import directory at '"%fp-                                       %"' (or in any of its parent directories).\n\n"-                                       %"Have a look at the documentation for more information:\n"%s%"\n")-                               startDir (docURL "getting-started")--determineBaseDir :: Maybe FilePath -> IO (FilePath, FilePath)-determineBaseDir (Just suppliedDir) = determineBaseDir' suppliedDir-determineBaseDir Nothing = pwd >>= determineBaseDir'--determineBaseDir' :: FilePath -> IO (FilePath, FilePath)-determineBaseDir' startDir = do-  currentDir <- pwd-  let absoluteStartDir = if relative startDir then collapse (currentDir </> startDir) else startDir-  ebd <- determineBaseDirFromAbsoluteStartDir absoluteStartDir-  case ebd of-    Right bd -> return bd-    Left  t  -> die t--determineBaseDirFromAbsoluteStartDir :: FilePath -> IO (Either Text (FilePath, FilePath))-determineBaseDirFromAbsoluteStartDir absoluteStartDir = determineBaseDirFromAbsoluteStartDir' absoluteStartDir absoluteStartDir--determineBaseDirFromAbsoluteStartDir' :: FilePath -> FilePath -> IO (Either Text (FilePath, FilePath))-determineBaseDirFromAbsoluteStartDir' startDir possibleBaseDir = do-  possibleBDExists <- testdir possibleBaseDir-  if not possibleBDExists then return $ Left $ format ("The provided directory does not exist: "%fp) possibleBaseDir-    else-    if relative startDir || relative possibleBaseDir then-    return $ Left $ format ("Internal error: found a relative path when expecting only absolute paths:\n"%fp%"\n"%fp%"\n") startDir possibleBaseDir-    else do-    foundBaseDir <- testdir $ possibleBaseDir </> "import"-    if foundBaseDir then-      do-      let baseD = forceTrailingSlash possibleBaseDir-      let runDir = forceTrailingSlash $ relativeToBase' baseD startDir-      return $ Right $ (baseD, runDir)-    else do-      let doneSearching = (possibleBaseDir `elem` ["/", "./"])-      if doneSearching-        then return $ Left $ errorMessageBaseDir startDir-        else determineBaseDirFromAbsoluteStartDir' startDir $ parent possibleBaseDir---dirOrPwd :: Maybe FilePath -> IO FilePath-dirOrPwd maybeBaseDir = fmap forceTrailingSlash (fromMaybe pwd $ fmap realpath maybeBaseDir)--forceTrailingSlash :: FilePath -> FilePath-forceTrailingSlash p = directory (p </> "temp")--importDirBreakdown ::  FilePath -> [FilePath]+importDirBreakdown ::  TurtlePath -> [TurtlePath] importDirBreakdown = importDirBreakdown' [] -importDirBreakdown' :: [FilePath] -> FilePath -> [FilePath]+importDirBreakdown' :: [TurtlePath] -> TurtlePath -> [TurtlePath] importDirBreakdown' acc path = do-  let dir = directory path-  if (dirname dir == "import" || (dirname dir == ""))+  let dir = Turtle.directory path+  if (Turtle.dirname dir == "import" || (Turtle.dirname dir == ""))     then dir:acc-    else importDirBreakdown' (dir:acc) $ parent dir+    else importDirBreakdown' (dir:acc) $ Turtle.parent dir -extractImportDirs :: FilePath -> Either Text IT.ImportDirs+extractImportDirs :: TurtlePath -> Either T.Text IT.ImportDirs extractImportDirs inputFile = do   case importDirBreakdown inputFile of     [bd,owner,bank,account,filestate,year] -> Right $ IT.ImportDirs bd owner bank account filestate year     _ -> do-      Left $ format ("I couldn't find the right number of directories between \"import\" and the input file:\n"%fp+      Left $ Turtle.format ("I couldn't find the right number of directories between \"import\" and the input file:\n"%Turtle.fp                       %"\n\nhledger-flow expects to find input files in this structure:\n"%                       "import/owner/bank/account/filestate/year/trxfile\n\n"%-                      "Have a look at the documentation for a detailed explanation:\n"%s) inputFile (docURL "input-files")+                      "Have a look at the documentation for a detailed explanation:\n"%Turtle.s) inputFile (docURL "input-files") -listOwners :: HasBaseDir o => o -> Shell FilePath-listOwners opts = fmap basename $ lsDirs $ (baseDir opts) </> "import"+listOwners :: HasBaseDir o => o -> Turtle.Shell TurtlePath+listOwners opts = fmap Turtle.basename $ lsDirs $ (turtleBaseDir opts) </> "import" -intPath :: Integer -> FilePath-intPath = fromText . (format d)+intPath :: Integer -> TurtlePath+intPath = Turtle.fromText . (Turtle.format Turtle.d) -includeYears :: TChan LogMessage -> FilePath -> IO [Integer]+includeYears :: TChan LogMessage -> TurtlePath -> IO [Integer] includeYears ch includeFile = do-  txt <- readTextFile includeFile+  txt <- Turtle.readTextFile includeFile   case includeYears' txt of     Left  msg   -> do       channelErrLn ch msg       return []     Right years -> return years -includeYears' :: Text -> Either Text [Integer]+includeYears' :: T.Text -> Either T.Text [Integer] includeYears' txt = case partitionEithers (includeYears'' txt) of   (errors, []) -> do-    let msg = format ("Unable to extract years from the following text:\n"%s%"\nErrors:\n"%s) txt (T.intercalate "\n" $ map T.pack errors)+    let msg = Turtle.format ("Unable to extract years from the following text:\n"%Turtle.s%"\nErrors:\n"%Turtle.s) txt (T.intercalate "\n" $ map T.pack errors)     Left msg   (_, years) -> Right years -includeYears'' :: Text -> [Either String Integer]+includeYears'' :: T.Text -> [Either String Integer] includeYears'' txt = map extractDigits (T.lines txt) -extractDigits :: Text -> Either String Integer+extractDigits :: T.Text -> Either String Integer extractDigits txt = fmap fst $ (T.decimal . (T.filter isDigit)) txt
+ src/Hledger/Flow/DocHelpers.hs view
@@ -0,0 +1,10 @@+{-# LANGUAGE OverloadedStrings #-}++module Hledger.Flow.DocHelpers where++import qualified Data.Text as T (Text)+import qualified Turtle as Turtle (Line, format, l)+import Turtle ((%))++docURL :: Turtle.Line -> T.Text+docURL = Turtle.format ("https://github.com/apauley/hledger-flow#"%Turtle.l)
src/Hledger/Flow/Import/Types.hs view
@@ -1,13 +1,12 @@ module Hledger.Flow.Import.Types where -import Turtle-import Prelude hiding (FilePath, putStrLn)+import Hledger.Flow.PathHelpers (TurtlePath) -data ImportDirs = ImportDirs { importDir  :: FilePath-                             , ownerDir   :: FilePath-                             , bankDir    :: FilePath-                             , accountDir :: FilePath-                             , stateDir   :: FilePath-                             , yearDir    :: FilePath+data ImportDirs = ImportDirs { importDir  :: TurtlePath+                             , ownerDir   :: TurtlePath+                             , bankDir    :: TurtlePath+                             , accountDir :: TurtlePath+                             , stateDir   :: TurtlePath+                             , yearDir    :: TurtlePath                              } deriving (Show)
+ src/Hledger/Flow/PathHelpers.hs view
@@ -0,0 +1,57 @@+{-# LANGUAGE OverloadedStrings #-}++module Hledger.Flow.PathHelpers where++import Control.Monad.Catch (MonadThrow, Exception, throwM)+import Control.Monad.IO.Class (MonadIO)++import qualified Data.Text as T+import qualified Path as Path+import qualified Path.IO as Path+import qualified Turtle as Turtle++import Hledger.Flow.DocHelpers (docURL)++type TurtlePath = Turtle.FilePath++type AbsFile = Path.Path Path.Abs Path.File+type RelFile = Path.Path Path.Rel Path.File+type AbsDir = Path.Path Path.Abs Path.Dir+type RelDir = Path.Path Path.Rel Path.Dir++data PathException = MissingBaseDir AbsDir | InvalidTurtleDir TurtlePath+  deriving (Eq)++instance Show PathException where+  show (MissingBaseDir d) = "Unable to find an import directory at " ++ show d +++    " (or in any of its parent directories).\n\n" +++    "Have a look at the documentation for more information:\n" +++    T.unpack (docURL "getting-started")+  show (InvalidTurtleDir  d) = "Expected a directory but got this instead: " ++ Turtle.encodeString d++instance Exception PathException++fromTurtleAbsFile :: MonadThrow m => TurtlePath -> m AbsFile+fromTurtleAbsFile turtlePath = Path.parseAbsFile $ Turtle.encodeString turtlePath++fromTurtleRelFile :: MonadThrow m => TurtlePath -> m RelFile+fromTurtleRelFile turtlePath = Path.parseRelFile $ Turtle.encodeString turtlePath++fromTurtleAbsDir :: MonadThrow m => TurtlePath -> m AbsDir+fromTurtleAbsDir turtlePath = Path.parseAbsDir $ Turtle.encodeString turtlePath++fromTurtleRelDir :: MonadThrow m => TurtlePath -> m RelDir+fromTurtleRelDir turtlePath = Path.parseRelDir $ Turtle.encodeString turtlePath++turtleToAbsDir :: (MonadIO m, MonadThrow m) => AbsDir -> TurtlePath -> m AbsDir+turtleToAbsDir baseDir p = do+  isDir <- Turtle.testdir p+  if isDir+    then Path.resolveDir baseDir $ Turtle.encodeString p+    else throwM $ InvalidTurtleDir p++pathToTurtle :: Path.Path b t -> TurtlePath+pathToTurtle = Turtle.decodeString . Path.toFilePath++forceTrailingSlash :: TurtlePath -> TurtlePath+forceTrailingSlash p = Turtle.directory (p Turtle.</> "temp")
src/Hledger/Flow/Reports.hs view
@@ -4,51 +4,56 @@     ( generateReports     ) where -import Turtle hiding (stdout, stderr, proc)-import Prelude hiding (FilePath, putStrLn, writeFile)+import qualified Turtle as Turtle hiding (stdout, stderr, proc)+import Turtle ((%), (</>), (<.>))+import Prelude hiding (putStrLn, writeFile)+ import Hledger.Flow.RuntimeOptions import Hledger.Flow.Common+import Hledger.Flow.BaseDir (turtleBaseDir, relativeToBase)+ import Control.Concurrent.STM import Data.Either import Data.Maybe  import qualified Data.Text as T import qualified Hledger.Flow.Types as FlowTypes+import Hledger.Flow.PathHelpers (TurtlePath) import qualified Data.List as List -data ReportParams = ReportParams { ledgerFile :: FilePath+data ReportParams = ReportParams { ledgerFile :: TurtlePath                                  , reportYears :: [Integer]-                                 , outputDir :: FilePath+                                 , outputDir :: TurtlePath                                  }                   deriving (Show)-type ReportGenerator = RuntimeOptions -> TChan FlowTypes.LogMessage -> FilePath -> FilePath -> Integer -> IO (Either FilePath FilePath)+type ReportGenerator = RuntimeOptions -> TChan FlowTypes.LogMessage -> TurtlePath -> TurtlePath -> Integer -> IO (Either TurtlePath TurtlePath)  generateReports :: RuntimeOptions -> IO ()-generateReports opts = sh (+generateReports opts = Turtle.sh (   do-    ch <- liftIO newTChanIO-    logHandle <- fork $ consoleChannelLoop ch-    liftIO $ if (showOptions opts) then channelOutLn ch (repr opts) else return ()-    (reports, diff) <- time $ liftIO $ generateReports' opts ch+    ch <- Turtle.liftIO newTChanIO+    logHandle <- Turtle.fork $ consoleChannelLoop ch+    Turtle.liftIO $ if (showOptions opts) then channelOutLn ch (Turtle.repr opts) else return ()+    (reports, diff) <- Turtle.time $ Turtle.liftIO $ generateReports' opts ch     let failedAttempts = lefts reports-    let failedText = if List.null failedAttempts then "" else format ("(and attempted to write "%d%" more) ") $ length failedAttempts-    liftIO $ channelOutLn ch $ format ("Generated "%d%" reports "%s%"in "%s) (length (rights reports)) failedText $ repr diff-    liftIO $ terminateChannelLoop ch-    wait logHandle+    let failedText = if List.null failedAttempts then "" else Turtle.format ("(and attempted to write "%Turtle.d%" more) ") $ length failedAttempts+    Turtle.liftIO $ channelOutLn ch $ Turtle.format ("Generated "%Turtle.d%" reports "%Turtle.s%"in "%Turtle.s) (length (rights reports)) failedText $ Turtle.repr diff+    Turtle.liftIO $ terminateChannelLoop ch+    Turtle.wait logHandle   ) -generateReports' :: RuntimeOptions -> TChan FlowTypes.LogMessage -> IO [Either FilePath FilePath]+generateReports' :: RuntimeOptions -> TChan FlowTypes.LogMessage -> IO [Either TurtlePath TurtlePath] generateReports' opts ch = do   let wipMsg = "Report generation is still a work-in-progress - please let me know how this can be more useful.\n\n"                <> "Keep an eye out for report-related pull requests and issues, and feel free to submit some of your own:\n"                <> "https://github.com/apauley/hledger-flow/pulls\n"                <> "https://github.com/apauley/hledger-flow/issues\n"   channelOutLn ch wipMsg-  owners <- single $ shellToList $ listOwners opts-  ledgerEnvValue <- need "LEDGER_FILE" :: IO (Maybe Text)-  let hledgerJournal = fromMaybe (baseDir opts </> allYearsFileName) $ fmap fromText ledgerEnvValue-  hledgerJournalExists <- testfile hledgerJournal-  _ <- if not hledgerJournalExists then die $ format ("Unable to find journal file: "%fp%"\nIs your LEDGER_FILE environment variable set correctly?") hledgerJournal else return ()+  owners <- Turtle.single $ shellToList $ listOwners opts+  ledgerEnvValue <- Turtle.need "LEDGER_FILE" :: IO (Maybe T.Text)+  let hledgerJournal = fromMaybe (turtleBaseDir opts </> allYearsFileName) $ fmap Turtle.fromText ledgerEnvValue+  hledgerJournalExists <- Turtle.testfile hledgerJournal+  _ <- if not hledgerJournalExists then Turtle.die $ Turtle.format ("Unable to find journal file: "%Turtle.fp%"\nIs your LEDGER_FILE environment variable set correctly?") hledgerJournal else return ()   let journalWithYears = journalFile opts []   let aggregateReportDir = outputReportDir opts ["all"]   aggregateYears <- includeYears ch journalWithYears@@ -59,77 +64,81 @@   ownerParams <- ownerParameters opts ch owners   let ownerWithAggregateParams = (if length owners > 1 then [aggregateParams] else []) ++ ownerParams   let sharedOptions = ["--pretty-tables", "--depth", "2"]-  let ownerWithAggregateReports = List.concat $ fmap (reportActions opts ch [incomeStatement sharedOptions, balanceSheet sharedOptions]) ownerWithAggregateParams+  let ownerWithAggregateReports = List.concat $ fmap (reportActions opts ch [incomeStatement sharedOptions, incomeMonthlyStatement sharedOptions, balanceSheet sharedOptions]) ownerWithAggregateParams   let ownerOnlyReports = List.concat $ fmap (reportActions opts ch [accountList, unknownTransactions]) ownerParams   parAwareActions opts (aggregateOnlyReports ++ ownerWithAggregateReports ++ ownerOnlyReports) -reportActions :: RuntimeOptions -> TChan FlowTypes.LogMessage -> [ReportGenerator] -> ReportParams -> [IO (Either FilePath FilePath)]+reportActions :: RuntimeOptions -> TChan FlowTypes.LogMessage -> [ReportGenerator] -> ReportParams -> [IO (Either TurtlePath TurtlePath)] reportActions opts ch reports (ReportParams journal years reportsDir) = do   y <- years   map (\r -> r opts ch journal reportsDir y) reports  accountList :: ReportGenerator-accountList opts ch journal reportsDir year = do+accountList opts ch journal baseOutDir year = do   let reportArgs = ["accounts"]-  generateReport opts ch journal reportsDir year ("accounts" <.> "txt") reportArgs (not . T.null)+  generateReport opts ch journal year (baseOutDir </> intPath year) ("accounts" <.> "txt") reportArgs (not . T.null)  unknownTransactions :: ReportGenerator-unknownTransactions opts ch journal reportsDir year = do+unknownTransactions opts ch journal baseOutDir year = do   let reportArgs = ["print", "unknown"]-  generateReport opts ch journal reportsDir year ("unknown-transactions" <.> "txt") reportArgs (not . T.null)+  generateReport opts ch journal year (baseOutDir </> intPath year) ("unknown-transactions" <.> "txt") reportArgs (not . T.null) -incomeStatement :: [Text] -> ReportGenerator-incomeStatement sharedOptions opts ch journal reportsDir year = do+incomeStatement :: [T.Text] -> ReportGenerator+incomeStatement sharedOptions opts ch journal baseOutDir year = do   let reportArgs = ["incomestatement"] ++ sharedOptions-  generateReport opts ch journal reportsDir year ("income-expenses" <.> "txt") reportArgs (not . T.null)+  generateReport opts ch journal year (baseOutDir </> intPath year) ("income-expenses" <.> "txt") reportArgs (not . T.null) -balanceSheet :: [Text] -> ReportGenerator-balanceSheet sharedOptions opts ch journal reportsDir year = do+incomeMonthlyStatement :: [T.Text] -> ReportGenerator+incomeMonthlyStatement sharedOptions opts ch journal baseOutDir year = do+  let reportArgs = ["incomestatement"] ++ sharedOptions ++ ["--monthly"]+  generateReport opts ch journal year (baseOutDir </> intPath year </> "monthly") ("income-expenses" <.> "txt") reportArgs (not . T.null)++balanceSheet :: [T.Text] -> ReportGenerator+balanceSheet sharedOptions opts ch journal baseOutDir year = do   let reportArgs = ["balancesheet"] ++ sharedOptions ++ ["--flat"]-  generateReport opts ch journal reportsDir year ("balance-sheet" <.> "txt") reportArgs (not . T.null)+  generateReport opts ch journal year (baseOutDir </> intPath year) ("balance-sheet" <.> "txt") reportArgs (not . T.null)  transferBalance :: ReportGenerator-transferBalance opts ch journal reportsDir year = do+transferBalance opts ch journal baseOutDir year = do   let reportArgs = ["balance", "--pretty-tables", "--quarterly", "--flat", "--no-total", "transfer"]-  generateReport opts ch journal reportsDir year ("transfer-balance" <.> "txt") reportArgs (\txt -> (length $ T.lines txt) > 4)+  generateReport opts ch journal year (baseOutDir </> intPath year) ("transfer-balance" <.> "txt") reportArgs (\txt -> (length $ T.lines txt) > 4) -generateReport :: RuntimeOptions -> TChan FlowTypes.LogMessage -> FilePath -> FilePath -> Integer -> FilePath -> [Text] -> (Text -> Bool) -> IO (Either FilePath FilePath)-generateReport opts ch journal baseOutDir year fileName args successCheck = do-  let reportsDir = baseOutDir </> intPath year-  mktree reportsDir+generateReport :: RuntimeOptions -> TChan FlowTypes.LogMessage -> TurtlePath -> Integer -> TurtlePath -> TurtlePath -> [T.Text] -> (T.Text -> Bool) -> IO (Either TurtlePath TurtlePath)+generateReport opts ch journal year reportsDir fileName args successCheck = do+  Turtle.mktree reportsDir   let outputFile = reportsDir </> fileName   let relativeJournal = relativeToBase opts journal   let relativeOutputFile = relativeToBase opts outputFile-  let reportArgs = ["--file", format fp journal, "--period", repr year] ++ args-  let reportDisplayArgs = ["--file", format fp relativeJournal, "--period", repr year] ++ args-  let hledger = format fp $ FlowTypes.hlPath . hledgerInfo $ opts :: Text-  let cmdLabel = format ("hledger "%s) $ showCmdArgs reportDisplayArgs-  ((exitCode, stdOut, _), _) <- timeAndExitOnErr opts ch cmdLabel dummyLogger channelErr procStrictWithErr (hledger, reportArgs, empty)+  let reportArgs = ["--file", Turtle.format Turtle.fp journal, "--period", Turtle.repr year] ++ args+  let reportDisplayArgs = ["--file", Turtle.format Turtle.fp relativeJournal, "--period", Turtle.repr year] ++ args+  let hledger = Turtle.format Turtle.fp $ FlowTypes.hlPath . hledgerInfo $ opts :: T.Text+  let cmdLabel = Turtle.format ("hledger "%Turtle.s) $ showCmdArgs reportDisplayArgs+  ((exitCode, stdOut, _), _) <- timeAndExitOnErr opts ch cmdLabel dummyLogger channelErr Turtle.procStrictWithErr (hledger, reportArgs, mempty)   if (successCheck stdOut)     then     do-      writeTextFile outputFile (cmdLabel <> "\n\n"<> stdOut)-      logVerbose opts ch $ format ("Wrote "%fp) $ relativeOutputFile+      Turtle.writeTextFile outputFile (cmdLabel <> "\n\n"<> stdOut)+      logVerbose opts ch $ Turtle.format ("Wrote "%Turtle.fp) $ relativeOutputFile       return $ Right outputFile     else     do-      channelErrLn ch $ format ("Did not write '"%fp%"' ("%s%") "%s) relativeOutputFile cmdLabel (repr exitCode)-      exists <- testfile outputFile-      if exists then rm outputFile else return ()+      channelErrLn ch $ Turtle.format ("Did not write '"%Turtle.fp%"' ("%Turtle.s%") "%Turtle.s) relativeOutputFile cmdLabel (Turtle.repr exitCode)+      exists <- Turtle.testfile outputFile+      if exists then Turtle.rm outputFile else return ()       return $ Left outputFile -journalFile :: RuntimeOptions -> [FilePath] -> FilePath-journalFile opts dirs = (foldl (</>) (baseDir opts) ("import":dirs)) </> allYearsFileName+journalFile :: RuntimeOptions -> [TurtlePath] -> TurtlePath+journalFile opts dirs = (foldl (</>) (turtleBaseDir opts) ("import":dirs)) </> allYearsFileName -outputReportDir :: RuntimeOptions -> [FilePath] -> FilePath-outputReportDir opts dirs = foldl (</>) (baseDir opts) ("reports":dirs)+outputReportDir :: RuntimeOptions -> [TurtlePath] -> TurtlePath+outputReportDir opts dirs = foldl (</>) (turtleBaseDir opts) ("reports":dirs) -ownerParameters :: RuntimeOptions -> TChan FlowTypes.LogMessage -> [FilePath] -> IO [ReportParams]+ownerParameters :: RuntimeOptions -> TChan FlowTypes.LogMessage -> [TurtlePath] -> IO [ReportParams] ownerParameters opts ch owners = do   let actions = map (ownerParameters' opts ch) owners   parAwareActions opts actions -ownerParameters' :: RuntimeOptions -> TChan FlowTypes.LogMessage -> FilePath -> IO ReportParams+ownerParameters' :: RuntimeOptions -> TChan FlowTypes.LogMessage -> TurtlePath-> IO ReportParams ownerParameters' opts ch owner = do   let ownerJournal = journalFile opts [owner]   years <- includeYears ch ownerJournal
src/Hledger/Flow/RuntimeOptions.hs view
@@ -1,14 +1,15 @@ module Hledger.Flow.RuntimeOptions where -import Turtle-import Prelude hiding (FilePath, putStrLn)+import qualified Data.Text as T+import Prelude hiding (putStrLn) import Hledger.Flow.Types -data RuntimeOptions = RuntimeOptions { baseDir :: FilePath-                                     , importRunDir :: FilePath+data RuntimeOptions = RuntimeOptions { baseDir :: BaseDir+                                     , importRunDir :: RunDir                                      , useRunDir :: Bool-                                     , hfVersion :: Text+                                     , onlyNewFiles :: Bool+                                     , hfVersion :: T.Text                                      , hledgerInfo :: HledgerInfo                                      , sysInfo :: SystemInfo                                      , verbose :: Bool@@ -18,10 +19,13 @@   deriving (Show)  instance HasVerbosity RuntimeOptions where-  verbose (RuntimeOptions _ _ _ _ _ _ v _ _) = v+  verbose (RuntimeOptions _ _ _ _ _ _ _ v _ _) = v  instance HasSequential RuntimeOptions where-  sequential (RuntimeOptions _ _ _ _ _ _ _ _ sq) = sq+  sequential (RuntimeOptions _ _ _ _ _ _ _ _ _ sq) = sq  instance HasBaseDir RuntimeOptions where-  baseDir (RuntimeOptions bd _ _ _ _ _ _ _ _) = bd+  baseDir (RuntimeOptions bd _ _ _ _ _ _ _ _ _) = bd++instance HasRunDir RuntimeOptions where+  importRunDir (RuntimeOptions _ rd _ _ _ _ _ _ _ _) = rd
src/Hledger/Flow/Types.hs view
@@ -4,19 +4,24 @@ module Hledger.Flow.Types where -import Turtle-import Prelude hiding (FilePath, putStrLn)+import qualified Turtle as Turtle (ExitCode, NominalDiffTime, Shell, Line)+import qualified Data.Text as T import Data.Version -data LogMessage = StdOut Text | StdErr Text | Terminate deriving (Show)-type FullOutput = (ExitCode, Text, Text)-type FullTimedOutput = (FullOutput, NominalDiffTime)+import Hledger.Flow.PathHelpers -type ProcFun = Text -> [Text] -> Shell Line -> IO FullOutput-type ProcInput = (Text, [Text], Shell Line)+type BaseDir = AbsDir+type RunDir = RelDir -data HledgerInfo = HledgerInfo { hlPath :: FilePath-                               , hlVersion :: Text+data LogMessage = StdOut T.Text | StdErr T.Text | Terminate deriving (Show)+type FullOutput = (Turtle.ExitCode, T.Text, T.Text)+type FullTimedOutput = (FullOutput, Turtle.NominalDiffTime)++type ProcFun = T.Text -> [T.Text] -> Turtle.Shell Turtle.Line -> IO FullOutput+type ProcInput = (T.Text, [T.Text], Turtle.Shell Turtle.Line)++data HledgerInfo = HledgerInfo { hlPath :: TurtlePath+                               , hlVersion :: T.Text                                }                  deriving (Show) @@ -31,7 +36,10 @@   verbose :: a -> Bool  class HasBaseDir a where-  baseDir :: a -> FilePath+  baseDir :: a -> BaseDir++class HasRunDir a where+  importRunDir :: a -> RunDir  class HasSequential a where   sequential :: a -> Bool
+ test/BaseDir/Integration.hs view
@@ -0,0 +1,121 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE OverloadedLists #-}+{-# LANGUAGE QuasiQuotes #-}++module BaseDir.Integration (tests) where++import Control.Exception (try)+import Test.HUnit++import Path+import Path.IO++import qualified Turtle as Turtle+import qualified Data.Text as T++import Hledger.Flow.Common+import Hledger.Flow.Types (BaseDir, RunDir)+import Hledger.Flow.BaseDir (determineBaseDir)+import Hledger.Flow.PathHelpers++assertSubDirsForDetermineBaseDir :: AbsDir -> BaseDir -> [Path.Path b Dir] -> IO ()+assertSubDirsForDetermineBaseDir initialPwd expectedBaseDir importDirs = do+  _ <- sequence $ map (assertDetermineBaseDir initialPwd expectedBaseDir) importDirs+  return ()++assertDetermineBaseDir :: AbsDir -> BaseDir -> Path.Path b Dir -> IO ()+assertDetermineBaseDir initialPwd expectedBaseDir subDir = do+  setCurrentDir initialPwd+  (bd1, runDir1) <- determineBaseDir $ Just $ pathToTurtle subDir+  assertFindTestFileUsingRundir bd1 runDir1++  setCurrentDir subDir+  (bd2, runDir2) <- determineBaseDir Nothing+  assertFindTestFileUsingRundir bd2 runDir2++  (bd3, runDir3) <- determineBaseDir $ Just "."+  assertFindTestFileUsingRundir bd3 runDir3++  (bd4, runDir4) <- determineBaseDir $ Just "./"+  assertFindTestFileUsingRundir bd4 runDir4++  setCurrentDir initialPwd+  let msg dir = "determineBaseDir searches from pwd upwards until it finds a dir containing 'import' - " ++ (show dir)+  _ <- sequence $ map (\dir -> assertEqual (msg dir) expectedBaseDir dir) [bd1, bd2, bd3, bd4]+  return ()++assertFindTestFileUsingRundir :: BaseDir -> RunDir -> IO ()+assertFindTestFileUsingRundir baseDir runDir = do+  let absRunDir = baseDir </> runDir++  found <- Turtle.single $ fmap head $ shellToList $ Turtle.find (Turtle.has "test-file.txt") $ pathToTurtle absRunDir+  fileContents <- Turtle.readTextFile found+  assertEqual "We should find our test file by searching from the returned runDir" (T.pack $ "The expected base dir is " ++ show baseDir) fileContents++assertCurrentDirVariations :: AbsDir -> RelDir -> IO ()+assertCurrentDirVariations absoluteTempDir bdRelativeToTempDir = do+  let absBaseDir = absoluteTempDir </> bdRelativeToTempDir++  setCurrentDir absBaseDir+  (bd1, runDir1) <- determineBaseDir Nothing+  (bd2, runDir2) <- determineBaseDir $ Just "."+  (bd3, runDir3) <- determineBaseDir $ Just "./"+  (bd4, runDir4) <- determineBaseDir $ Just $ pathToTurtle absBaseDir++  let msg label dir = "When pwd is the base dir, determineBaseDir returns the same " ++ label ++ ", regardless of the input variation. " ++ (show dir)+  _ <- sequence $ map (\dir -> assertEqual (msg "baseDir" dir) absBaseDir dir) [bd1, bd2, bd3, bd4]+  _ <- sequence $ map (\dir -> assertEqual (msg "runDir" dir) [reldir|.|] dir) [runDir1, runDir2, runDir3, runDir4]+  return ()++testBaseDirWithTempDir :: AbsDir -> AbsDir -> IO ()+testBaseDirWithTempDir initialPwd absoluteTempDir = do+  error1 <- try $ determineBaseDir $ Just "/path/to/dir"+  assertEqual "determineBaseDir produces an error message when given a non-existant dir" (Left $ InvalidTurtleDir "/path/to/dir") error1++  let unrelatedDir = absoluteTempDir </> [reldir|unrelated|]+  createDir unrelatedDir++  bdUnrelated <- try $ determineBaseDir $ Just (pathToTurtle unrelatedDir)+  assertEqual "determineBaseDir produces an error message when it cannot find a baseDir" (Left $ MissingBaseDir unrelatedDir) bdUnrelated++  let baseDir = [reldir|bd1|]+  let importDir = baseDir </> [reldir|import|]+  let ownerDir = importDir </> [reldir|john|]+  let bankDir = ownerDir </> [reldir|mybank|]+  let accDir = bankDir </> [reldir|myacc|]+  let inDir = accDir </> [reldir|1-in|]+  let yearDir = inDir </> [reldir|2019|]+  let subDirs = [yearDir, inDir, accDir, bankDir, ownerDir, importDir, baseDir] :: [RelDir]++  createDirIfMissing True $ absoluteTempDir </> yearDir++  let fictionalDir = absoluteTempDir </> ownerDir </> [reldir|fictionalDir|]+  errorSub <- try $ determineBaseDir $ Just $ pathToTurtle fictionalDir+  assertEqual "determineBaseDir produces an error message when given a non-existant subdir of a valid basedir" (Left $ InvalidTurtleDir $ pathToTurtle fictionalDir) errorSub++  assertCurrentDirVariations absoluteTempDir baseDir++  relativeTempDir <- makeRelative initialPwd absoluteTempDir+  let subDirsRelativeToTop = map (relativeTempDir </>) subDirs+  let absoluteSubDirs = map (absoluteTempDir </>) subDirs++  let absoluteBaseDir = absoluteTempDir </> baseDir++  Turtle.writeTextFile (pathToTurtle $ absoluteTempDir </> yearDir </> [relfile|test-file.txt|]) (T.pack $ "The expected base dir is " ++ show absoluteBaseDir)++  assertSubDirsForDetermineBaseDir absoluteTempDir absoluteBaseDir subDirs+  assertSubDirsForDetermineBaseDir absoluteTempDir absoluteBaseDir absoluteSubDirs+  assertSubDirsForDetermineBaseDir initialPwd absoluteBaseDir subDirsRelativeToTop+  return ()++testDetermineBaseDir :: Test+testDetermineBaseDir = TestCase (+  do+    initialPwd <- getCurrentDir+    let tmpbase = initialPwd </> [reldir|test|] </> [reldir|tmp|]+    createDirIfMissing True tmpbase+    withTempDir tmpbase "hlflowtest" $ testBaseDirWithTempDir initialPwd+  )++tests :: Test+tests = TestList [testDetermineBaseDir]
test/CSVImport/Integration.hs view
@@ -5,24 +5,27 @@  import Test.HUnit import Turtle-import Prelude hiding (FilePath) import qualified Data.Map.Strict as Map import qualified Data.List as List (sort)  import TestHelpers import Hledger.Flow.Common+import Hledger.Flow.PathHelpers import Control.Concurrent.STM  testExtraIncludesForFile :: Test testExtraIncludesForFile = TestCase (   sh (       do-        tmpdir <- using (mktempdir "." "hlflow")-        let importedJournals = map (tmpdir </>) journalFiles :: [FilePath]+        currentDir <- pwd+        tmpdir <- using (mktempdir currentDir "hlflow")+        tmpdirAbsPath <- fromTurtleAbsDir tmpdir++        let importedJournals = map (tmpdir </>) journalFiles :: [TurtlePath]         let accountDir = "import/john/bogartbank/savings"         let opening = tmpdir </> accountDir </> "2017-opening.journal"         let closing = tmpdir </> accountDir </> "2017-closing.journal"-        let hidden = map (tmpdir </>) hiddenFiles :: [FilePath]+        let hidden = map (tmpdir </>) hiddenFiles :: [TurtlePath]         touchAll $ importedJournals ++ hidden          let accountInclude = tmpdir </> accountDir </> "2017-include.journal"@@ -30,18 +33,18 @@          ch <- liftIO newTChanIO -        extraOpening1 <- liftIO $ extraIncludesForFile (defaultOpts tmpdir) ch accountInclude ["opening.journal"] [] []+        extraOpening1 <- liftIO $ extraIncludesForFile (defaultOpts tmpdirAbsPath) ch accountInclude ["opening.journal"] [] []         liftIO $ assertEqual "The opening journal should not be included when it is not on disk" expectedEmpty extraOpening1 -        extraClosing1 <- liftIO $ extraIncludesForFile (defaultOpts tmpdir) ch accountInclude ["closing.journal"] [] []+        extraClosing1 <- liftIO $ extraIncludesForFile (defaultOpts tmpdirAbsPath) ch accountInclude ["closing.journal"] [] []         liftIO $ assertEqual "The closing journal should not be included when it is not on disk" expectedEmpty extraClosing1          touchAll [opening, closing] -        extraOpening2 <- liftIO $ extraIncludesForFile (defaultOpts tmpdir) ch accountInclude ["opening.journal"] [] []+        extraOpening2 <- liftIO $ extraIncludesForFile (defaultOpts tmpdirAbsPath) ch accountInclude ["opening.journal"] [] []         liftIO $ assertEqual "The opening journal should be included when it is on disk" [(accountInclude, [opening])] extraOpening2 -        extraClosing2 <- liftIO $ extraIncludesForFile (defaultOpts tmpdir) ch accountInclude ["closing.journal"] [] []+        extraClosing2 <- liftIO $ extraIncludesForFile (defaultOpts tmpdirAbsPath) ch accountInclude ["closing.journal"] [] []         liftIO $ assertEqual "The closing journal should be included when it is on disk" [(accountInclude, [closing])] extraClosing2      )) @@ -49,8 +52,11 @@ testExtraIncludesPrices = TestCase (   sh (       do-        tmpdir <- using (mktempdir "." "hlflow")-        let importedJournals = map (tmpdir </>) journalFiles :: [FilePath]+        currentDir <- pwd+        tmpdir <- using (mktempdir currentDir "hlflow")+        tmpdirAbsPath <- fromTurtleAbsDir tmpdir++        let importedJournals = map (tmpdir </>) journalFiles :: [TurtlePath]         touchAll $ importedJournals          let priceFile = "prices" </> "2020" </> "prices.journal"@@ -60,13 +66,13 @@          ch <- liftIO newTChanIO -        price1 <- liftIO $ extraIncludesForFile (defaultOpts tmpdir) ch includeFile [] [] ["prices.journal"]+        price1 <- liftIO $ extraIncludesForFile (defaultOpts tmpdirAbsPath) ch includeFile [] [] ["prices.journal"]         liftIO $ assertEqual "The price file should not be included when it is not on disk" expectedEmpty price1          touchAll [tmpdir </> priceFile]         let expectedPricePath = tmpdir </> "import" </> ".." </> priceFile -        price2 <- liftIO $ extraIncludesForFile (defaultOpts tmpdir) ch includeFile [] [] ["prices.journal"]+        price2 <- liftIO $ extraIncludesForFile (defaultOpts tmpdirAbsPath) ch includeFile [] [] ["prices.journal"]         liftIO $ assertEqual "The price file should be included when it is on disk" [(includeFile, [expectedPricePath])] price2      )) @@ -74,7 +80,10 @@ testIncludesPrePost = TestCase (   sh (       do-        tmpdir <- using (mktempdir "." "hlflow")+        currentDir <- pwd+        tmpdir <- using (mktempdir currentDir "hlflow")+        tmpdirAbsPath <- fromTurtleAbsDir tmpdir+         let ownerDir = tmpdir </> "import" </> "john"         let includeFile = ownerDir </> "2019-include.journal"         let pre  = ownerDir </> "_manual_" </> "2019" </> "pre-import.journal"@@ -85,7 +94,7 @@                                                     ownerDir </> "bank2" </> "2019-include.journal"]          ch <- liftIO newTChanIO-        fileMap <- liftIO $ toIncludeFiles (defaultOpts tmpdir) ch includeMap+        fileMap <- liftIO $ toIncludeFiles (defaultOpts tmpdirAbsPath) ch includeMap         let expectedText = includePreamble <> "\n"               <> "!include _manual_/2019/pre-import.journal\n"               <> "!include bank1/2019-include.journal\n"@@ -99,7 +108,10 @@ testIncludesOpeningClosing = TestCase (   sh (       do-        tmpdir <- using (mktempdir "." "hlflow")+        currentDir <- pwd+        tmpdir <- using (mktempdir currentDir "hlflow")+        tmpdirAbsPath <- fromTurtleAbsDir tmpdir+         let ownerDir = tmpdir </> "import/john"         let accountDir = ownerDir </> "bank1" </> "savings"         let includeFile = accountDir </> "2019-include.journal"@@ -110,7 +122,7 @@         let includeMap = Map.singleton includeFile [accountDir </> "3-journal" </> "2019" </> "2019-01-30.journal"]          ch <- liftIO newTChanIO-        fileMap <- liftIO $ toIncludeFiles (defaultOpts tmpdir) ch includeMap+        fileMap <- liftIO $ toIncludeFiles (defaultOpts tmpdirAbsPath) ch includeMap         let expectedText = includePreamble <> "\n"               <> "!include 2019-opening.journal\n"               <> "!include 3-journal/2019/2019-01-30.journal\n"@@ -123,7 +135,10 @@ testIncludesPrices = TestCase (   sh (       do-        tmpdir <- using (mktempdir "." "hlflow")+        currentDir <- pwd+        tmpdir <- using (mktempdir currentDir "hlflow")+        tmpdirAbsPath <- fromTurtleAbsDir tmpdir+         let importDir = tmpdir </> "import"         let includeFile = importDir </> "2020-include.journal"         let prices = tmpdir </> "prices" </> "2020" </> "prices.journal"@@ -134,7 +149,7 @@         let includeMap = Map.singleton includeFile [importDir </> "john" </> "2020-include.journal"]          ch <- liftIO newTChanIO-        fileMap <- liftIO $ toIncludeFiles (defaultOpts tmpdir) ch includeMap+        fileMap <- liftIO $ toIncludeFiles (defaultOpts tmpdirAbsPath) ch includeMap         let expectedText = includePreamble <> "\n"               <> "!include _manual_/2020/pre-import.journal\n"               <> "!include john/2020-include.journal\n"@@ -148,10 +163,13 @@ testWriteIncludeFiles = TestCase (   sh (       do-        tmpdir <- using (mktempdir "." "hlflow")-        let importedJournals = map (tmpdir </>) journalFiles :: [FilePath]-        let extras = map (tmpdir </>) extraFiles :: [FilePath]-        let hidden = map (tmpdir </>) hiddenFiles :: [FilePath]+        currentDir <- pwd+        tmpdir <- using (mktempdir currentDir "hlflow")+        tmpdirAbsPath <- fromTurtleAbsDir tmpdir++        let importedJournals = map (tmpdir </>) journalFiles :: [TurtlePath]+        let extras = map (tmpdir </>) extraFiles :: [TurtlePath]+        let hidden = map (tmpdir </>) hiddenFiles :: [TurtlePath]         touchAll $ importedJournals ++ extras ++ hidden          let jane1 = tmpdir </> "import/jane/bogartbank/checking/2018-include.journal"@@ -175,7 +193,7 @@                                 john1, john2, john3, john4, john5, john6, john7, john8]          ch <- liftIO newTChanIO-        reportedAsWritten <- liftIO $ groupAndWriteIncludeFiles (defaultOpts tmpdir) ch importedJournals+        reportedAsWritten <- liftIO $ groupAndWriteIncludeFiles (defaultOpts tmpdirAbsPath) ch importedJournals         liftIO $ assertEqual "groupAndWriteIncludeFiles should return which files it wrote" expectedIncludes reportedAsWritten          let allYears = [tmpdir </> "import/jane/bogartbank/checking/all-years.journal",
test/CSVImport/Unit.hs view
@@ -1,21 +1,23 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedLists #-}+{-# LANGUAGE QuasiQuotes #-}  module CSVImport.Unit where  import Test.HUnit+import Path import Turtle-import Prelude hiding (FilePath) import qualified Data.Map.Strict as Map import Control.Concurrent.STM  import TestHelpers+import Hledger.Flow.PathHelpers (TurtlePath) import Hledger.Flow.Common  import Data.Either import qualified Data.Text as T -groupedJaneBogart :: Map.Map FilePath [FilePath]+groupedJaneBogart :: Map.Map TurtlePath [TurtlePath] groupedJaneBogart = [   ("./import/jane/bogartbank/checking/2018-include.journal",    ["import/jane/bogartbank/checking/3-journal/2018/2018-12-30.journal"]),@@ -26,7 +28,7 @@   ("./import/jane/bogartbank/savings/2018-include.journal",    ["import/jane/bogartbank/savings/3-journal/2018/2018-01-30.journal"])] -groupedJaneOther :: Map.Map FilePath [FilePath]+groupedJaneOther :: Map.Map TurtlePath [TurtlePath] groupedJaneOther = [   ("./import/jane/otherbank/creditcard/2017-include.journal",    ["import/jane/otherbank/creditcard/3-journal/2017/2017-12-30.journal"]),@@ -37,7 +39,7 @@   ("./import/jane/otherbank/investments/2019-include.journal",    ["import/jane/otherbank/investments/3-journal/2019/2019-01-30.journal"])] -groupedJohnBogart :: Map.Map FilePath [FilePath]+groupedJohnBogart :: Map.Map TurtlePath [TurtlePath] groupedJohnBogart = [   ("./import/john/bogartbank/checking/2018-include.journal",    ["import/john/bogartbank/checking/3-journal/2018/2018-11-30.journal",@@ -53,7 +55,7 @@    ["import/john/bogartbank/savings/3-journal/2018/2018-02-30.journal",     "import/john/bogartbank/savings/3-journal/2018/2018-01-30.journal"])] -groupedJohnOther :: Map.Map FilePath [FilePath]+groupedJohnOther :: Map.Map TurtlePath [TurtlePath] groupedJohnOther = [   ("./import/john/otherbank/creditcard/2017-include.journal",    ["import/john/otherbank/creditcard/3-journal/2017/2017-12-30.journal"]),@@ -64,7 +66,7 @@   ("./import/john/otherbank/investments/2019-include.journal",    ["import/john/otherbank/investments/3-journal/2019/2019-01-30.journal"])] -groupedIncludeFiles :: Map.Map FilePath [FilePath]+groupedIncludeFiles :: Map.Map TurtlePath [TurtlePath] groupedIncludeFiles = groupedJaneBogart <> groupedJaneOther <>                       groupedJohnBogart <> groupedJohnOther @@ -99,21 +101,21 @@ testGroupIncludeFilesTinySet = TestCase (   do     let journals1 = [   "import/jane/bogartbank/savings/3-journals/2017/2017-12-30.journal"]-    let expected1 = [("./import/jane/bogartbank/savings/2017-include.journal", journals1)] :: Map.Map FilePath [FilePath]+    let expected1 = [("./import/jane/bogartbank/savings/2017-include.journal", journals1)] :: Map.Map TurtlePath [TurtlePath]     let expectedAllYears1 = [("./import/jane/bogartbank/savings/all-years.journal", ["./import/jane/bogartbank/savings/2017-include.journal"])]     let (group1, allYears1) = groupIncludeFiles journals1     assertEqual "groupIncludeFiles small allYears 1" expectedAllYears1 allYears1     assertEqual "groupIncludeFiles small set 1" expected1 group1 -    let journals2 = [(fst . head . Map.toList) expected1] :: [FilePath]-    let expected2 = [("./import/jane/bogartbank/2017-include.journal", journals2)] :: Map.Map FilePath [FilePath]+    let journals2 = [(fst . head . Map.toList) expected1] :: [TurtlePath]+    let expected2 = [("./import/jane/bogartbank/2017-include.journal", journals2)] :: Map.Map TurtlePath [TurtlePath]     let expectedAllYears2 = [("./import/jane/bogartbank/all-years.journal", ["./import/jane/bogartbank/2017-include.journal"])]     let (group2, allYears2) = groupIncludeFiles journals2     assertEqual "groupIncludeFiles small allYears 2" expectedAllYears2 allYears2     assertEqual "groupIncludeFiles small set 2" expected2 group2 -    let journals3 = [(fst . head . Map.toList) expected2] :: [FilePath]-    let expected3 = [("./import/jane/2017-include.journal", journals3)] :: Map.Map FilePath [FilePath]+    let journals3 = [(fst . head . Map.toList) expected2] :: [TurtlePath]+    let expected3 = [("./import/jane/2017-include.journal", journals3)] :: Map.Map TurtlePath [TurtlePath]     let expectedAllYears3 = [("./import/jane/all-years.journal", ["./import/jane/2017-include.journal"])]     let (group3, allYears3) = groupIncludeFiles journals3     assertEqual "groupIncludeFiles small allYears 3" expectedAllYears3 allYears3@@ -420,7 +422,7 @@            "!include import/john/bogartbank/savings/3-journal/2018/2018-02-30.journal\n")]      ch <- newTChanIO-    txt <- toIncludeFiles (defaultOpts ".") ch groupedJohnBogart+    txt <- toIncludeFiles (defaultOpts [absdir|/|]) ch groupedJohnBogart     assertEqual "Convert a grouped map of paths, to a map with text contents for each file" expected txt)  tests :: Test
test/Common/Integration.hs view
@@ -5,11 +5,10 @@  import Test.HUnit import Turtle-import Prelude hiding (FilePath) import qualified Data.List as List (sort)-import qualified Data.Text as T  import TestHelpers+import Hledger.Flow.PathHelpers (TurtlePath) import Hledger.Flow.Common  testHiddenFiles :: Test@@ -19,9 +18,9 @@         let tmpbase = "." </> "test" </> "tmp"         mktree tmpbase         tmpdir <- using (mktempdir tmpbase "hlflowtest")-        let tmpJournals = map (tmpdir </>) journalFiles :: [FilePath]-        let tmpExtras = map (tmpdir </>) extraFiles :: [FilePath]-        let tmpHidden = map (tmpdir </>) hiddenFiles :: [FilePath]+        let tmpJournals = map (tmpdir </>) journalFiles :: [TurtlePath]+        let tmpExtras = map (tmpdir </>) extraFiles :: [TurtlePath]+        let tmpHidden = map (tmpdir </>) hiddenFiles :: [TurtlePath]         let onDisk = List.sort $ tmpJournals ++ tmpExtras ++ tmpHidden         touchAll onDisk         filtered <- (fmap List.sort) $ shellToList $ onlyFiles $ select onDisk@@ -30,113 +29,6 @@      )   ) -assertSubDirsForDetermineBaseDir :: FilePath -> FilePath -> [FilePath] -> IO ()-assertSubDirsForDetermineBaseDir initialPwd expectedBaseDir importDirs = do-  _ <- sequence $ map (assertDetermineBaseDir initialPwd expectedBaseDir) importDirs-  return ()--assertDetermineBaseDir :: FilePath -> FilePath -> FilePath -> IO ()-assertDetermineBaseDir initialPwd expectedBaseDir subDir = do-  cd initialPwd-  (bd1, runDir1) <- determineBaseDir $ Just subDir-  assertFindTestFileUsingRundir bd1 runDir1--  cd subDir-  (bd2, runDir2) <- determineBaseDir Nothing-  assertFindTestFileUsingRundir bd2 runDir2--  (bd3, runDir3) <- determineBaseDir $ Just "."-  assertFindTestFileUsingRundir bd3 runDir3--  (bd4, runDir4) <- determineBaseDir $ Just "./"-  assertFindTestFileUsingRundir bd4 runDir4--  cd initialPwd-  let msg = format ("determineBaseDir searches from pwd upwards until it finds a dir containing 'import' - "%fp) subDir-  _ <- sequence $ map (assertEqual (T.unpack msg) expectedBaseDir) [bd1, bd2, bd3, bd4]-  return ()--assertFindTestFileUsingRundir :: FilePath -> FilePath -> IO ()-assertFindTestFileUsingRundir baseDir runDir = do-  let absRunDir = baseDir </> runDir-  let dirTestMsg = format ("Directories should end with a slash: "%fp)-  _ <- sequence $ map (\dir -> assertEqual (T.unpack $ dirTestMsg dir) (forceTrailingSlash dir) dir) [baseDir, runDir, absRunDir]--  found <- single $ fmap head $ shellToList $ find (has "test-file.txt") absRunDir-  fileContents <- readTextFile found-  assertEqual "We should find our test file by searching from the returned runDir" (format ("The expected base dir is "%fp) baseDir) fileContents--assertCurrentDirVariations :: FilePath -> FilePath -> IO ()-assertCurrentDirVariations absoluteTempDir bdRelativeToTempDir = do-  let absBaseDirNoTrailingSlash = absoluteTempDir </> bdRelativeToTempDir-  let absBaseDirWithTrailingSlash = forceTrailingSlash absBaseDirNoTrailingSlash-  assertEqual "Test the test: with and without slashes should really be that" '/' (T.last $ format fp absBaseDirWithTrailingSlash)-  assertBool "Test the test: with and without slashes should really be that" ('/' /= (T.last $ format fp absBaseDirNoTrailingSlash))--  cd absBaseDirNoTrailingSlash-  (bd1, runDir1) <- liftIO $ determineBaseDir Nothing-  (bd2, runDir2) <- liftIO $ determineBaseDir $ Just "."-  (bd3, runDir3) <- liftIO $ determineBaseDir $ Just "./"-  (bd4, runDir4) <- liftIO $ determineBaseDir $ Just absBaseDirNoTrailingSlash-  (bd5, runDir5) <- liftIO $ determineBaseDir $ Just absBaseDirWithTrailingSlash--  let msg label dir = format ("When pwd is the base dir, determineBaseDir returns the same "%s%", regardless of the input variation. "%s%":  "%fp) label label dir-  _ <- sequence $ map (\dir -> assertEqual (T.unpack $ msg "baseDir" dir) (forceTrailingSlash dir) dir) [bd1, bd2, bd3, bd4,bd5]-  _ <- sequence $ map (\dir -> assertEqual (T.unpack $ msg "runDir" dir) "./" dir) [runDir1, runDir2, runDir3, runDir4, runDir5]-  -- _ <- sequence $ map (\dir -> assertEqual (T.unpack $ msg "runDir" dir) "./" dir) [runDir2, runDir3, runDir4, runDir5]-  return ()--testDetermineBaseDir :: Test-testDetermineBaseDir = TestCase (-  sh (-      do-        error1 <- liftIO $ determineBaseDirFromAbsoluteStartDir "/path/to/dir"-        liftIO $ assertEqual "determineBaseDir produces an error message when given a non-existant dir" (Left "The provided directory does not exist: /path/to/dir") error1--        initialPwd <- fmap forceTrailingSlash pwd-        let tmpbase = initialPwd </> "test" </> "tmp"-        mktree tmpbase-        absoluteTempDir <- fmap forceTrailingSlash $ using (mktempdir tmpbase "hlflowtest")--        let unrelatedDir = absoluteTempDir </> "unrelated"-        mkdir unrelatedDir--        bdUnrelated <- liftIO $ determineBaseDirFromAbsoluteStartDir unrelatedDir-        liftIO $ assertEqual "determineBaseDir produces an error message when it cannot find a baseDir" (Left $ errorMessageBaseDir unrelatedDir) bdUnrelated--        let baseDir = "bd1"-        let importDir = baseDir </> "import"-        let ownerDir = importDir </> "john"-        let bankDir = ownerDir </> "mybank"-        let accDir = bankDir </> "myacc"-        let inDir = accDir </> "1-in"-        let yearDir = inDir </> "2019"-        let subDirs = [yearDir, inDir, accDir, bankDir, ownerDir, importDir, baseDir]--        mktree $ absoluteTempDir </> yearDir--        let fictionalDir = absoluteTempDir </> ownerDir </> "fictionalDir"-        errorSub <- liftIO $ determineBaseDirFromAbsoluteStartDir fictionalDir-        liftIO $ assertEqual "determineBaseDir produces an error message when given a non-existant subdir of a valid basedir" (Left $ format ("The provided directory does not exist: "%fp) fictionalDir) errorSub--        let relativeTempDir = foldl1 mappend (dropWhile (\el -> el `elem` (splitDirectories initialPwd)) (splitDirectories absoluteTempDir))---        let subDirsRelativeToTop = map (relativeTempDir </>) subDirs-        let absoluteSubDirs = map (absoluteTempDir </>) subDirs--        let absoluteBaseDir = forceTrailingSlash $ absoluteTempDir </> baseDir--        liftIO $ assertCurrentDirVariations absoluteTempDir baseDir--        liftIO $ writeTextFile (absoluteTempDir </> yearDir </> "test-file.txt") $ format ("The expected base dir is "%fp) absoluteBaseDir--        liftIO $ assertSubDirsForDetermineBaseDir absoluteTempDir absoluteBaseDir subDirs-        liftIO $ assertSubDirsForDetermineBaseDir absoluteTempDir absoluteBaseDir absoluteSubDirs-        liftIO $ assertSubDirsForDetermineBaseDir initialPwd absoluteBaseDir subDirsRelativeToTop-     )-  )- testFilterPaths :: Test testFilterPaths = TestCase (   sh (@@ -144,9 +36,9 @@         let tmpbase = "." </> "test" </> "tmp"         mktree tmpbase         tmpdir <- using (mktempdir tmpbase "hlflowtest")-        let tmpJournals = map (tmpdir </>) journalFiles :: [FilePath]-        let tmpExtras = map (tmpdir </>) extraFiles :: [FilePath]-        let tmpHidden = map (tmpdir </>) hiddenFiles :: [FilePath]+        let tmpJournals = map (tmpdir </>) journalFiles :: [TurtlePath]+        let tmpExtras = map (tmpdir </>) extraFiles :: [TurtlePath]+        let tmpHidden = map (tmpdir </>) hiddenFiles :: [TurtlePath]         let onDisk = List.sort $ tmpJournals ++ tmpExtras ++ tmpHidden         touchAll onDisk @@ -159,4 +51,4 @@   )  tests :: Test-tests = TestList [testDetermineBaseDir, testHiddenFiles, testFilterPaths]+tests = TestList [testHiddenFiles, testFilterPaths]
test/Common/Unit.hs view
@@ -4,8 +4,8 @@ module Common.Unit where  import Test.HUnit-import Prelude hiding (FilePath) +import Hledger.Flow.BaseDir (relativeToBase') import Hledger.Flow.Common  testShowCmdArgs :: Test
test/Spec.hs view
@@ -5,15 +5,15 @@  import Test.HUnit import Turtle-import Prelude hiding (FilePath)  import qualified Common.Unit import qualified Common.Integration+import qualified BaseDir.Integration import qualified CSVImport.Unit import qualified CSVImport.Integration  tests :: Test-tests = TestList [Common.Unit.tests, Common.Integration.tests, CSVImport.Unit.tests, CSVImport.Integration.tests]+tests = TestList [Common.Unit.tests, Common.Integration.tests, BaseDir.Integration.tests, CSVImport.Unit.tests, CSVImport.Integration.tests]  main :: IO Counts main = do
test/TestHelpers.hs view
@@ -1,16 +1,18 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedLists #-}+{-# LANGUAGE QuasiQuotes #-}  module TestHelpers where +import Path import Turtle-import Prelude hiding (FilePath)  import qualified Hledger.Flow.Types as FlowTypes+import Hledger.Flow.PathHelpers (TurtlePath) import Hledger.Flow.Common import Hledger.Flow.RuntimeOptions -inputJohnBogart :: [FilePath]+inputJohnBogart :: [TurtlePath] inputJohnBogart = [   "import/john/bogartbank/savings/1-in/2017/2017-11-30.csv",   "import/john/bogartbank/savings/1-in/2017/2017-12-30.csv",@@ -22,52 +24,63 @@   "import/john/bogartbank/checking/1-in/2019/2019-01-30.csv",   "import/john/bogartbank/checking/1-in/2019/2019-02-30.csv"] -inputJohnOther :: [FilePath]+inputJohnOther :: [TurtlePath] inputJohnOther = [   "import/john/otherbank/creditcard/1-in/2017/2017-12-30.csv",   "import/john/otherbank/creditcard/1-in/2018/2018-01-30.csv",   "import/john/otherbank/investments/1-in/2018/2018-12-30.csv",   "import/john/otherbank/investments/1-in/2019/2019-01-30.csv"] -inputJaneBogart :: [FilePath]+inputJaneBogart :: [TurtlePath] inputJaneBogart = [   "import/jane/bogartbank/savings/1-in/2017/2017-12-30.csv",   "import/jane/bogartbank/savings/1-in/2018/2018-01-30.csv",   "import/jane/bogartbank/checking/1-in/2018/2018-12-30.csv",   "import/jane/bogartbank/checking/1-in/2019/2019-01-30.csv"] -inputJaneOther :: [FilePath]+inputJaneOther :: [TurtlePath] inputJaneOther = [   "import/jane/otherbank/creditcard/1-in/2017/2017-12-30.csv",   "import/jane/otherbank/creditcard/1-in/2018/2018-01-30.csv",   "import/jane/otherbank/investments/1-in/2018/2018-12-30.csv",   "import/jane/otherbank/investments/1-in/2019/2019-01-30.csv"] -inputFiles :: [FilePath]+inputFiles :: [TurtlePath] inputFiles = inputJohnBogart <> inputJohnOther <> inputJaneBogart <> inputJaneOther -journalFiles :: [FilePath]+journalFiles :: [TurtlePath] journalFiles = toJournals inputFiles -extraFiles :: [FilePath]+extraFiles :: [TurtlePath] extraFiles = ["import/john/bogartbank/savings/2017-opening.journal"] -hiddenFiles :: [FilePath]+hiddenFiles :: [TurtlePath] hiddenFiles = [".hiddenfile", "checking/.DS_Store", "import/john/bogartbank/savings/1-in/.anotherhiddenfile", "import/john/bogartbank/checking/1-in/2018/.hidden"]  defaultHlInfo :: FlowTypes.HledgerInfo defaultHlInfo = FlowTypes.HledgerInfo "/path/to/hledger" "1.2.3" -defaultOpts :: FilePath -> RuntimeOptions-defaultOpts bd = RuntimeOptions bd "./" True versionInfo' defaultHlInfo systemInfo False False False+defaultOpts :: FlowTypes.BaseDir -> RuntimeOptions+defaultOpts bd = RuntimeOptions {+    baseDir = bd+  , importRunDir = [reldir|./|]+  , useRunDir = True+  , onlyNewFiles = False+  , hfVersion = versionInfo'+  , hledgerInfo = defaultHlInfo+  , sysInfo = systemInfo+  , verbose = False+  , showOptions = False+  , sequential = False+} -toJournals :: [FilePath] -> [FilePath]+toJournals :: [TurtlePath] -> [TurtlePath] toJournals = map (changePathAndExtension "3-journal" "journal") -touchAll :: [FilePath] -> Shell ()+touchAll :: [TurtlePath] -> Shell () touchAll = foldl (\acc file -> acc <> superTouch file) (return ()) -superTouch :: FilePath -> Shell ()+superTouch :: TurtlePath -> Shell () superTouch file = do   mktree $ directory file   touch file