optionsgeddon.. port to cmdargs and a fully modal cli
This commit is contained in:
		
							parent
							
								
									c3954cad43
								
							
						
					
					
						commit
						059825a9b2
					
				| @ -18,78 +18,49 @@ import Data.Maybe | |||||||
| import Data.Ord | import Data.Ord | ||||||
| import Data.Tree | import Data.Tree | ||||||
| import Graphics.Rendering.Chart | import Graphics.Rendering.Chart | ||||||
| import Safe (readDef) |  | ||||||
| import System.Console.GetOpt |  | ||||||
| import System.Exit (exitFailure) | import System.Exit (exitFailure) | ||||||
|  | import Text.Printf | ||||||
| 
 | 
 | ||||||
| import Hledger | import Hledger | ||||||
| import Prelude hiding (putStr, putStrLn) | import Hledger.Cli hiding (progname,progversion) | ||||||
| import Hledger.Utils.UTF8 (putStr, putStrLn) | import Prelude hiding (putStrLn) | ||||||
| import Hledger.Cli.Options | import Hledger.Utils.UTF8 (putStrLn) | ||||||
| import Hledger.Cli.Utils (withJournalDo) |  | ||||||
| import Hledger.Cli.Version |  | ||||||
| 
 | 
 | ||||||
| 
 | import Hledger.Chart.Options | ||||||
| progname_chart = progname_cli ++ "-chart" |  | ||||||
| 
 |  | ||||||
| defchartoutput   = "hledger.png" |  | ||||||
| defchartitems    = 10 |  | ||||||
| defchartsize     = "600x400" |  | ||||||
| 
 |  | ||||||
| options_chart :: [OptDescr Opt] |  | ||||||
| options_chart = [ |  | ||||||
|   Option "o" ["output"] (ReqArg ChartOutput "FILE")       ("output filename (default: "++defchartoutput++")") |  | ||||||
|  ,Option ""  ["items"]  (ReqArg ChartItems "N")           ("number of accounts to show (default: "++show defchartitems++")") |  | ||||||
|  ,Option ""  ["size"]   (ReqArg ChartSize "WIDTHxHEIGHT") ("image size (default: "++defchartsize++")") |  | ||||||
|  ] |  | ||||||
| 
 |  | ||||||
| usage_preamble_chart = |  | ||||||
|   "Usage: hledger-chart [OPTIONS] [PATTERNS]\n" ++ |  | ||||||
|   "\n" ++ |  | ||||||
|   "Reads your ~/.journal file, or another specified by $LEDGER or -f, and\n" ++ |  | ||||||
|   "generates simple pie chart images.\n" ++ |  | ||||||
|   "\n" |  | ||||||
| 
 |  | ||||||
| usage_options_chart = usageInfo "hledger-chart options:" options_chart ++ "\n" |  | ||||||
| 
 |  | ||||||
| usage_chart = concat [ |  | ||||||
|              usage_preamble_chart |  | ||||||
|             ,usage_options_chart |  | ||||||
|             ,usage_options_cli |  | ||||||
|             ,usage_postscript_cli |  | ||||||
|             ] |  | ||||||
| 
 | 
 | ||||||
| main :: IO () | main :: IO () | ||||||
| main = do | main = do | ||||||
|   (opts, args) <- parseArgumentsWith $ options_cli++options_chart |   opts <- getHledgerChartOpts | ||||||
|   run opts args |   when (debug_ $ cliopts_ opts) $ printf "%s\n" progversion >> printf "opts: %s\n" (show opts) | ||||||
|  |   runWith opts | ||||||
|  | 
 | ||||||
|  | runWith :: ChartOpts -> IO () | ||||||
|  | runWith opts = run opts | ||||||
|     where |     where | ||||||
|       run opts args |       run opts | ||||||
|        | Help `elem` opts             = putStr usage_chart |           | "help" `in_` (rawopts_ $ cliopts_ opts)            = printModeHelpAndExit chartmode | ||||||
|        | Version `elem` opts          = putStrLn $ progversionstr progname_chart |           | "binary-filename" `in_` (rawopts_ $ cliopts_ opts) = putStrLn (binaryfilename progname) | ||||||
|        | BinaryFilename `elem` opts   = putStrLn $ binaryfilename progname_chart |           | otherwise                                          = withJournalDo' opts chart | ||||||
|        | otherwise                    = withJournalDo opts args "chart" chart | 
 | ||||||
|  | withJournalDo' :: ChartOpts -> (ChartOpts -> Journal -> IO ()) -> IO () | ||||||
|  | withJournalDo' opts cmd = do | ||||||
|  |   journalFilePathFromOpts (cliopts_ opts) >>= readJournalFile Nothing >>= | ||||||
|  |     either error' (cmd opts . journalApplyAliases (aliasesFromOpts $ cliopts_ opts)) | ||||||
| 
 | 
 | ||||||
| -- | Generate an image with the pie chart and write it to a file | -- | Generate an image with the pie chart and write it to a file | ||||||
| chart :: [Opt] -> [String] -> Journal -> IO () | chart :: ChartOpts -> Journal -> IO () | ||||||
| chart opts args j = do | chart opts j = do | ||||||
|   d <- getCurrentDay |   d <- getCurrentDay | ||||||
|   if null $ jtxns j |   if null $ jtxns j | ||||||
|    then putStrLn "This journal has no transactions, can't make a chart." >> exitFailure |    then putStrLn "This journal has no transactions, can't make a chart." >> exitFailure | ||||||
|    else do |    else do | ||||||
|      let chart = genPie opts (optsToFilterSpec opts args d) j |      let chart = genPie opts (optsToFilterSpec ropts d) j | ||||||
|      renderableToPNGFile (toRenderable chart) w h filename |      renderableToPNGFile (toRenderable chart) w h filename | ||||||
|      return () |      return () | ||||||
|       where |       where | ||||||
|         filename = getOption opts ChartOutput defchartoutput |         filename = chart_output_ opts | ||||||
|         (w,h) = parseSize $ getOption opts ChartSize defchartsize |         (w,h) = parseSize $ chart_size_ opts | ||||||
| 
 |         ropts = reportopts_ $ cliopts_ opts | ||||||
| -- | Extract string option value from a list of options or use the default |  | ||||||
| getOption :: [Opt] -> (String->Opt) -> String -> String |  | ||||||
| getOption opts opt def =  |  | ||||||
|     case reverse $ optValuesForConstructor opt opts of |  | ||||||
|         [] -> def |  | ||||||
|         x:_ -> x |  | ||||||
| 
 | 
 | ||||||
| -- | Parse image size from a command-line option | -- | Parse image size from a command-line option | ||||||
| parseSize :: String -> (Int,Int) | parseSize :: String -> (Int,Int) | ||||||
| @ -99,26 +70,28 @@ parseSize str = (read w, read h) | |||||||
|     (w,_:h) = splitAt x str |     (w,_:h) = splitAt x str | ||||||
| 
 | 
 | ||||||
| -- | Generate pie chart | -- | Generate pie chart | ||||||
| genPie :: [Opt] -> FilterSpec -> Journal -> PieLayout | genPie :: ChartOpts -> FilterSpec -> Journal -> PieLayout | ||||||
| genPie opts filterspec j = defaultPieLayout { pie_background_ = solidFillStyle $ opaque $ white | genPie opts filterspec j = defaultPieLayout { pie_background_ = solidFillStyle $ opaque $ white | ||||||
|                                             , pie_plot_ = pie_chart } |                                             , pie_plot_ = pie_chart } | ||||||
|     where |     where | ||||||
|       pie_chart = defaultPieChart { pie_data_ = map (uncurry accountPieItem) chartitems' |       pie_chart = defaultPieChart { pie_data_ = map (uncurry accountPieItem) chartitems | ||||||
|                                   , pie_start_angle_ = (-90) |                                   , pie_start_angle_ = (-90) | ||||||
|                                   , pie_colors_ = mkColours hue |                                   , pie_colors_ = mkColours hue | ||||||
|                                   , pie_label_style_ = defaultFontStyle{font_size_=12} |                                   , pie_label_style_ = defaultFontStyle{font_size_=12} | ||||||
|                                   } |                                   } | ||||||
|       chartitems' = debug "chart" $ top num samesignitems |       chartitems = debug "chart" $ top num samesignitems | ||||||
|       (samesignitems, sign) = sameSignNonZero rawitems |       (samesignitems, sign) = sameSignNonZero rawitems | ||||||
|       rawitems = debug "raw" $ flatten $ balances $ |       rawitems = debug "raw" $ flatten $ balances $ | ||||||
|                  ledgerAccountTree (fromMaybe 99999 $ depthFromOpts opts) $ journalToLedger filterspec j |                  ledgerAccountTree (fromMaybe 99999 $ depth_ ropts) $ journalToLedger filterspec j | ||||||
|       top n t = topn ++ [other] |       top n t = topn ++ [other] | ||||||
|           where |           where | ||||||
|             (topn,rest) = splitAt n $ reverse $ sortBy (comparing snd) t |             (topn,rest) = splitAt n $ reverse $ sortBy (comparing snd) t | ||||||
|             other = ("other", sum $ map snd rest) |             other = ("other", sum $ map snd rest) | ||||||
|       num = readDef (fromIntegral defchartitems) (getOption opts ChartItems (show defchartitems)) |       num = chart_items_ opts | ||||||
|       hue = if sign > 0 then red else green where (red, green) = (0, 110) |       hue = if sign > 0 then red else green where (red, green) = (0, 110) | ||||||
|       debug s = if Debug `elem` opts then ltrace s else id |       debug s = if debug_ copts then ltrace s else id | ||||||
|  |       copts = cliopts_ opts | ||||||
|  |       ropts = reportopts_ copts | ||||||
| 
 | 
 | ||||||
| -- | Select the nonzero items with same sign as the first, and make | -- | Select the nonzero items with same sign as the first, and make | ||||||
| -- them positive. Also return a 1 or -1 corresponding to the original sign. | -- them positive. Also return a 1 or -1 corresponding to the original sign. | ||||||
|  | |||||||
							
								
								
									
										68
									
								
								hledger-chart/Hledger/Chart/Options.hs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								hledger-chart/Hledger/Chart/Options.hs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,68 @@ | |||||||
|  | {-| | ||||||
|  | 
 | ||||||
|  | -} | ||||||
|  | 
 | ||||||
|  | module Hledger.Chart.Options | ||||||
|  | where | ||||||
|  | import Data.Maybe | ||||||
|  | import System.Console.CmdArgs | ||||||
|  | import System.Console.CmdArgs.Explicit | ||||||
|  | 
 | ||||||
|  | import Hledger.Cli hiding (progname,progversion) | ||||||
|  | import qualified Hledger.Cli (progname) | ||||||
|  | 
 | ||||||
|  | progname = Hledger.Cli.progname ++ "-chart" | ||||||
|  | progversion = progversionstr progname | ||||||
|  | 
 | ||||||
|  | defchartoutput   = "hledger.png" | ||||||
|  | defchartitems    = 10 | ||||||
|  | defchartsize     = "600x400" | ||||||
|  | 
 | ||||||
|  | chartflags = [ | ||||||
|  |   flagReq ["chart-output","o"]  (\s opts -> Right $ setopt "chart-output" s opts) "IMGFILE" ("output filename (default: "++defchartoutput++")") | ||||||
|  |  ,flagReq ["chart-items"]  (\s opts -> Right $ setopt "chart-items" s opts) "N" ("number of accounts to show (default: "++show defchartitems++")") | ||||||
|  |  ,flagReq ["chart-size"]  (\s opts -> Right $ setopt "chart-size" s opts) "WIDTHxHEIGHT" ("image size (default: "++defchartsize++")") | ||||||
|  |  ] | ||||||
|  |   | ||||||
|  | chartmode =  (mode "hledger-chart" [("command","chart")] | ||||||
|  |             "generate a pie chart image for the top account balances (of one sign only)" | ||||||
|  |             commandargsflag (chartflags++generalflags1)){ | ||||||
|  |              modeHelpSuffix=[ | ||||||
|  |                   -- "Reads your ~/.hledger.journal file, or another specified by $LEDGER_FILE or -f, and starts the full-window curses ui." | ||||||
|  |                  ] | ||||||
|  |            } | ||||||
|  | 
 | ||||||
|  | -- hledger-chart options, used in hledger-chart and above | ||||||
|  | data ChartOpts = ChartOpts { | ||||||
|  |      chart_output_ :: FilePath | ||||||
|  |     ,chart_items_ :: Int | ||||||
|  |     ,chart_size_ :: String | ||||||
|  |     ,cliopts_   :: CliOpts | ||||||
|  |  } deriving (Show) | ||||||
|  | 
 | ||||||
|  | defchartopts = ChartOpts | ||||||
|  |     def | ||||||
|  |     def | ||||||
|  |     def | ||||||
|  |     def | ||||||
|  | 
 | ||||||
|  | -- instance Default CliOpts where def = defcliopts | ||||||
|  | 
 | ||||||
|  | toChartOpts :: RawOpts -> IO ChartOpts | ||||||
|  | toChartOpts rawopts = do | ||||||
|  |   cliopts <- toCliOpts rawopts | ||||||
|  |   return defchartopts { | ||||||
|  |               chart_output_ = fromMaybe defchartoutput $ maybestringopt "debug-chart" rawopts | ||||||
|  |              ,chart_items_ = fromMaybe defchartitems $ maybeintopt "debug-items" rawopts | ||||||
|  |              ,chart_size_ = fromMaybe defchartsize $ maybestringopt "debug-size" rawopts | ||||||
|  |              ,cliopts_   = cliopts | ||||||
|  |              } | ||||||
|  | 
 | ||||||
|  | checkChartOpts :: ChartOpts -> IO ChartOpts | ||||||
|  | checkChartOpts opts = do | ||||||
|  |   checkCliOpts $ cliopts_ opts | ||||||
|  |   return opts | ||||||
|  | 
 | ||||||
|  | getHledgerChartOpts :: IO ChartOpts | ||||||
|  | getHledgerChartOpts = processArgs chartmode >>= return . decodeRawOpts >>= toChartOpts >>= checkChartOpts | ||||||
|  | 
 | ||||||
| @ -35,6 +35,7 @@ executable hledger-chart | |||||||
|                  ,hledger-lib == 0.15 |                  ,hledger-lib == 0.15 | ||||||
|                  -- ,HUnit |                  -- ,HUnit | ||||||
|                  ,base >= 3 && < 5 |                  ,base >= 3 && < 5 | ||||||
|  |                  ,cmdargs >= 0.7   && < 0.8 | ||||||
|                  ,containers |                  ,containers | ||||||
|                  -- ,csv |                  -- ,csv | ||||||
|                  -- ,directory |                  -- ,directory | ||||||
|  | |||||||
| @ -115,6 +115,9 @@ orDatesFrom (DateSpan a1 b1) (DateSpan a2 b2) = DateSpan a b | |||||||
| parsePeriodExpr :: Day -> String -> Either ParseError (Interval, DateSpan) | parsePeriodExpr :: Day -> String -> Either ParseError (Interval, DateSpan) | ||||||
| parsePeriodExpr refdate = parsewith (periodexpr refdate) | parsePeriodExpr refdate = parsewith (periodexpr refdate) | ||||||
| 
 | 
 | ||||||
|  | maybePeriod :: Day -> String -> Maybe (Interval,DateSpan) | ||||||
|  | maybePeriod refdate = either (const Nothing) Just . parsePeriodExpr refdate | ||||||
|  | 
 | ||||||
| -- | Show a DateSpan as a human-readable pseudo-period-expression string. | -- | Show a DateSpan as a human-readable pseudo-period-expression string. | ||||||
| dateSpanAsText :: DateSpan -> String | dateSpanAsText :: DateSpan -> String | ||||||
| dateSpanAsText (DateSpan Nothing Nothing)   = "all" | dateSpanAsText (DateSpan Nothing Nothing)   = "all" | ||||||
|  | |||||||
| @ -53,7 +53,7 @@ data Matcher = MatchAny                   -- ^ always match | |||||||
| 
 | 
 | ||||||
| -- | A query option changes a query's/report's behaviour and output in some way. | -- | A query option changes a query's/report's behaviour and output in some way. | ||||||
| 
 | 
 | ||||||
| -- XXX could use regular cli Opts ? | -- XXX could use regular CliOpts ? | ||||||
| data QueryOpt = QueryOptInAcctOnly AccountName  -- ^ show an account register focussed on this account | data QueryOpt = QueryOptInAcctOnly AccountName  -- ^ show an account register focussed on this account | ||||||
|               | QueryOptInAcct AccountName      -- ^ as above but include sub-accounts in the account register |               | QueryOptInAcct AccountName      -- ^ as above but include sub-accounts in the account register | ||||||
|            -- | QueryOptCostBasis      -- ^ show amounts converted to cost where possible |            -- | QueryOptCostBasis      -- ^ show amounts converted to cost where possible | ||||||
|  | |||||||
| @ -36,7 +36,7 @@ import Control.Monad.Error (ErrorT) | |||||||
| import Data.Time.Calendar | import Data.Time.Calendar | ||||||
| import Data.Time.LocalTime | import Data.Time.LocalTime | ||||||
| import Data.Tree | import Data.Tree | ||||||
| import Data.Typeable (Typeable) | import Data.Typeable | ||||||
| import qualified Data.Map as Map | import qualified Data.Map as Map | ||||||
| import System.Time (ClockTime) | import System.Time (ClockTime) | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -15,6 +15,8 @@ module Hledger.Read ( | |||||||
|        myJournal, |        myJournal, | ||||||
|        myTimelog, |        myTimelog, | ||||||
|        someamount, |        someamount, | ||||||
|  |        journalenvvar, | ||||||
|  |        journaldefaultfilename | ||||||
| ) | ) | ||||||
| where | where | ||||||
| import Control.Monad.Error | import Control.Monad.Error | ||||||
|  | |||||||
| @ -12,48 +12,34 @@ import Data.List | |||||||
| import Data.Maybe | import Data.Maybe | ||||||
| import Data.Time.Calendar | import Data.Time.Calendar | ||||||
| import Graphics.Vty | import Graphics.Vty | ||||||
| import Safe (headDef) | import Safe | ||||||
| import System.Console.GetOpt | import Text.Printf | ||||||
| 
 | 
 | ||||||
| import Hledger | import Hledger | ||||||
| import Prelude hiding (putStr, putStrLn) | import Hledger.Cli hiding (progname,progversion) | ||||||
| import Hledger.Utils.UTF8 (putStr, putStrLn) | import Hledger.Vty.Options | ||||||
| import Hledger.Cli | import Prelude hiding (putStrLn) | ||||||
|  | import Hledger.Utils.UTF8 (putStrLn) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| progname_vty = progname_cli ++ "-vty" |  | ||||||
| 
 |  | ||||||
| options_vty :: [OptDescr Opt] |  | ||||||
| options_vty = [ |  | ||||||
|  Option ""  ["debug-vty"]    (NoArg  DebugVty)      "run with no terminal output, showing console" |  | ||||||
|  ] |  | ||||||
| 
 |  | ||||||
| usage_preamble_vty = |  | ||||||
|   "Usage: hledger-vty [OPTIONS] [PATTERNS]\n" ++ |  | ||||||
|   "\n" ++ |  | ||||||
|   "Reads your ~/.journal file, or another specified by $LEDGER or -f, and\n" ++ |  | ||||||
|   "starts the full-window curses ui.\n" ++ |  | ||||||
|   "\n" |  | ||||||
| 
 |  | ||||||
| usage_options_vty = usageInfo "hledger-vty options:" options_vty ++ "\n" |  | ||||||
| 
 |  | ||||||
| usage_vty = concat [ |  | ||||||
|              usage_preamble_vty |  | ||||||
|             ,usage_options_vty |  | ||||||
|             ,usage_options_cli |  | ||||||
|             ,usage_postscript_cli |  | ||||||
|             ] |  | ||||||
| 
 |  | ||||||
| main :: IO () | main :: IO () | ||||||
| main = do | main = do | ||||||
|   (opts, args) <- parseArgumentsWith $ options_cli++options_vty |   opts <- getHledgerVtyOpts | ||||||
|   run opts args |   when (debug_ $ cliopts_ opts) $ printf "%s\n" progversion >> printf "opts: %s\n" (show opts) | ||||||
|  |   runWith opts | ||||||
|  | 
 | ||||||
|  | runWith :: VtyOpts -> IO () | ||||||
|  | runWith opts = run opts | ||||||
|     where |     where | ||||||
|       run opts args |       run opts | ||||||
|        | Help `elem` opts             = putStr usage_vty |           | "help" `in_` (rawopts_ $ cliopts_ opts)            = printModeHelpAndExit vtymode | ||||||
|        | Version `elem` opts          = putStrLn $ progversionstr progname_vty |           | "binary-filename" `in_` (rawopts_ $ cliopts_ opts) = putStrLn (binaryfilename progname) | ||||||
|        | BinaryFilename `elem` opts   = putStrLn $ binaryfilename progname_vty |           | otherwise                                          = withJournalDo' opts vty | ||||||
|        | otherwise                    = withJournalDo opts args "vty" vty | 
 | ||||||
|  | withJournalDo' :: VtyOpts -> (VtyOpts -> Journal -> IO ()) -> IO () | ||||||
|  | withJournalDo' opts cmd = do | ||||||
|  |   journalFilePathFromOpts (cliopts_ opts) >>= readJournalFile Nothing >>= | ||||||
|  |     either error' (cmd opts . journalApplyAliases (aliasesFromOpts $ cliopts_ opts)) | ||||||
| 
 | 
 | ||||||
| helpmsg = "(b)alance, (r)egister, (p)rint, (right) to drill down, (left) to back up, (q)uit" | helpmsg = "(b)alance, (r)egister, (p)rint, (right) to drill down, (left) to back up, (q)uit" | ||||||
| 
 | 
 | ||||||
| @ -62,10 +48,10 @@ instance Show Vty where show = const "a Vty" | |||||||
| -- | The application state when running the vty command. | -- | The application state when running the vty command. | ||||||
| data AppState = AppState { | data AppState = AppState { | ||||||
|      av :: Vty                   -- ^ the vty context |      av :: Vty                   -- ^ the vty context | ||||||
|     ,aw :: Int                  -- ^ window width |     ,aw :: Int                   -- ^ window width | ||||||
|     ,ah :: Int                  -- ^ window height |     ,ah :: Int                   -- ^ window height | ||||||
|     ,amsg :: String              -- ^ status message |     ,amsg :: String              -- ^ status message | ||||||
|     ,aopts :: [Opt]              -- ^ command-line opts |     ,aopts :: VtyOpts            -- ^ command-line opts | ||||||
|     ,aargs :: [String]           -- ^ command-line args at startup |     ,aargs :: [String]           -- ^ command-line args at startup | ||||||
|     ,ajournal :: Journal         -- ^ parsed journal |     ,ajournal :: Journal         -- ^ parsed journal | ||||||
|     ,abuf :: [String]            -- ^ lines of the current buffered view |     ,abuf :: [String]            -- ^ lines of the current buffered view | ||||||
| @ -89,19 +75,19 @@ data Screen = BalanceScreen     -- ^ like hledger balance, shows accounts | |||||||
|               deriving (Eq,Show) |               deriving (Eq,Show) | ||||||
| 
 | 
 | ||||||
| -- | Run the vty (curses-style) ui. | -- | Run the vty (curses-style) ui. | ||||||
| vty :: [Opt] -> [String] -> Journal -> IO () | vty :: VtyOpts -> Journal -> IO () | ||||||
| vty opts args j = do | vty opts j = do | ||||||
|   v <- mkVty |   v <- mkVty | ||||||
|   DisplayRegion w h <- display_bounds $ terminal v |   DisplayRegion w h <- display_bounds $ terminal v | ||||||
|   d <-  getCurrentDay |   d <-  getCurrentDay | ||||||
|   let a = enter d BalanceScreen args |   let a = enter d BalanceScreen (patterns_ $ reportopts_ $ cliopts_ opts) | ||||||
|           AppState { |           AppState { | ||||||
|                   av=v |                   av=v | ||||||
|                  ,aw=fromIntegral w |                  ,aw=fromIntegral w | ||||||
|                  ,ah=fromIntegral h |                  ,ah=fromIntegral h | ||||||
|                  ,amsg=helpmsg |                  ,amsg=helpmsg | ||||||
|                  ,aopts=opts |                  ,aopts=opts | ||||||
|                  ,aargs=args |                  ,aargs=patterns_ $ reportopts_ $ cliopts_ opts | ||||||
|                  ,ajournal=j |                  ,ajournal=j | ||||||
|                  ,abuf=[] |                  ,abuf=[] | ||||||
|                  ,alocs=[] |                  ,alocs=[] | ||||||
| @ -111,7 +97,7 @@ vty opts args j = do | |||||||
| -- | Update the screen, wait for the next event, repeat. | -- | Update the screen, wait for the next event, repeat. | ||||||
| go :: AppState -> IO () | go :: AppState -> IO () | ||||||
| go a@AppState{av=av,aopts=opts} = do | go a@AppState{av=av,aopts=opts} = do | ||||||
|   when (notElem DebugVty opts) $ update av (renderScreen a) |   when (not $ debug_vty_ opts) $ update av (renderScreen a) | ||||||
|   k <- next_event av |   k <- next_event av | ||||||
|   d <- getCurrentDay |   d <- getCurrentDay | ||||||
|   case k of  |   case k of  | ||||||
| @ -268,10 +254,11 @@ resetTrailAndEnter d scr a = enter d scr (aargs a) $ clearLocs a | |||||||
| updateData :: Day -> AppState -> AppState | updateData :: Day -> AppState -> AppState | ||||||
| updateData d a@AppState{aopts=opts,ajournal=j} = | updateData d a@AppState{aopts=opts,ajournal=j} = | ||||||
|     case screen a of |     case screen a of | ||||||
|       BalanceScreen  -> a{abuf=accountsReportAsText opts $ accountsReport opts fspec j} |       BalanceScreen  -> a{abuf=accountsReportAsText ropts $ accountsReport ropts fspec j} | ||||||
|       RegisterScreen -> a{abuf=lines $ postingsReportAsText opts $ postingsReport opts fspec j} |       RegisterScreen -> a{abuf=lines $ postingsReportAsText ropts $ postingsReport ropts fspec j} | ||||||
|       PrintScreen    -> a{abuf=lines $ showTransactions opts fspec j} |       PrintScreen    -> a{abuf=lines $ showTransactions ropts fspec j} | ||||||
|     where fspec = optsToFilterSpec opts (currentArgs a) d |     where fspec = optsToFilterSpec ropts{patterns_=currentArgs a} d | ||||||
|  |           ropts = reportopts_ $ cliopts_ opts | ||||||
| 
 | 
 | ||||||
| backout :: Day -> AppState -> AppState | backout :: Day -> AppState -> AppState | ||||||
| backout d a | screen a == BalanceScreen = a | backout d a | screen a == BalanceScreen = a | ||||||
|  | |||||||
							
								
								
									
										55
									
								
								hledger-vty/Hledger/Vty/Options.hs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								hledger-vty/Hledger/Vty/Options.hs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,55 @@ | |||||||
|  | {-| | ||||||
|  | 
 | ||||||
|  | -} | ||||||
|  | 
 | ||||||
|  | module Hledger.Vty.Options | ||||||
|  | where | ||||||
|  | import System.Console.CmdArgs | ||||||
|  | import System.Console.CmdArgs.Explicit | ||||||
|  | 
 | ||||||
|  | import Hledger.Cli hiding (progname,progversion) | ||||||
|  | import qualified Hledger.Cli (progname) | ||||||
|  | 
 | ||||||
|  | progname = Hledger.Cli.progname ++ "-vty" | ||||||
|  | progversion = progversionstr progname | ||||||
|  | 
 | ||||||
|  | vtyflags = [ | ||||||
|  |   flagNone ["debug-vty"]  (\opts -> setboolopt "rules-file" opts) "run with no terminal output, showing console" | ||||||
|  |  ] | ||||||
|  | 
 | ||||||
|  | vtymode =  (mode "hledger-vty" [("command","vty")] | ||||||
|  |             "browse accounts, postings and entries in a full-window curses interface" | ||||||
|  |             commandargsflag (vtyflags++generalflags1)){ | ||||||
|  |              modeHelpSuffix=[ | ||||||
|  |                   -- "Reads your ~/.hledger.journal file, or another specified by $LEDGER_FILE or -f, and starts the full-window curses ui." | ||||||
|  |                  ] | ||||||
|  |            } | ||||||
|  | 
 | ||||||
|  | -- hledger-vty options, used in hledger-vty and above | ||||||
|  | data VtyOpts = VtyOpts { | ||||||
|  |      debug_vty_ :: Bool | ||||||
|  |     ,cliopts_   :: CliOpts | ||||||
|  |  } deriving (Show) | ||||||
|  | 
 | ||||||
|  | defvtyopts = VtyOpts | ||||||
|  |     def | ||||||
|  |     def | ||||||
|  | 
 | ||||||
|  | -- instance Default CliOpts where def = defcliopts | ||||||
|  | 
 | ||||||
|  | toVtyOpts :: RawOpts -> IO VtyOpts | ||||||
|  | toVtyOpts rawopts = do | ||||||
|  |   cliopts <- toCliOpts rawopts | ||||||
|  |   return defvtyopts { | ||||||
|  |               debug_vty_ = boolopt "debug-vty" rawopts | ||||||
|  |              ,cliopts_   = cliopts | ||||||
|  |              } | ||||||
|  | 
 | ||||||
|  | checkVtyOpts :: VtyOpts -> IO VtyOpts | ||||||
|  | checkVtyOpts opts = do | ||||||
|  |   checkCliOpts $ cliopts_ opts | ||||||
|  |   return opts | ||||||
|  | 
 | ||||||
|  | getHledgerVtyOpts :: IO VtyOpts | ||||||
|  | getHledgerVtyOpts = processArgs vtymode >>= return . decodeRawOpts >>= toVtyOpts >>= checkVtyOpts | ||||||
|  | 
 | ||||||
| @ -35,6 +35,7 @@ executable hledger-vty | |||||||
|                  ,hledger-lib == 0.15 |                  ,hledger-lib == 0.15 | ||||||
|                  -- ,HUnit |                  -- ,HUnit | ||||||
|                  ,base >= 3 && < 5 |                  ,base >= 3 && < 5 | ||||||
|  |                  ,cmdargs >= 0.7   && < 0.8 | ||||||
|                  -- ,containers |                  -- ,containers | ||||||
|                  -- ,csv |                  -- ,csv | ||||||
|                  -- ,directory |                  -- ,directory | ||||||
|  | |||||||
| @ -7,6 +7,7 @@ module Hledger.Web ( | |||||||
|                      module Hledger.Web.AppRun, |                      module Hledger.Web.AppRun, | ||||||
|                      module Hledger.Web.EmbeddedFiles, |                      module Hledger.Web.EmbeddedFiles, | ||||||
|                      module Hledger.Web.Handlers, |                      module Hledger.Web.Handlers, | ||||||
|  |                      module Hledger.Web.Options, | ||||||
|                      module Hledger.Web.Settings, |                      module Hledger.Web.Settings, | ||||||
|                      module Hledger.Web.StaticFiles, |                      module Hledger.Web.StaticFiles, | ||||||
|                      tests_Hledger_Web |                      tests_Hledger_Web | ||||||
| @ -18,6 +19,7 @@ import Hledger.Web.App | |||||||
| import Hledger.Web.AppRun | import Hledger.Web.AppRun | ||||||
| import Hledger.Web.EmbeddedFiles | import Hledger.Web.EmbeddedFiles | ||||||
| import Hledger.Web.Handlers | import Hledger.Web.Handlers | ||||||
|  | import Hledger.Web.Options | ||||||
| import Hledger.Web.Settings | import Hledger.Web.Settings | ||||||
| import Hledger.Web.StaticFiles | import Hledger.Web.StaticFiles | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1,5 +1,4 @@ | |||||||
| {-# LANGUAGE QuasiQuotes, TemplateHaskell, TypeFamilies #-} | {-# LANGUAGE QuasiQuotes, TemplateHaskell, TypeFamilies, OverloadedStrings #-} | ||||||
| {-# LANGUAGE OverloadedStrings #-} |  | ||||||
| module Hledger.Web.App | module Hledger.Web.App | ||||||
|     ( App (..) |     ( App (..) | ||||||
|     , AppRoute (..) |     , AppRoute (..) | ||||||
| @ -22,8 +21,8 @@ import Text.Hamlet hiding (hamletFile) | |||||||
| import Yesod.Core | import Yesod.Core | ||||||
| import Yesod.Helpers.Static | import Yesod.Helpers.Static | ||||||
| 
 | 
 | ||||||
| import Hledger.Cli.Options |  | ||||||
| import Hledger.Data | import Hledger.Data | ||||||
|  | import Hledger.Web.Options | ||||||
| import Hledger.Web.Settings | import Hledger.Web.Settings | ||||||
| import Hledger.Web.StaticFiles | import Hledger.Web.StaticFiles | ||||||
| 
 | 
 | ||||||
| @ -34,7 +33,7 @@ import Hledger.Web.StaticFiles | |||||||
| data App = App | data App = App | ||||||
|     {getStatic :: Static -- ^ Settings for static file serving. |     {getStatic :: Static -- ^ Settings for static file serving. | ||||||
|     ,appRoot    :: T.Text |     ,appRoot    :: T.Text | ||||||
|     ,appOpts    :: [Opt] |     ,appOpts    :: WebOpts | ||||||
|     ,appArgs    :: [String] |     ,appArgs    :: [String] | ||||||
|     ,appJournal :: Journal |     ,appJournal :: Journal | ||||||
|     } |     } | ||||||
|  | |||||||
| @ -18,6 +18,7 @@ import Hledger | |||||||
| import Hledger.Cli | import Hledger.Cli | ||||||
| import Hledger.Web.App | import Hledger.Web.App | ||||||
| import Hledger.Web.Handlers | import Hledger.Web.Handlers | ||||||
|  | import Hledger.Web.Options | ||||||
| import Hledger.Web.Settings | import Hledger.Web.Settings | ||||||
| 
 | 
 | ||||||
| -- This line actually creates our YesodSite instance. It is the second half | -- This line actually creates our YesodSite instance. It is the second half | ||||||
| @ -38,7 +39,7 @@ withDevelApp = toDyn (withApp a :: (Application -> IO ()) -> IO ()) | |||||||
|    where a = App{ |    where a = App{ | ||||||
|               getStatic=static Hledger.Web.Settings.staticdir |               getStatic=static Hledger.Web.Settings.staticdir | ||||||
|              ,appRoot=Hledger.Web.Settings.defapproot |              ,appRoot=Hledger.Web.Settings.defapproot | ||||||
|              ,appOpts=[] |              ,appOpts=defwebopts | ||||||
|              ,appArgs=[] |              ,appArgs=[] | ||||||
|              ,appJournal=nulljournal |              ,appJournal=nulljournal | ||||||
|              } |              } | ||||||
| @ -53,7 +54,7 @@ withWaiHandlerDevelApp func = do | |||||||
|   let a = App{ |   let a = App{ | ||||||
|               getStatic=static Hledger.Web.Settings.staticdir |               getStatic=static Hledger.Web.Settings.staticdir | ||||||
|              ,appRoot=Settings.defapproot |              ,appRoot=Settings.defapproot | ||||||
|              ,appOpts=[File f] |              ,appOpts=defwebopts{cliopts_=defcliopts{file_=Just f}} | ||||||
|              ,appArgs=[] |              ,appArgs=[] | ||||||
|              ,appJournal=j |              ,appJournal=j | ||||||
|              } |              } | ||||||
|  | |||||||
| @ -29,6 +29,7 @@ import Yesod.Json | |||||||
| import Hledger hiding (today) | import Hledger hiding (today) | ||||||
| import Hledger.Cli | import Hledger.Cli | ||||||
| import Hledger.Web.App | import Hledger.Web.App | ||||||
|  | import Hledger.Web.Options | ||||||
| import Hledger.Web.Settings | import Hledger.Web.Settings | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @ -60,7 +61,7 @@ getJournalR = do | |||||||
|                                   where andsubs = if subs then " (and subaccounts)" else "" |                                   where andsubs = if subs then " (and subaccounts)" else "" | ||||||
|                 where |                 where | ||||||
|                   filter = if filtering then ", filtered" else "" |                   filter = if filtering then ", filtered" else "" | ||||||
|       maincontent = journalTransactionsReportAsHtml opts vd $ journalTransactionsReport opts j m |       maincontent = journalTransactionsReportAsHtml opts vd $ journalTransactionsReport (reportopts_ $ cliopts_ opts) j m | ||||||
|   defaultLayout $ do |   defaultLayout $ do | ||||||
|       setTitle "hledger-web journal" |       setTitle "hledger-web journal" | ||||||
|       addHamlet [$hamlet| |       addHamlet [$hamlet| | ||||||
| @ -93,7 +94,7 @@ getJournalEntriesR = do | |||||||
|   let |   let | ||||||
|       sidecontent = sidebar vd |       sidecontent = sidebar vd | ||||||
|       title = "Journal entries" ++ if m /= MatchAny then ", filtered" else "" :: String |       title = "Journal entries" ++ if m /= MatchAny then ", filtered" else "" :: String | ||||||
|       maincontent = entriesReportAsHtml opts vd $ entriesReport opts nullfilterspec $ filterJournalTransactions2 m j |       maincontent = entriesReportAsHtml opts vd $ entriesReport (reportopts_ $ cliopts_ opts) nullfilterspec $ filterJournalTransactions2 m j | ||||||
|   defaultLayout $ do |   defaultLayout $ do | ||||||
|       setTitle "hledger-web journal" |       setTitle "hledger-web journal" | ||||||
|       addHamlet [$hamlet| |       addHamlet [$hamlet| | ||||||
| @ -117,7 +118,7 @@ getJournalOnlyR = do | |||||||
|   vd@VD{..} <- getViewData |   vd@VD{..} <- getViewData | ||||||
|   defaultLayout $ do |   defaultLayout $ do | ||||||
|       setTitle "hledger-web journal only" |       setTitle "hledger-web journal only" | ||||||
|       addHamlet $ entriesReportAsHtml opts vd $ entriesReport opts nullfilterspec $ filterJournalTransactions2 m j |       addHamlet $ entriesReportAsHtml opts vd $ entriesReport (reportopts_ $ cliopts_ opts) nullfilterspec $ filterJournalTransactions2 m j | ||||||
| 
 | 
 | ||||||
| ---------------------------------------------------------------------- | ---------------------------------------------------------------------- | ||||||
| 
 | 
 | ||||||
| @ -133,7 +134,7 @@ getRegisterR = do | |||||||
|                  (a,subs) = fromMaybe ("all accounts",False) $ inAccount qopts |                  (a,subs) = fromMaybe ("all accounts",False) $ inAccount qopts | ||||||
|                  andsubs = if subs then " (and subaccounts)" else "" |                  andsubs = if subs then " (and subaccounts)" else "" | ||||||
|                  filter = if filtering then ", filtered" else "" |                  filter = if filtering then ", filtered" else "" | ||||||
|       maincontent = registerReportHtml opts vd $ accountTransactionsReport opts j m $ fromMaybe MatchAny $ inAccountMatcher qopts |       maincontent = registerReportHtml opts vd $ accountTransactionsReport (reportopts_ $ cliopts_ opts) j m $ fromMaybe MatchAny $ inAccountMatcher qopts | ||||||
|   defaultLayout $ do |   defaultLayout $ do | ||||||
|       setTitle "hledger-web register" |       setTitle "hledger-web register" | ||||||
|       addHamlet [$hamlet| |       addHamlet [$hamlet| | ||||||
| @ -158,8 +159,8 @@ getRegisterOnlyR = do | |||||||
|   defaultLayout $ do |   defaultLayout $ do | ||||||
|       setTitle "hledger-web register only" |       setTitle "hledger-web register only" | ||||||
|       addHamlet $ |       addHamlet $ | ||||||
|           case inAccountMatcher qopts of Just m' -> registerReportHtml opts vd $ accountTransactionsReport opts j m m' |           case inAccountMatcher qopts of Just m' -> registerReportHtml opts vd $ accountTransactionsReport (reportopts_ $ cliopts_ opts) j m m' | ||||||
|                                          Nothing -> registerReportHtml opts vd $ journalTransactionsReport opts j m |                                          Nothing -> registerReportHtml opts vd $ journalTransactionsReport (reportopts_ $ cliopts_ opts) j m | ||||||
| 
 | 
 | ||||||
| ---------------------------------------------------------------------- | ---------------------------------------------------------------------- | ||||||
| 
 | 
 | ||||||
| @ -171,7 +172,7 @@ getAccountsR = do | |||||||
|   let j' = filterJournalPostings2 m j |   let j' = filterJournalPostings2 m j | ||||||
|       html = do |       html = do | ||||||
|         setTitle "hledger-web accounts" |         setTitle "hledger-web accounts" | ||||||
|         addHamlet $ accountsReportAsHtml opts vd $ accountsReport2 opts am j' |         addHamlet $ accountsReportAsHtml opts vd $ accountsReport2 (reportopts_ $ cliopts_ opts) am j' | ||||||
|       json = jsonMap [("accounts", toJSON $ journalAccountNames j')] |       json = jsonMap [("accounts", toJSON $ journalAccountNames j')] | ||||||
|   defaultLayoutJson html json |   defaultLayoutJson html json | ||||||
| 
 | 
 | ||||||
| @ -187,10 +188,10 @@ getAccountsJsonR = do | |||||||
| 
 | 
 | ||||||
| -- | Render the sidebar used on most views. | -- | Render the sidebar used on most views. | ||||||
| sidebar :: ViewData -> Hamlet AppRoute | sidebar :: ViewData -> Hamlet AppRoute | ||||||
| sidebar vd@VD{..} = accountsReportAsHtml opts vd $ accountsReport2 opts am j | sidebar vd@VD{..} = accountsReportAsHtml opts vd $ accountsReport2 (reportopts_ $ cliopts_ opts) am j | ||||||
| 
 | 
 | ||||||
| -- | Render a "AccountsReport" as HTML. | -- | Render a "AccountsReport" as HTML. | ||||||
| accountsReportAsHtml :: [Opt] -> ViewData -> AccountsReport -> Hamlet AppRoute | accountsReportAsHtml :: WebOpts -> ViewData -> AccountsReport -> Hamlet AppRoute | ||||||
| accountsReportAsHtml _ vd@VD{..} (items',total) = | accountsReportAsHtml _ vd@VD{..} (items',total) = | ||||||
|  [$hamlet| |  [$hamlet| | ||||||
| <div#accountsheading | <div#accountsheading | ||||||
| @ -271,7 +272,7 @@ accountOnlyQuery a = "inacctonly:" ++ quoteIfSpaced a -- (accountNameToAccountRe | |||||||
| accountUrl r a = (r, [("q",pack $ accountQuery a)]) | accountUrl r a = (r, [("q",pack $ accountQuery a)]) | ||||||
| 
 | 
 | ||||||
| -- | Render a "EntriesReport" as HTML for the journal entries view. | -- | Render a "EntriesReport" as HTML for the journal entries view. | ||||||
| entriesReportAsHtml :: [Opt] -> ViewData -> EntriesReport -> Hamlet AppRoute | entriesReportAsHtml :: WebOpts -> ViewData -> EntriesReport -> Hamlet AppRoute | ||||||
| entriesReportAsHtml _ vd items = [$hamlet| | entriesReportAsHtml _ vd items = [$hamlet| | ||||||
| <table.journalreport> | <table.journalreport> | ||||||
|  $forall i <- numbered items |  $forall i <- numbered items | ||||||
| @ -289,7 +290,7 @@ entriesReportAsHtml _ vd items = [$hamlet| | |||||||
|        txn = trimnl $ showTransaction t where trimnl = reverse . dropWhile (=='\n') . reverse |        txn = trimnl $ showTransaction t where trimnl = reverse . dropWhile (=='\n') . reverse | ||||||
| 
 | 
 | ||||||
| -- | Render an "TransactionsReport" as HTML for the formatted journal view. | -- | Render an "TransactionsReport" as HTML for the formatted journal view. | ||||||
| journalTransactionsReportAsHtml :: [Opt] -> ViewData -> TransactionsReport -> Hamlet AppRoute | journalTransactionsReportAsHtml :: WebOpts -> ViewData -> TransactionsReport -> Hamlet AppRoute | ||||||
| journalTransactionsReportAsHtml _ vd (_,items) = [$hamlet| | journalTransactionsReportAsHtml _ vd (_,items) = [$hamlet| | ||||||
| <table.journalreport | <table.journalreport | ||||||
|  <tr.headings |  <tr.headings | ||||||
| @ -327,14 +328,14 @@ $forall p <- tpostings t | |||||||
|        showamt = not split || not (isZeroMixedAmount amt) |        showamt = not split || not (isZeroMixedAmount amt) | ||||||
| 
 | 
 | ||||||
| -- Generate html for an account register, including a balance chart and transaction list. | -- Generate html for an account register, including a balance chart and transaction list. | ||||||
| registerReportHtml :: [Opt] -> ViewData -> TransactionsReport -> Hamlet AppRoute | registerReportHtml :: WebOpts -> ViewData -> TransactionsReport -> Hamlet AppRoute | ||||||
| registerReportHtml opts vd r@(_,items) = [$hamlet| | registerReportHtml opts vd r@(_,items) = [$hamlet| | ||||||
|  ^{registerChartHtml items} |  ^{registerChartHtml items} | ||||||
|  ^{registerItemsHtml opts vd r} |  ^{registerItemsHtml opts vd r} | ||||||
| |] | |] | ||||||
| 
 | 
 | ||||||
| -- Generate html for a transaction list from an "TransactionsReport". | -- Generate html for a transaction list from an "TransactionsReport". | ||||||
| registerItemsHtml :: [Opt] -> ViewData -> TransactionsReport -> Hamlet AppRoute | registerItemsHtml :: WebOpts -> ViewData -> TransactionsReport -> Hamlet AppRoute | ||||||
| registerItemsHtml _ vd (balancelabel,items) = [$hamlet| | registerItemsHtml _ vd (balancelabel,items) = [$hamlet| | ||||||
| <table.registerreport | <table.registerreport | ||||||
|  <tr.headings |  <tr.headings | ||||||
| @ -825,7 +826,7 @@ nulltemplate = [$hamlet||] | |||||||
| 
 | 
 | ||||||
| -- | A bundle of data useful for hledger-web request handlers and templates. | -- | A bundle of data useful for hledger-web request handlers and templates. | ||||||
| data ViewData = VD { | data ViewData = VD { | ||||||
|      opts         :: [Opt]      -- ^ the command-line options at startup |      opts         :: WebOpts    -- ^ the command-line options at startup | ||||||
|     ,here         :: AppRoute   -- ^ the current route |     ,here         :: AppRoute   -- ^ the current route | ||||||
|     ,msg          :: Maybe Html -- ^ the current UI message if any, possibly from the current request |     ,msg          :: Maybe Html -- ^ the current UI message if any, possibly from the current request | ||||||
|     ,today        :: Day        -- ^ today's date (for queries containing relative dates) |     ,today        :: Day        -- ^ today's date (for queries containing relative dates) | ||||||
| @ -848,7 +849,7 @@ viewdataWithDateAndParams d q a p = | |||||||
|     let (querymatcher,queryopts) = parseQuery d q |     let (querymatcher,queryopts) = parseQuery d q | ||||||
|         (acctsmatcher,acctsopts) = parseQuery d a |         (acctsmatcher,acctsopts) = parseQuery d a | ||||||
|     in VD { |     in VD { | ||||||
|            opts         = [NoElide] |            opts         = defwebopts{cliopts_=defcliopts{reportopts_=defreportopts{no_elide_=True}}} | ||||||
|           ,j            = nulljournal |           ,j            = nulljournal | ||||||
|           ,here         = RootR |           ,here         = RootR | ||||||
|           ,msg          = Nothing |           ,msg          = Nothing | ||||||
| @ -865,8 +866,8 @@ viewdataWithDateAndParams d q a p = | |||||||
| getViewData :: Handler ViewData | getViewData :: Handler ViewData | ||||||
| getViewData = do | getViewData = do | ||||||
|   app        <- getYesod |   app        <- getYesod | ||||||
|   let opts = appOpts app ++ [NoElide] |   let opts@WebOpts{cliopts_=copts@CliOpts{reportopts_=ropts}} = appOpts app | ||||||
|   (j, err)   <- getCurrentJournal opts |   (j, err)   <- getCurrentJournal $ copts{reportopts_=ropts{no_elide_=True}} | ||||||
|   msg        <- getMessageOr err |   msg        <- getMessageOr err | ||||||
|   Just here  <- getCurrentRoute |   Just here  <- getCurrentRoute | ||||||
|   today      <- liftIO getCurrentDay |   today      <- liftIO getCurrentDay | ||||||
| @ -884,7 +885,7 @@ getViewData = do | |||||||
|       -- | Update our copy of the journal if the file changed. If there is an |       -- | Update our copy of the journal if the file changed. If there is an | ||||||
|       -- error while reloading, keep the old one and return the error, and set a |       -- error while reloading, keep the old one and return the error, and set a | ||||||
|       -- ui message. |       -- ui message. | ||||||
|       getCurrentJournal :: [Opt] -> Handler (Journal, Maybe String) |       getCurrentJournal :: CliOpts -> Handler (Journal, Maybe String) | ||||||
|       getCurrentJournal opts = do |       getCurrentJournal opts = do | ||||||
|         j <- liftIO $ fromJust `fmap` getValue "hledger" "journal" |         j <- liftIO $ fromJust `fmap` getValue "hledger" "journal" | ||||||
|         (jE, changed) <- liftIO $ journalReloadIfChanged opts j |         (jE, changed) <- liftIO $ journalReloadIfChanged opts j | ||||||
|  | |||||||
							
								
								
									
										66
									
								
								hledger-web/Hledger/Web/Options.hs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								hledger-web/Hledger/Web/Options.hs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,66 @@ | |||||||
|  | {-| | ||||||
|  | 
 | ||||||
|  | -} | ||||||
|  | 
 | ||||||
|  | module Hledger.Web.Options | ||||||
|  | where | ||||||
|  | import Data.Maybe | ||||||
|  | import Data.Text (unpack) | ||||||
|  | import System.Console.CmdArgs | ||||||
|  | import System.Console.CmdArgs.Explicit | ||||||
|  | 
 | ||||||
|  | import Hledger.Cli hiding (progname,progversion) | ||||||
|  | import qualified Hledger.Cli (progname) | ||||||
|  | 
 | ||||||
|  | import Hledger.Web.Settings | ||||||
|  | 
 | ||||||
|  | progname = Hledger.Cli.progname ++ "-web" | ||||||
|  | progversion = progversionstr progname | ||||||
|  | 
 | ||||||
|  | defbaseurl = unpack defapproot | ||||||
|  | defbaseurl' = (reverse $ drop 4 $ reverse defbaseurl) ++ "PORT" | ||||||
|  | 
 | ||||||
|  | webflags = [ | ||||||
|  |   flagReq ["base-url"]  (\s opts -> Right $ setopt "base-url" s opts) "URL" ("set the base url (default: "++defbaseurl'++")") | ||||||
|  |  ,flagReq ["port"]  (\s opts -> Right $ setopt "port" s opts) "PORT" ("listen on this tcp port (default: "++show defport++")") | ||||||
|  |  ] | ||||||
|  |   | ||||||
|  | webmode =  (mode "hledger-web" [("command","web")] | ||||||
|  |             "start serving the hledger web interface" | ||||||
|  |             commandargsflag (webflags++generalflags1)){ | ||||||
|  |              modeHelpSuffix=[ | ||||||
|  |                   -- "Reads your ~/.hledger.journal file, or another specified by $LEDGER_FILE or -f, and starts the full-window curses ui." | ||||||
|  |                  ] | ||||||
|  |            } | ||||||
|  | 
 | ||||||
|  | -- hledger-web options, used in hledger-web and above | ||||||
|  | data WebOpts = WebOpts { | ||||||
|  |      base_url_ :: String | ||||||
|  |     ,port_     :: Int | ||||||
|  |     ,cliopts_  :: CliOpts | ||||||
|  |  } deriving (Show) | ||||||
|  | 
 | ||||||
|  | defwebopts = WebOpts | ||||||
|  |     def | ||||||
|  |     def | ||||||
|  |     def | ||||||
|  | 
 | ||||||
|  | -- instance Default WebOpts where def = defwebopts | ||||||
|  | 
 | ||||||
|  | toWebOpts :: RawOpts -> IO WebOpts | ||||||
|  | toWebOpts rawopts = do | ||||||
|  |   cliopts <- toCliOpts rawopts | ||||||
|  |   return defwebopts { | ||||||
|  |               base_url_ = fromMaybe defbaseurl $ maybestringopt "base-url" rawopts | ||||||
|  |              ,port_ = fromMaybe defport $ maybeintopt "port" rawopts | ||||||
|  |              ,cliopts_   = cliopts | ||||||
|  |              } | ||||||
|  | 
 | ||||||
|  | checkWebOpts :: WebOpts -> IO WebOpts | ||||||
|  | checkWebOpts opts = do | ||||||
|  |   checkCliOpts $ cliopts_ opts | ||||||
|  |   return opts | ||||||
|  | 
 | ||||||
|  | getHledgerWebOpts :: IO WebOpts | ||||||
|  | getHledgerWebOpts = processArgs webmode >>= return . decodeRawOpts >>= toWebOpts >>= checkWebOpts | ||||||
|  | 
 | ||||||
| @ -64,6 +64,7 @@ executable hledger-web | |||||||
|                  ,HUnit |                  ,HUnit | ||||||
|                  ,base >= 4 && < 5 |                  ,base >= 4 && < 5 | ||||||
|                  ,bytestring |                  ,bytestring | ||||||
|  |                  ,cmdargs >= 0.7   && < 0.8 | ||||||
|                  -- ,containers |                  -- ,containers | ||||||
|                  -- ,csv |                  -- ,csv | ||||||
|                  ,directory |                  ,directory | ||||||
|  | |||||||
| @ -9,6 +9,7 @@ module Main | |||||||
| where | where | ||||||
| 
 | 
 | ||||||
| -- import Control.Concurrent (forkIO, threadDelay) | -- import Control.Concurrent (forkIO, threadDelay) | ||||||
|  | import Control.Monad | ||||||
| import Data.Maybe | import Data.Maybe | ||||||
| import Data.Text(pack) | import Data.Text(pack) | ||||||
| import Network.Wai.Handler.Warp (run) | import Network.Wai.Handler.Warp (run) | ||||||
| @ -16,58 +17,41 @@ import Network.Wai.Handler.Warp (run) | |||||||
| #else | #else | ||||||
| import Network.Wai.Middleware.Debug (debug) | import Network.Wai.Middleware.Debug (debug) | ||||||
| #endif | #endif | ||||||
| import System.Console.GetOpt |  | ||||||
| import System.Exit (exitFailure) | import System.Exit (exitFailure) | ||||||
| import System.IO.Storage (withStore, putValue) | import System.IO.Storage (withStore, putValue) | ||||||
| import Text.Printf | import Text.Printf | ||||||
| import Yesod.Helpers.Static | import Yesod.Helpers.Static | ||||||
| 
 | 
 | ||||||
| import Hledger.Cli | import Hledger | ||||||
| import Hledger.Cli.Tests (runTestsOrExit) | import Hledger.Cli hiding (progname,progversion) | ||||||
| import Hledger.Data | import Hledger.Cli.Tests | ||||||
| import Prelude hiding (putStr, putStrLn) | import Prelude hiding (putStrLn) | ||||||
| import Hledger.Utils.UTF8 (putStr, putStrLn) | import Hledger.Utils.UTF8 (putStrLn) | ||||||
| import Hledger.Web | import Hledger.Web | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| progname_web = progname_cli ++ "-web" |  | ||||||
| 
 |  | ||||||
| options_web :: [OptDescr Opt] |  | ||||||
| options_web = [ |  | ||||||
|   Option ""  ["base-url"]     (ReqArg BaseUrl "URL") "use this base url (default http://localhost:PORT)" |  | ||||||
|  ,Option ""  ["port"]         (ReqArg Port "N")      "serve on tcp port N (default 5000)" |  | ||||||
|  ] |  | ||||||
| 
 |  | ||||||
| usage_preamble_web = |  | ||||||
|   "Usage: hledger-web [OPTIONS] [PATTERNS]\n" ++ |  | ||||||
|   "\n" ++ |  | ||||||
|   "Reads your ~/.journal file, or another specified by $LEDGER or -f, and\n" ++ |  | ||||||
|   "starts a web ui server. Also attempts to start a web browser (unless --debug).\n" ++ |  | ||||||
|   "\n" |  | ||||||
| 
 |  | ||||||
| usage_options_web = usageInfo "hledger-web options:" options_web ++ "\n" |  | ||||||
| 
 |  | ||||||
| usage_web = concat [ |  | ||||||
|              usage_preamble_web |  | ||||||
|             ,usage_options_web |  | ||||||
|             ,usage_options_cli |  | ||||||
|             ,usage_postscript_cli |  | ||||||
|             ] |  | ||||||
| 
 |  | ||||||
| main :: IO () | main :: IO () | ||||||
| main = do | main = do | ||||||
|   (opts, args) <- parseArgumentsWith $ options_cli++options_web |   opts <- getHledgerWebOpts | ||||||
|   run opts args |   when (debug_ $ cliopts_ opts) $ printf "%s\n" progversion >> printf "opts: %s\n" (show opts) | ||||||
|  |   runWith opts | ||||||
|  | 
 | ||||||
|  | runWith :: WebOpts -> IO () | ||||||
|  | runWith opts = run opts | ||||||
|     where |     where | ||||||
|       run opts args |       run opts | ||||||
|        | Help `elem` opts             = putStr usage_web |           | "help" `in_` (rawopts_ $ cliopts_ opts)            = printModeHelpAndExit webmode | ||||||
|        | Version `elem` opts          = putStrLn $ progversionstr progname_web |           | "binary-filename" `in_` (rawopts_ $ cliopts_ opts) = putStrLn (binaryfilename progname) | ||||||
|        | BinaryFilename `elem` opts   = putStrLn $ binaryfilename progname_web |           | otherwise                                          = withJournalDo' opts web | ||||||
|        | otherwise                    = withJournalDo opts args "web" web | 
 | ||||||
|  | withJournalDo' :: WebOpts -> (WebOpts -> Journal -> IO ()) -> IO () | ||||||
|  | withJournalDo' opts cmd = do | ||||||
|  |   journalFilePathFromOpts (cliopts_ opts) >>= readJournalFile Nothing >>= | ||||||
|  |     either error' (cmd opts . journalApplyAliases (aliasesFromOpts $ cliopts_ opts)) | ||||||
| 
 | 
 | ||||||
| -- | The web command. | -- | The web command. | ||||||
| web :: [Opt] -> [String] -> Journal -> IO () | web :: WebOpts -> Journal -> IO () | ||||||
| web opts args j = do | web opts j = do | ||||||
|   created <- createFilesIfMissing |   created <- createFilesIfMissing | ||||||
|   if created |   if created | ||||||
|    then do |    then do | ||||||
| @ -75,13 +59,10 @@ web opts args j = do | |||||||
|      exitFailure |      exitFailure | ||||||
|    else do |    else do | ||||||
|      putStrLn $ "Running self-tests..." |      putStrLn $ "Running self-tests..." | ||||||
|      runTestsOrExit opts args |      runTestsOrExit $ cliopts_ opts | ||||||
|      putStrLn $ "Using support files in "++datadir |      putStrLn $ "Using support files in "++datadir | ||||||
|      let host    = defhost |      -- unless (debug_ $ cliopts_ opts) $ forkIO (browser baseurl) >> return () | ||||||
|          port    = fromMaybe defport $ portFromOpts opts |      server (base_url_ opts) (port_ opts) opts j | ||||||
|          baseurl = fromMaybe (printf "http://%s:%d" host port) $ baseUrlFromOpts opts |  | ||||||
|      -- unless (Debug `elem` opts) $ forkIO (browser baseurl) >> return () |  | ||||||
|      server baseurl port opts args j |  | ||||||
| 
 | 
 | ||||||
| -- browser :: String -> IO () | -- browser :: String -> IO () | ||||||
| -- browser baseurl = do | -- browser baseurl = do | ||||||
| @ -89,17 +70,18 @@ web opts args j = do | |||||||
| --   putStrLn "Attempting to start a web browser" | --   putStrLn "Attempting to start a web browser" | ||||||
| --   openBrowserOn baseurl >> return () | --   openBrowserOn baseurl >> return () | ||||||
| 
 | 
 | ||||||
| server :: String -> Int -> [Opt] -> [String] -> Journal -> IO () | server :: String -> Int -> WebOpts -> Journal -> IO () | ||||||
| server baseurl port opts args j = do | server baseurl port opts j = do | ||||||
|   printf "Starting http server on port %d with base url %s\n" port baseurl |   printf "Starting http server on port %d with base url %s\n" port baseurl | ||||||
|   let a = App{getStatic=static staticdir |   let a = App{getStatic=static staticdir | ||||||
|              ,appRoot=pack baseurl |              ,appRoot=pack baseurl | ||||||
|              ,appOpts=opts |              ,appOpts=opts | ||||||
|              ,appArgs=args |              ,appArgs=patterns_ $ reportopts_ $ cliopts_ opts | ||||||
|              ,appJournal=j |              ,appJournal=j | ||||||
|              } |              } | ||||||
|   withStore "hledger" $ do |   withStore "hledger" $ do | ||||||
|     putValue "hledger" "journal" j |     putValue "hledger" "journal" j | ||||||
|  |     return () | ||||||
| #if PRODUCTION | #if PRODUCTION | ||||||
|     withApp a (run port) |     withApp a (run port) | ||||||
| #else | #else | ||||||
|  | |||||||
| @ -37,7 +37,6 @@ import Hledger.Cli.Options | |||||||
| import Hledger.Cli.Utils | import Hledger.Cli.Utils | ||||||
| import Hledger.Cli.Version | import Hledger.Cli.Version | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
| -- | hledger and hledger-lib's unit tests aggregated from all modules | -- | hledger and hledger-lib's unit tests aggregated from all modules | ||||||
| -- plus some more which are easier to define here for now. | -- plus some more which are easier to define here for now. | ||||||
| tests_Hledger_Cli :: Test | tests_Hledger_Cli :: Test | ||||||
| @ -108,15 +107,14 @@ tests_Hledger_Cli = TestList | |||||||
|       "liabilities","liabilities:credit cards","liabilities:credit cards:discover"] |       "liabilities","liabilities:credit cards","liabilities:credit cards:discover"] | ||||||
| 
 | 
 | ||||||
|   ,"balance report tests" ~: |   ,"balance report tests" ~: | ||||||
|    let (opts,args) `gives` es = do  |    let opts `gives` es = do | ||||||
|         j <- samplejournal |         j <- samplejournal | ||||||
|         d <- getCurrentDay |         d <- getCurrentDay | ||||||
|         accountsReportAsText opts (accountsReport opts (optsToFilterSpec opts args d) j) `is` es |         accountsReportAsText opts (accountsReport opts (optsToFilterSpec opts d) j) `is` es | ||||||
|    in TestList |    in TestList | ||||||
|    [ |    [ | ||||||
| 
 |  | ||||||
|     "balance report with no args" ~: |     "balance report with no args" ~: | ||||||
|     ([], []) `gives` |     defreportopts `gives` | ||||||
|     ["                 $-1  assets" |     ["                 $-1  assets" | ||||||
|     ,"                  $1    bank:saving" |     ,"                  $1    bank:saving" | ||||||
|     ,"                 $-2    cash" |     ,"                 $-2    cash" | ||||||
| @ -132,7 +130,7 @@ tests_Hledger_Cli = TestList | |||||||
|     ] |     ] | ||||||
| 
 | 
 | ||||||
|    ,"balance report can be limited with --depth" ~: |    ,"balance report can be limited with --depth" ~: | ||||||
|     ([Depth "1"], []) `gives` |     defreportopts{depth_=Just 1} `gives` | ||||||
|     ["                 $-1  assets" |     ["                 $-1  assets" | ||||||
|     ,"                  $2  expenses" |     ,"                  $2  expenses" | ||||||
|     ,"                 $-2  income" |     ,"                 $-2  income" | ||||||
| @ -142,7 +140,7 @@ tests_Hledger_Cli = TestList | |||||||
|     ] |     ] | ||||||
|      |      | ||||||
|    ,"balance report with account pattern o" ~: |    ,"balance report with account pattern o" ~: | ||||||
|     ([], ["o"]) `gives` |     defreportopts{patterns_=["o"]} `gives` | ||||||
|     ["                  $1  expenses:food" |     ["                  $1  expenses:food" | ||||||
|     ,"                 $-2  income" |     ,"                 $-2  income" | ||||||
|     ,"                 $-1    gifts" |     ,"                 $-1    gifts" | ||||||
| @ -152,7 +150,7 @@ tests_Hledger_Cli = TestList | |||||||
|     ] |     ] | ||||||
| 
 | 
 | ||||||
|    ,"balance report with account pattern o and --depth 1" ~: |    ,"balance report with account pattern o and --depth 1" ~: | ||||||
|     ([Depth "1"], ["o"]) `gives` |     defreportopts{patterns_=["o"],depth_=Just 1} `gives` | ||||||
|     ["                  $1  expenses" |     ["                  $1  expenses" | ||||||
|     ,"                 $-2  income" |     ,"                 $-2  income" | ||||||
|     ,"--------------------" |     ,"--------------------" | ||||||
| @ -160,7 +158,7 @@ tests_Hledger_Cli = TestList | |||||||
|     ] |     ] | ||||||
| 
 | 
 | ||||||
|    ,"balance report with account pattern a" ~: |    ,"balance report with account pattern a" ~: | ||||||
|     ([], ["a"]) `gives` |     defreportopts{patterns_=["a"]} `gives` | ||||||
|     ["                 $-1  assets" |     ["                 $-1  assets" | ||||||
|     ,"                  $1    bank:saving" |     ,"                  $1    bank:saving" | ||||||
|     ,"                 $-2    cash" |     ,"                 $-2    cash" | ||||||
| @ -171,7 +169,7 @@ tests_Hledger_Cli = TestList | |||||||
|     ] |     ] | ||||||
| 
 | 
 | ||||||
|    ,"balance report with account pattern e" ~: |    ,"balance report with account pattern e" ~: | ||||||
|     ([], ["e"]) `gives` |     defreportopts{patterns_=["e"]} `gives` | ||||||
|     ["                 $-1  assets" |     ["                 $-1  assets" | ||||||
|     ,"                  $1    bank:saving" |     ,"                  $1    bank:saving" | ||||||
|     ,"                 $-2    cash" |     ,"                 $-2    cash" | ||||||
| @ -187,7 +185,7 @@ tests_Hledger_Cli = TestList | |||||||
|     ] |     ] | ||||||
| 
 | 
 | ||||||
|    ,"balance report with unmatched parent of two matched subaccounts" ~:  |    ,"balance report with unmatched parent of two matched subaccounts" ~:  | ||||||
|     ([], ["cash","saving"]) `gives` |     defreportopts{patterns_=["cash","saving"]} `gives` | ||||||
|     ["                 $-1  assets" |     ["                 $-1  assets" | ||||||
|     ,"                  $1    bank:saving" |     ,"                  $1    bank:saving" | ||||||
|     ,"                 $-2    cash" |     ,"                 $-2    cash" | ||||||
| @ -196,14 +194,14 @@ tests_Hledger_Cli = TestList | |||||||
|     ] |     ] | ||||||
| 
 | 
 | ||||||
|    ,"balance report with multi-part account name" ~:  |    ,"balance report with multi-part account name" ~:  | ||||||
|     ([], ["expenses:food"]) `gives` |     defreportopts{patterns_=["expenses:food"]} `gives` | ||||||
|     ["                  $1  expenses:food" |     ["                  $1  expenses:food" | ||||||
|     ,"--------------------" |     ,"--------------------" | ||||||
|     ,"                  $1" |     ,"                  $1" | ||||||
|     ] |     ] | ||||||
| 
 | 
 | ||||||
|    ,"balance report with negative account pattern" ~: |    ,"balance report with negative account pattern" ~: | ||||||
|     ([], ["not:assets"]) `gives` |     defreportopts{patterns_=["not:assets"]} `gives` | ||||||
|     ["                  $2  expenses" |     ["                  $2  expenses" | ||||||
|     ,"                  $1    food" |     ,"                  $1    food" | ||||||
|     ,"                  $1    supplies" |     ,"                  $1    supplies" | ||||||
| @ -216,20 +214,20 @@ tests_Hledger_Cli = TestList | |||||||
|     ] |     ] | ||||||
| 
 | 
 | ||||||
|    ,"balance report negative account pattern always matches full name" ~:  |    ,"balance report negative account pattern always matches full name" ~:  | ||||||
|     ([], ["not:e"]) `gives` |     defreportopts{patterns_=["not:e"]} `gives` | ||||||
|     ["--------------------" |     ["--------------------" | ||||||
|     ,"                   0" |     ,"                   0" | ||||||
|     ] |     ] | ||||||
| 
 | 
 | ||||||
|    ,"balance report negative patterns affect totals" ~:  |    ,"balance report negative patterns affect totals" ~:  | ||||||
|     ([], ["expenses","not:food"]) `gives` |     defreportopts{patterns_=["expenses","not:food"]} `gives` | ||||||
|     ["                  $1  expenses:supplies" |     ["                  $1  expenses:supplies" | ||||||
|     ,"--------------------" |     ,"--------------------" | ||||||
|     ,"                  $1" |     ,"                  $1" | ||||||
|     ] |     ] | ||||||
| 
 | 
 | ||||||
|    ,"balance report with -E shows zero-balance accounts" ~: |    ,"balance report with -E shows zero-balance accounts" ~: | ||||||
|     ([Empty], ["assets"]) `gives` |     defreportopts{patterns_=["assets"],empty_=True} `gives` | ||||||
|     ["                 $-1  assets" |     ["                 $-1  assets" | ||||||
|     ,"                  $1    bank" |     ,"                  $1    bank" | ||||||
|     ,"                   0      checking" |     ,"                   0      checking" | ||||||
| @ -247,7 +245,7 @@ tests_Hledger_Cli = TestList | |||||||
|              ,"  c:d                   " |              ,"  c:d                   " | ||||||
|              ]) >>= either error' return |              ]) >>= either error' return | ||||||
|       let j' = journalCanonicaliseAmounts $ journalConvertAmountsToCost j -- enable cost basis adjustment |       let j' = journalCanonicaliseAmounts $ journalConvertAmountsToCost j -- enable cost basis adjustment | ||||||
|       accountsReportAsText [] (accountsReport [] nullfilterspec j') `is` |       accountsReportAsText defreportopts (accountsReport defreportopts nullfilterspec j') `is` | ||||||
|         ["                $500  a:b" |         ["                $500  a:b" | ||||||
|         ,"               $-500  c:d" |         ,"               $-500  c:d" | ||||||
|         ,"--------------------" |         ,"--------------------" | ||||||
| @ -261,7 +259,7 @@ tests_Hledger_Cli = TestList | |||||||
|               ,"  test:a  1" |               ,"  test:a  1" | ||||||
|               ,"  test:b" |               ,"  test:b" | ||||||
|               ]) |               ]) | ||||||
|       accountsReportAsText [] (accountsReport [] nullfilterspec j) `is` |       accountsReportAsText defreportopts (accountsReport defreportopts nullfilterspec j) `is` | ||||||
|         ["                   1  test:a" |         ["                   1  test:a" | ||||||
|         ,"                  -1  test:b" |         ,"                  -1  test:b" | ||||||
|         ,"--------------------" |         ,"--------------------" | ||||||
| @ -294,11 +292,10 @@ tests_Hledger_Cli = TestList | |||||||
| 
 | 
 | ||||||
|    "print expenses" ~: |    "print expenses" ~: | ||||||
|    do  |    do  | ||||||
|     let args = ["expenses"] |     let opts = defreportopts{patterns_=["expenses"]} | ||||||
|         opts = [] |  | ||||||
|     j <- samplejournal |     j <- samplejournal | ||||||
|     d <- getCurrentDay |     d <- getCurrentDay | ||||||
|     showTransactions opts (optsToFilterSpec opts args d) j `is` unlines |     showTransactions opts (optsToFilterSpec opts d) j `is` unlines | ||||||
|      ["2008/06/03 * eat & shop" |      ["2008/06/03 * eat & shop" | ||||||
|      ,"    expenses:food                $1" |      ,"    expenses:food                $1" | ||||||
|      ,"    expenses:supplies            $1" |      ,"    expenses:supplies            $1" | ||||||
| @ -308,9 +305,10 @@ tests_Hledger_Cli = TestList | |||||||
| 
 | 
 | ||||||
|   , "print report with depth arg" ~: |   , "print report with depth arg" ~: | ||||||
|    do  |    do  | ||||||
|  |     let opts = defreportopts{depth_=Just 2} | ||||||
|     j <- samplejournal |     j <- samplejournal | ||||||
|     d <- getCurrentDay |     d <- getCurrentDay | ||||||
|     showTransactions [] (optsToFilterSpec [Depth "2"] [] d) j `is` unlines |     showTransactions opts (optsToFilterSpec opts d) j `is` unlines | ||||||
|       ["2008/01/01 income" |       ["2008/01/01 income" | ||||||
|       ,"    income:salary           $-1" |       ,"    income:salary           $-1" | ||||||
|       ,"" |       ,"" | ||||||
| @ -338,7 +336,8 @@ tests_Hledger_Cli = TestList | |||||||
|    "register report with no args" ~: |    "register report with no args" ~: | ||||||
|    do  |    do  | ||||||
|     j <- samplejournal |     j <- samplejournal | ||||||
|     (postingsReportAsText [] $ postingsReport [] (optsToFilterSpec [] [] date1) j) `is` unlines |     let opts = defreportopts | ||||||
|  |     (postingsReportAsText opts $ postingsReport opts (optsToFilterSpec opts date1) j) `is` unlines | ||||||
|      ["2008/01/01 income               assets:bank:checking             $1           $1" |      ["2008/01/01 income               assets:bank:checking             $1           $1" | ||||||
|      ,"                                income:salary                   $-1            0" |      ,"                                income:salary                   $-1            0" | ||||||
|      ,"2008/06/01 gift                 assets:bank:checking             $1           $1" |      ,"2008/06/01 gift                 assets:bank:checking             $1           $1" | ||||||
| @ -354,9 +353,9 @@ tests_Hledger_Cli = TestList | |||||||
| 
 | 
 | ||||||
|   ,"register report with cleared option" ~: |   ,"register report with cleared option" ~: | ||||||
|    do  |    do  | ||||||
|     let opts = [Cleared] |     let opts = defreportopts{cleared_=True} | ||||||
|     j <- readJournal' sample_journal_str |     j <- readJournal' sample_journal_str | ||||||
|     (postingsReportAsText opts $ postingsReport opts (optsToFilterSpec opts [] date1) j) `is` unlines |     (postingsReportAsText opts $ postingsReport opts (optsToFilterSpec opts date1) j) `is` unlines | ||||||
|      ["2008/06/03 eat & shop           expenses:food                    $1           $1" |      ["2008/06/03 eat & shop           expenses:food                    $1           $1" | ||||||
|      ,"                                expenses:supplies                $1           $2" |      ,"                                expenses:supplies                $1           $2" | ||||||
|      ,"                                assets:cash                     $-2            0" |      ,"                                assets:cash                     $-2            0" | ||||||
| @ -366,9 +365,9 @@ tests_Hledger_Cli = TestList | |||||||
| 
 | 
 | ||||||
|   ,"register report with uncleared option" ~: |   ,"register report with uncleared option" ~: | ||||||
|    do  |    do  | ||||||
|     let opts = [UnCleared] |     let opts = defreportopts{uncleared_=True} | ||||||
|     j <- readJournal' sample_journal_str |     j <- readJournal' sample_journal_str | ||||||
|     (postingsReportAsText opts $ postingsReport opts (optsToFilterSpec opts [] date1) j) `is` unlines |     (postingsReportAsText opts $ postingsReport opts (optsToFilterSpec opts date1) j) `is` unlines | ||||||
|      ["2008/01/01 income               assets:bank:checking             $1           $1" |      ["2008/01/01 income               assets:bank:checking             $1           $1" | ||||||
|      ,"                                income:salary                   $-1            0" |      ,"                                income:salary                   $-1            0" | ||||||
|      ,"2008/06/01 gift                 assets:bank:checking             $1           $1" |      ,"2008/06/01 gift                 assets:bank:checking             $1           $1" | ||||||
| @ -388,19 +387,22 @@ tests_Hledger_Cli = TestList | |||||||
|         ,"  e  1" |         ,"  e  1" | ||||||
|         ,"  f" |         ,"  f" | ||||||
|         ] |         ] | ||||||
|     registerdates (postingsReportAsText [] $ postingsReport [] (optsToFilterSpec [] [] date1) j) `is` ["2008/01/01","2008/02/02"] |     let opts = defreportopts | ||||||
|  |     registerdates (postingsReportAsText opts $ postingsReport opts (optsToFilterSpec opts date1) j) `is` ["2008/01/01","2008/02/02"] | ||||||
| 
 | 
 | ||||||
|   ,"register report with account pattern" ~: |   ,"register report with account pattern" ~: | ||||||
|    do |    do | ||||||
|     j <- samplejournal |     j <- samplejournal | ||||||
|     (postingsReportAsText [] $ postingsReport [] (optsToFilterSpec [] ["cash"] date1) j) `is` unlines |     let opts = defreportopts{patterns_=["cash"]} | ||||||
|  |     (postingsReportAsText opts $ postingsReport opts (optsToFilterSpec opts date1) j) `is` unlines | ||||||
|      ["2008/06/03 eat & shop           assets:cash                     $-2          $-2" |      ["2008/06/03 eat & shop           assets:cash                     $-2          $-2" | ||||||
|      ] |      ] | ||||||
| 
 | 
 | ||||||
|   ,"register report with account pattern, case insensitive" ~: |   ,"register report with account pattern, case insensitive" ~: | ||||||
|    do  |    do  | ||||||
|     j <- samplejournal |     j <- samplejournal | ||||||
|     (postingsReportAsText [] $ postingsReport [] (optsToFilterSpec [] ["cAsH"] date1) j) `is` unlines |     let opts = defreportopts{patterns_=["cAsH"]} | ||||||
|  |     (postingsReportAsText opts $ postingsReport opts (optsToFilterSpec opts date1) j) `is` unlines | ||||||
|      ["2008/06/03 eat & shop           assets:cash                     $-2          $-2" |      ["2008/06/03 eat & shop           assets:cash                     $-2          $-2" | ||||||
|      ] |      ] | ||||||
| 
 | 
 | ||||||
| @ -408,8 +410,8 @@ tests_Hledger_Cli = TestList | |||||||
|    do  |    do  | ||||||
|     j <- samplejournal |     j <- samplejournal | ||||||
|     let gives displayexpr =  |     let gives displayexpr =  | ||||||
|             (registerdates (postingsReportAsText opts $ postingsReport opts (optsToFilterSpec opts [] date1) j) `is`) |             (registerdates (postingsReportAsText opts $ postingsReport opts (optsToFilterSpec opts date1) j) `is`) | ||||||
|                 where opts = [Display displayexpr] |                 where opts = defreportopts{display_=Just displayexpr} | ||||||
|     "d<[2008/6/2]"  `gives` ["2008/01/01","2008/06/01"] |     "d<[2008/6/2]"  `gives` ["2008/01/01","2008/06/01"] | ||||||
|     "d<=[2008/6/2]" `gives` ["2008/01/01","2008/06/01","2008/06/02"] |     "d<=[2008/6/2]" `gives` ["2008/01/01","2008/06/01","2008/06/02"] | ||||||
|     "d=[2008/6/2]"  `gives` ["2008/06/02"] |     "d=[2008/6/2]"  `gives` ["2008/06/02"] | ||||||
| @ -421,16 +423,16 @@ tests_Hledger_Cli = TestList | |||||||
|     j <- samplejournal |     j <- samplejournal | ||||||
|     let periodexpr `gives` dates = do |     let periodexpr `gives` dates = do | ||||||
|           j' <- samplejournal |           j' <- samplejournal | ||||||
|           registerdates (postingsReportAsText opts $ postingsReport opts (optsToFilterSpec opts [] date1) j') `is` dates |           registerdates (postingsReportAsText opts $ postingsReport opts (optsToFilterSpec opts date1) j') `is` dates | ||||||
|               where opts = [Period periodexpr] |               where opts = defreportopts{period_=maybePeriod date1 periodexpr} | ||||||
|     ""     `gives` ["2008/01/01","2008/06/01","2008/06/02","2008/06/03","2008/12/31"] |     ""     `gives` ["2008/01/01","2008/06/01","2008/06/02","2008/06/03","2008/12/31"] | ||||||
|     "2008" `gives` ["2008/01/01","2008/06/01","2008/06/02","2008/06/03","2008/12/31"] |     "2008" `gives` ["2008/01/01","2008/06/01","2008/06/02","2008/06/03","2008/12/31"] | ||||||
|     "2007" `gives` [] |     "2007" `gives` [] | ||||||
|     "june" `gives` ["2008/06/01","2008/06/02","2008/06/03"] |     "june" `gives` ["2008/06/01","2008/06/02","2008/06/03"] | ||||||
|     "monthly" `gives` ["2008/01/01","2008/06/01","2008/12/01"] |     "monthly" `gives` ["2008/01/01","2008/06/01","2008/12/01"] | ||||||
|     "quarterly" `gives` ["2008/01/01","2008/04/01","2008/10/01"] |     "quarterly" `gives` ["2008/01/01","2008/04/01","2008/10/01"] | ||||||
|     let opts = [Period "yearly"] |     let opts = defreportopts{period_=maybePeriod date1 "yearly"} | ||||||
|     (postingsReportAsText opts $ postingsReport opts (optsToFilterSpec opts [] date1) j) `is` unlines |     (postingsReportAsText opts $ postingsReport opts (optsToFilterSpec opts date1) j) `is` unlines | ||||||
|      ["2008/01/01 - 2008/12/31         assets:bank:saving               $1           $1" |      ["2008/01/01 - 2008/12/31         assets:bank:saving               $1           $1" | ||||||
|      ,"                                assets:cash                     $-2          $-1" |      ,"                                assets:cash                     $-2          $-1" | ||||||
|      ,"                                expenses:food                    $1            0" |      ,"                                expenses:food                    $1            0" | ||||||
| @ -439,18 +441,18 @@ tests_Hledger_Cli = TestList | |||||||
|      ,"                                income:salary                   $-1          $-1" |      ,"                                income:salary                   $-1          $-1" | ||||||
|      ,"                                liabilities:debts                $1            0" |      ,"                                liabilities:debts                $1            0" | ||||||
|      ] |      ] | ||||||
|     let opts = [Period "quarterly"] |     let opts = defreportopts{period_=maybePeriod date1 "quarterly"} | ||||||
|     registerdates (postingsReportAsText opts $ postingsReport opts (optsToFilterSpec opts [] date1) j) `is` ["2008/01/01","2008/04/01","2008/10/01"] |     registerdates (postingsReportAsText opts $ postingsReport opts (optsToFilterSpec opts date1) j) `is` ["2008/01/01","2008/04/01","2008/10/01"] | ||||||
|     let opts = [Period "quarterly",Empty] |     let opts = defreportopts{period_=maybePeriod date1 "quarterly",empty_=True} | ||||||
|     registerdates (postingsReportAsText opts $ postingsReport opts (optsToFilterSpec opts [] date1) j) `is` ["2008/01/01","2008/04/01","2008/07/01","2008/10/01"] |     registerdates (postingsReportAsText opts $ postingsReport opts (optsToFilterSpec opts date1) j) `is` ["2008/01/01","2008/04/01","2008/07/01","2008/10/01"] | ||||||
| 
 | 
 | ||||||
|   ] |   ] | ||||||
| 
 | 
 | ||||||
|   , "register report with depth arg" ~: |   , "register report with depth arg" ~: | ||||||
|    do  |    do  | ||||||
|     j <- samplejournal |     j <- samplejournal | ||||||
|     let opts = [Depth "2"] |     let opts = defreportopts{depth_=Just 2} | ||||||
|     (postingsReportAsText opts $ postingsReport opts (optsToFilterSpec opts [] date1) j) `is` unlines |     (postingsReportAsText opts $ postingsReport opts (optsToFilterSpec opts date1) j) `is` unlines | ||||||
|      ["2008/01/01 income               assets:bank                      $1           $1" |      ["2008/01/01 income               assets:bank                      $1           $1" | ||||||
|      ,"                                income:salary                   $-1            0" |      ,"                                income:salary                   $-1            0" | ||||||
|      ,"2008/06/01 gift                 assets:bank                      $1           $1" |      ,"2008/06/01 gift                 assets:bank                      $1           $1" | ||||||
| @ -471,7 +473,8 @@ tests_Hledger_Cli = TestList | |||||||
|   ,"unicode in balance layout" ~: do |   ,"unicode in balance layout" ~: do | ||||||
|     j <- readJournal' |     j <- readJournal' | ||||||
|       "2009/01/01 * медвежья шкура\n  расходы:покупки  100\n  актив:наличные\n" |       "2009/01/01 * медвежья шкура\n  расходы:покупки  100\n  актив:наличные\n" | ||||||
|     accountsReportAsText [] (accountsReport [] (optsToFilterSpec [] [] date1) j) `is` |     let opts = defreportopts | ||||||
|  |     accountsReportAsText opts (accountsReport opts (optsToFilterSpec opts date1) j) `is` | ||||||
|       ["                -100  актив:наличные" |       ["                -100  актив:наличные" | ||||||
|       ,"                 100  расходы:покупки" |       ,"                 100  расходы:покупки" | ||||||
|       ,"--------------------" |       ,"--------------------" | ||||||
| @ -481,7 +484,8 @@ tests_Hledger_Cli = TestList | |||||||
|   ,"unicode in register layout" ~: do |   ,"unicode in register layout" ~: do | ||||||
|     j <- readJournal' |     j <- readJournal' | ||||||
|       "2009/01/01 * медвежья шкура\n  расходы:покупки  100\n  актив:наличные\n" |       "2009/01/01 * медвежья шкура\n  расходы:покупки  100\n  актив:наличные\n" | ||||||
|     (postingsReportAsText [] $ postingsReport [] (optsToFilterSpec [] [] date1) j) `is` unlines |     let opts = defreportopts | ||||||
|  |     (postingsReportAsText opts $ postingsReport opts (optsToFilterSpec opts date1) j) `is` unlines | ||||||
|       ["2009/01/01 медвежья шкура       расходы:покупки                 100          100" |       ["2009/01/01 медвежья шкура       расходы:покупки                 100          100" | ||||||
|       ,"                                актив:наличные                 -100            0"] |       ,"                                актив:наличные                 -100            0"] | ||||||
| 
 | 
 | ||||||
| @ -921,4 +925,3 @@ journalWithAmounts as = | |||||||
|         [] |         [] | ||||||
|         (TOD 0 0) |         (TOD 0 0) | ||||||
|     where parse = fromparse . parseWithCtx nullctx someamount |     where parse = fromparse . parseWithCtx nullctx someamount | ||||||
| 
 |  | ||||||
|  | |||||||
| @ -49,8 +49,8 @@ data PostingState = PostingState { | |||||||
| -- | Read transactions from the terminal, prompting for each field, | -- | Read transactions from the terminal, prompting for each field, | ||||||
| -- and append them to the journal file. If the journal came from stdin, this | -- and append them to the journal file. If the journal came from stdin, this | ||||||
| -- command has no effect. | -- command has no effect. | ||||||
| add :: [Opt] -> [String] -> Journal -> IO () | add :: CliOpts -> Journal -> IO () | ||||||
| add opts args j | add opts j | ||||||
|     | f == "-" = return () |     | f == "-" = return () | ||||||
|     | otherwise = do |     | otherwise = do | ||||||
|   hPutStrLn stderr $ |   hPutStrLn stderr $ | ||||||
| @ -58,7 +58,7 @@ add opts args j | |||||||
|     ++"To complete a transaction, enter . when prompted for an account.\n" |     ++"To complete a transaction, enter . when prompted for an account.\n" | ||||||
|     ++"To quit, press control-d or control-c." |     ++"To quit, press control-d or control-c." | ||||||
|   today <- getCurrentDay |   today <- getCurrentDay | ||||||
|   getAndAddTransactions j opts args today |   getAndAddTransactions j opts today | ||||||
|         `catch` (\e -> unless (isEOFError e) $ ioError e) |         `catch` (\e -> unless (isEOFError e) $ ioError e) | ||||||
|       where f = journalFilePath j |       where f = journalFilePath j | ||||||
| 
 | 
 | ||||||
| @ -66,29 +66,29 @@ add opts args j | |||||||
| -- validating, displaying and appending them to the journal file, until | -- validating, displaying and appending them to the journal file, until | ||||||
| -- end of input (then raise an EOF exception). Any command-line arguments | -- end of input (then raise an EOF exception). Any command-line arguments | ||||||
| -- are used as the first transaction's description. | -- are used as the first transaction's description. | ||||||
| getAndAddTransactions :: Journal -> [Opt] -> [String] -> Day -> IO () | getAndAddTransactions :: Journal -> CliOpts -> Day -> IO () | ||||||
| getAndAddTransactions j opts args defaultDate = do | getAndAddTransactions j opts defaultDate = do | ||||||
|   (t, d) <- getTransaction j opts args defaultDate |   (t, d) <- getTransaction j opts defaultDate | ||||||
|   j <- journalAddTransaction j opts t |   j <- journalAddTransaction j opts t | ||||||
|   getAndAddTransactions j opts args d |   getAndAddTransactions j opts d | ||||||
| 
 | 
 | ||||||
| -- | Read a transaction from the command line, with history-aware prompting. | -- | Read a transaction from the command line, with history-aware prompting. | ||||||
| getTransaction :: Journal -> [Opt] -> [String] -> Day | getTransaction :: Journal -> CliOpts -> Day | ||||||
|                     -> IO (Transaction,Day) |                     -> IO (Transaction,Day) | ||||||
| getTransaction j opts args defaultDate = do | getTransaction j opts defaultDate = do | ||||||
|   today <- getCurrentDay |   today <- getCurrentDay | ||||||
|   datestr <- runInteractionDefault $ askFor "date"  |   datestr <- runInteractionDefault $ askFor "date"  | ||||||
|             (Just $ showDate defaultDate) |             (Just $ showDate defaultDate) | ||||||
|             (Just $ \s -> null s ||  |             (Just $ \s -> null s ||  | ||||||
|              isRight (parse (smartdate >> many spacenonewline >> eof) "" $ lowercase s)) |              isRight (parse (smartdate >> many spacenonewline >> eof) "" $ lowercase s)) | ||||||
|   description <- runInteractionDefault $ askFor "description" (Just "") Nothing |   description <- runInteractionDefault $ askFor "description" (Just "") Nothing | ||||||
|   let historymatches = transactionsSimilarTo j args description |   let historymatches = transactionsSimilarTo j (patterns_ $ reportopts_ opts) description | ||||||
|       bestmatch | null historymatches = Nothing |       bestmatch | null historymatches = Nothing | ||||||
|                 | otherwise = Just $ snd $ head historymatches |                 | otherwise = Just $ snd $ head historymatches | ||||||
|       bestmatchpostings = maybe Nothing (Just . tpostings) bestmatch |       bestmatchpostings = maybe Nothing (Just . tpostings) bestmatch | ||||||
|       date = fixSmartDate today $ fromparse $ (parse smartdate "" . lowercase) datestr |       date = fixSmartDate today $ fromparse $ (parse smartdate "" . lowercase) datestr | ||||||
|       accept x = x == "." || (not . null) x && |       accept x = x == "." || (not . null) x && | ||||||
|         if NoNewAccts `elem` opts |         if no_new_accounts_ opts | ||||||
|             then isJust $ Foldable.find (== x) ant |             then isJust $ Foldable.find (== x) ant | ||||||
|             else True |             else True | ||||||
|         where (ant,_,_,_) = groupPostings $ journalPostings j |         where (ant,_,_,_) = groupPostings $ journalPostings j | ||||||
| @ -190,11 +190,11 @@ askFor prompt def validator = do | |||||||
| 
 | 
 | ||||||
| -- | Append this transaction to the journal's file, and to the journal's | -- | Append this transaction to the journal's file, and to the journal's | ||||||
| -- transaction list. | -- transaction list. | ||||||
| journalAddTransaction :: Journal -> [Opt] -> Transaction -> IO Journal | journalAddTransaction :: Journal -> CliOpts -> Transaction -> IO Journal | ||||||
| journalAddTransaction j@Journal{jtxns=ts} opts t = do | journalAddTransaction j@Journal{jtxns=ts} opts t = do | ||||||
|   let f = journalFilePath j |   let f = journalFilePath j | ||||||
|   appendToJournalFile f $ showTransaction t |   appendToJournalFile f $ showTransaction t | ||||||
|   when (Debug `elem` opts) $ do |   when (debug_ opts) $ do | ||||||
|     putStrLn $ printf "\nAdded transaction to %s:" f |     putStrLn $ printf "\nAdded transaction to %s:" f | ||||||
|     putStrLn =<< registerFromString (show t) |     putStrLn =<< registerFromString (show t) | ||||||
|   return j{jtxns=ts++[t]} |   return j{jtxns=ts++[t]} | ||||||
| @ -219,8 +219,8 @@ registerFromString :: String -> IO String | |||||||
| registerFromString s = do | registerFromString s = do | ||||||
|   d <- getCurrentDay |   d <- getCurrentDay | ||||||
|   j <- readJournal' s |   j <- readJournal' s | ||||||
|   return $ postingsReportAsText opts $ postingsReport opts (optsToFilterSpec opts [] d) j |   return $ postingsReportAsText opts $ postingsReport opts (optsToFilterSpec opts d) j | ||||||
|     where opts = [Empty] |       where opts = defreportopts{empty_=True} | ||||||
| 
 | 
 | ||||||
| -- | Return a similarity measure, from 0 to 1, for two strings. | -- | Return a similarity measure, from 0 to 1, for two strings. | ||||||
| -- This is Simon White's letter pairs algorithm from | -- This is Simon White's letter pairs algorithm from | ||||||
|  | |||||||
| @ -115,26 +115,27 @@ import Hledger.Cli.Reports | |||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| -- | Print a balance report. | -- | Print a balance report. | ||||||
| balance :: [Opt] -> [String] -> Journal -> IO () | balance :: CliOpts -> Journal -> IO () | ||||||
| balance opts args j = do | balance CliOpts{reportopts_=ropts} j = do | ||||||
|   d <- getCurrentDay |   d <- getCurrentDay | ||||||
|   let lines = case parseFormatFromOpts opts of |   let lines = case formatFromOpts ropts of | ||||||
|             Left err -> [err] |             Left err -> [err] | ||||||
|             Right _ -> accountsReportAsText opts $ accountsReport opts (optsToFilterSpec opts args d) j |             Right _ -> accountsReportAsText ropts $ accountsReport ropts (optsToFilterSpec ropts d) j | ||||||
|   putStr $ unlines lines |   putStr $ unlines lines | ||||||
| 
 | 
 | ||||||
| -- | Render a balance report as plain text suitable for console output. | -- | Render a balance report as plain text suitable for console output. | ||||||
| accountsReportAsText :: [Opt] -> AccountsReport -> [String] | accountsReportAsText :: ReportOpts -> AccountsReport -> [String] | ||||||
| accountsReportAsText opts (items, total) = concat lines ++ t | accountsReportAsText opts (items, total) = concat lines ++ t | ||||||
|     where |     where | ||||||
|       lines = map (accountsReportItemAsText opts format) items |       lines = case formatFromOpts opts of | ||||||
|       format = formatFromOpts opts |                 Right f -> map (accountsReportItemAsText opts f) items | ||||||
|       t = if NoTotal `elem` opts |                 Left err -> [[err]] | ||||||
|              then [] |       t = if no_total_ opts | ||||||
|              else ["--------------------" |            then [] | ||||||
|                     -- TODO: This must use the format somehow |            else ["--------------------" | ||||||
|                   , padleft 20 $ showMixedAmountWithoutPrice total |                  -- TODO: This must use the format somehow | ||||||
|                   ] |                 ,padleft 20 $ showMixedAmountWithoutPrice total | ||||||
|  |                 ] | ||||||
| 
 | 
 | ||||||
| {- | {- | ||||||
| This implementation turned out to be a bit convoluted but implements the following algorithm for formatting: | This implementation turned out to be a bit convoluted but implements the following algorithm for formatting: | ||||||
| @ -147,7 +148,7 @@ This implementation turned out to be a bit convoluted but implements the followi | |||||||
|     b         USD -1  ; Account 'b' has two amounts. The account name is printed on the last line. |     b         USD -1  ; Account 'b' has two amounts. The account name is printed on the last line. | ||||||
| -} | -} | ||||||
| -- | Render one balance report line item as plain text. | -- | Render one balance report line item as plain text. | ||||||
| accountsReportItemAsText :: [Opt] -> [FormatString] -> AccountsReportItem -> [String] | accountsReportItemAsText :: ReportOpts -> [FormatString] -> AccountsReportItem -> [String] | ||||||
| accountsReportItemAsText opts format (_, accountName, depth, Mixed amounts) = | accountsReportItemAsText opts format (_, accountName, depth, Mixed amounts) = | ||||||
|     case amounts of |     case amounts of | ||||||
|       [] -> [] |       [] -> [] | ||||||
| @ -159,7 +160,7 @@ accountsReportItemAsText opts format (_, accountName, depth, Mixed amounts) = | |||||||
|       asText [a]    = [formatAccountsReportItem opts (Just accountName) depth a format] |       asText [a]    = [formatAccountsReportItem opts (Just accountName) depth a format] | ||||||
|       asText (a:as) = (formatAccountsReportItem opts Nothing depth a format) : asText as |       asText (a:as) = (formatAccountsReportItem opts Nothing depth a format) : asText as | ||||||
| 
 | 
 | ||||||
| formatAccountsReportItem :: [Opt] -> Maybe AccountName -> Int -> Amount -> [FormatString] -> String | formatAccountsReportItem :: ReportOpts -> Maybe AccountName -> Int -> Amount -> [FormatString] -> String | ||||||
| formatAccountsReportItem _ _ _ _ [] = "" | formatAccountsReportItem _ _ _ _ [] = "" | ||||||
| formatAccountsReportItem opts accountName depth amount (f:fs) = s ++ (formatAccountsReportItem opts accountName depth amount fs) | formatAccountsReportItem opts accountName depth amount (f:fs) = s ++ (formatAccountsReportItem opts accountName depth amount fs) | ||||||
|   where |   where | ||||||
| @ -167,7 +168,7 @@ formatAccountsReportItem opts accountName depth amount (f:fs) = s ++ (formatAcco | |||||||
|             FormatLiteral l -> l |             FormatLiteral l -> l | ||||||
|             FormatField leftJustified min max field  -> formatAccount opts accountName depth amount leftJustified min max field |             FormatField leftJustified min max field  -> formatAccount opts accountName depth amount leftJustified min max field | ||||||
| 
 | 
 | ||||||
| formatAccount :: [Opt] -> Maybe AccountName -> Int -> Amount -> Bool -> Maybe Int -> Maybe Int -> Field -> String | formatAccount :: ReportOpts -> Maybe AccountName -> Int -> Amount -> Bool -> Maybe Int -> Maybe Int -> Field -> String | ||||||
| formatAccount opts accountName depth balance leftJustified min max field = case field of | formatAccount opts accountName depth balance leftJustified min max field = case field of | ||||||
|         Format.Account  -> formatValue leftJustified min max a |         Format.Account  -> formatValue leftJustified min max a | ||||||
|         DepthSpacer     -> case min of |         DepthSpacer     -> case min of | ||||||
| @ -176,7 +177,7 @@ formatAccount opts accountName depth balance leftJustified min max field = case | |||||||
|         Total           -> formatValue leftJustified min max $ showAmountWithoutPrice balance |         Total           -> formatValue leftJustified min max $ showAmountWithoutPrice balance | ||||||
|         _	        -> "" |         _	        -> "" | ||||||
|     where |     where | ||||||
|       a = maybe "" (accountNameDrop (dropFromOpts opts)) accountName |       a = maybe "" (accountNameDrop (drop_ opts)) accountName | ||||||
| 
 | 
 | ||||||
| tests_Hledger_Cli_Balance = TestList | tests_Hledger_Cli_Balance = TestList | ||||||
|  [ |  [ | ||||||
|  | |||||||
| @ -8,8 +8,7 @@ import Prelude hiding (getContents) | |||||||
| import Control.Monad (when, guard, liftM) | import Control.Monad (when, guard, liftM) | ||||||
| import Data.Maybe | import Data.Maybe | ||||||
| import Data.Time.Format (parseTime) | import Data.Time.Format (parseTime) | ||||||
| import Safe (atDef, atMay, maximumDef) | import Safe | ||||||
| import Safe (readDef, readMay) |  | ||||||
| import System.Directory (doesFileExist) | import System.Directory (doesFileExist) | ||||||
| import System.Exit (exitFailure) | import System.Exit (exitFailure) | ||||||
| import System.FilePath (takeBaseName, replaceExtension) | import System.FilePath (takeBaseName, replaceExtension) | ||||||
| @ -23,13 +22,14 @@ import Text.Printf (hPrintf) | |||||||
| import Hledger.Cli.Format | import Hledger.Cli.Format | ||||||
| import qualified Hledger.Cli.Format as Format | import qualified Hledger.Cli.Format as Format | ||||||
| import Hledger.Cli.Version | import Hledger.Cli.Version | ||||||
| import Hledger.Cli.Options (Opt(Debug), progname_cli, rulesFileFromOpts) | import Hledger.Cli.Options | ||||||
|  | import Hledger.Cli.Reports | ||||||
| import Hledger.Data.Amount (nullmixedamt, costOfMixedAmount) | import Hledger.Data.Amount (nullmixedamt, costOfMixedAmount) | ||||||
| import Hledger.Data.Dates (firstJust, showDate, parsedate) | import Hledger.Data.Dates (firstJust, showDate, parsedate) | ||||||
| import Hledger.Data (Journal,AccountName,Transaction(..),Posting(..),PostingType(..)) | import Hledger.Data (Journal,AccountName,Transaction(..),Posting(..),PostingType(..)) | ||||||
| import Hledger.Data.Journal (nullctx) | import Hledger.Data.Journal (nullctx) | ||||||
| import Hledger.Read.JournalReader (someamount,ledgeraccountname) | import Hledger.Read.JournalReader (someamount,ledgeraccountname) | ||||||
| import Hledger.Utils (choice', strip, spacenonewline, restofline, parseWithCtx, assertParse, assertParseEqual, error', regexMatchesCI, regexReplaceCI) | import Hledger.Utils | ||||||
| import Hledger.Utils.UTF8 (getContents) | import Hledger.Utils.UTF8 (getContents) | ||||||
| 
 | 
 | ||||||
| {- | | {- | | ||||||
| @ -84,20 +84,19 @@ type CsvRecord = [String] | |||||||
| 
 | 
 | ||||||
| -- | Read the CSV file named as an argument and print equivalent journal transactions, | -- | Read the CSV file named as an argument and print equivalent journal transactions, | ||||||
| -- using/creating a .rules file. | -- using/creating a .rules file. | ||||||
| convert :: [Opt] -> [String] -> Journal -> IO () | convert :: CliOpts -> Journal -> IO () | ||||||
| convert opts args _ = do | convert opts _ = do | ||||||
|   when (null args) $ error' "please specify a csv data file." |   let csvfile = headDef "" $ patterns_ $ reportopts_ opts | ||||||
|   let csvfile = head args |   when (null csvfile) $ error' "please specify a csv data file." | ||||||
|   let  |   let  | ||||||
|     rulesFileSpecified = isJust $ rulesFileFromOpts opts |     rulesFileSpecified = isJust $ rules_file_ opts | ||||||
|  |     rulesfile = rulesFileFor opts csvfile | ||||||
|     usingStdin = csvfile == "-" |     usingStdin = csvfile == "-" | ||||||
|   when (usingStdin && (not rulesFileSpecified)) $ error' "please specify a files file when converting stdin" |   when (usingStdin && (not rulesFileSpecified)) $ error' "please specify a files file when converting stdin" | ||||||
|   csvparse <- parseCsv csvfile |   csvparse <- parseCsv csvfile | ||||||
|   let records = case csvparse of |   let records = case csvparse of | ||||||
|                   Left e -> error' $ show e |                   Left e -> error' $ show e | ||||||
|                   Right rs -> reverse $ filter (/= [""]) rs |                   Right rs -> reverse $ filter (/= [""]) rs | ||||||
|   let debug = Debug `elem` opts |  | ||||||
|       rulesfile = rulesFileFor opts csvfile |  | ||||||
|   exists <- doesFileExist rulesfile |   exists <- doesFileExist rulesfile | ||||||
|   if (not exists) then do |   if (not exists) then do | ||||||
|                   hPrintf stderr "creating conversion rules file %s, edit this file for better results\n" rulesfile |                   hPrintf stderr "creating conversion rules file %s, edit this file for better results\n" rulesfile | ||||||
| @ -106,12 +105,12 @@ convert opts args _ = do | |||||||
|       hPrintf stderr "using conversion rules file %s\n" rulesfile |       hPrintf stderr "using conversion rules file %s\n" rulesfile | ||||||
|   rules <- liftM (either (error'.show) id) $ parseCsvRulesFile rulesfile |   rules <- liftM (either (error'.show) id) $ parseCsvRulesFile rulesfile | ||||||
|   let invalid = validateRules rules |   let invalid = validateRules rules | ||||||
|   when debug $ hPrintf stderr "rules: %s\n" (show rules) |   when (debug_ opts) $ hPrintf stderr "rules: %s\n" (show rules) | ||||||
|   when (isJust invalid) $ error (fromJust invalid) |   when (isJust invalid) $ error (fromJust invalid) | ||||||
|   let requiredfields = max 2 (maxFieldIndex rules + 1) |   let requiredfields = max 2 (maxFieldIndex rules + 1) | ||||||
|       badrecords = take 1 $ filter ((< requiredfields).length) records |       badrecords = take 1 $ filter ((< requiredfields).length) records | ||||||
|   if null badrecords |   if null badrecords | ||||||
|    then mapM_ (printTxn debug rules) records |    then mapM_ (printTxn (debug_ opts) rules) records | ||||||
|    else do |    else do | ||||||
|      hPrintf stderr (unlines [ |      hPrintf stderr (unlines [ | ||||||
|                       "Warning, at least one CSV record does not contain a field referenced by the" |                       "Warning, at least one CSV record does not contain a field referenced by the" | ||||||
| @ -142,17 +141,13 @@ maxFieldIndex r = maximumDef (-1) $ catMaybes [ | |||||||
|                   ,effectiveDateField r |                   ,effectiveDateField r | ||||||
|                   ] |                   ] | ||||||
| 
 | 
 | ||||||
| rulesFileFor :: [Opt] -> FilePath -> FilePath | rulesFileFor :: CliOpts -> FilePath -> FilePath | ||||||
| rulesFileFor opts csvfile =  | rulesFileFor CliOpts{rules_file_=Just f} _ = f | ||||||
|     case opt of | rulesFileFor CliOpts{rules_file_=Nothing} csvfile = replaceExtension csvfile ".rules" | ||||||
|       Just path -> path |  | ||||||
|       Nothing   -> replaceExtension csvfile ".rules" |  | ||||||
|     where |  | ||||||
|       opt = rulesFileFromOpts opts |  | ||||||
| 
 | 
 | ||||||
| initialRulesFileContent :: String | initialRulesFileContent :: String | ||||||
| initialRulesFileContent = | initialRulesFileContent = | ||||||
|     "# csv conversion rules file generated by "++(progversionstr progname_cli)++"\n" ++ |     "# csv conversion rules file generated by "++(progversionstr progname)++"\n" ++ | ||||||
|     "# Add rules to this file for more accurate conversion, see\n"++ |     "# Add rules to this file for more accurate conversion, see\n"++ | ||||||
|     "# http://hledger.org/MANUAL.html#convert\n" ++ |     "# http://hledger.org/MANUAL.html#convert\n" ++ | ||||||
|     "\n" ++ |     "\n" ++ | ||||||
|  | |||||||
| @ -13,6 +13,7 @@ import Data.Ord | |||||||
| import Text.Printf | import Text.Printf | ||||||
| 
 | 
 | ||||||
| import Hledger.Cli.Options | import Hledger.Cli.Options | ||||||
|  | import Hledger.Cli.Reports | ||||||
| import Hledger.Data | import Hledger.Data | ||||||
| import Prelude hiding (putStr) | import Prelude hiding (putStr) | ||||||
| import Hledger.Utils.UTF8 (putStr) | import Hledger.Utils.UTF8 (putStr) | ||||||
| @ -22,12 +23,12 @@ barchar = '*' | |||||||
| 
 | 
 | ||||||
| -- | Print a histogram of some statistic per reporting interval, such as | -- | Print a histogram of some statistic per reporting interval, such as | ||||||
| -- number of postings per day. | -- number of postings per day. | ||||||
| histogram :: [Opt] -> [String] -> Journal -> IO () | histogram :: CliOpts -> Journal -> IO () | ||||||
| histogram opts args j = do | histogram CliOpts{reportopts_=reportopts_} j = do | ||||||
|   d <- getCurrentDay |   d <- getCurrentDay | ||||||
|   putStr $ showHistogram opts (optsToFilterSpec opts args d) j |   putStr $ showHistogram reportopts_ (optsToFilterSpec reportopts_ d) j | ||||||
| 
 | 
 | ||||||
| showHistogram :: [Opt] -> FilterSpec -> Journal -> String | showHistogram :: ReportOpts -> FilterSpec -> Journal -> String | ||||||
| showHistogram opts filterspec j = concatMap (printDayWith countBar) spanps | showHistogram opts filterspec j = concatMap (printDayWith countBar) spanps | ||||||
|     where |     where | ||||||
|       i = intervalFromOpts opts |       i = intervalFromOpts opts | ||||||
| @ -40,13 +41,13 @@ showHistogram opts filterspec j = concatMap (printDayWith countBar) spanps | |||||||
|       -- should count transactions, not postings ? |       -- should count transactions, not postings ? | ||||||
|       ps = sortBy (comparing postingDate) $ filterempties $ filter matchapats $ filterdepth $ journalPostings j |       ps = sortBy (comparing postingDate) $ filterempties $ filter matchapats $ filterdepth $ journalPostings j | ||||||
|       filterempties |       filterempties | ||||||
|           | Empty `elem` opts = id |           | empty_ opts = id | ||||||
|           | otherwise = filter (not . isZeroMixedAmount . pamount) |           | otherwise = filter (not . isZeroMixedAmount . pamount) | ||||||
|       matchapats = matchpats apats . paccount |       matchapats = matchpats apats . paccount | ||||||
|       apats = acctpats filterspec |       apats = acctpats filterspec | ||||||
|       filterdepth | interval == NoInterval = filter (\p -> accountNameLevel (paccount p) <= depth) |       filterdepth | interval == NoInterval = filter (\p -> accountNameLevel (paccount p) <= depth) | ||||||
|                   | otherwise = id |                   | otherwise = id | ||||||
|       depth = fromMaybe 99999 $ depthFromOpts opts |       depth = fromMaybe 99999 $ depth_ opts | ||||||
| 
 | 
 | ||||||
| printDayWith f (DateSpan b _, ts) = printf "%s %s\n" (show $ fromJust b) (f ts) | printDayWith f (DateSpan b _, ts) = printf "%s %s\n" (show $ fromJust b) (f ts) | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -39,7 +39,9 @@ See "Hledger.Data.Ledger" for more examples. | |||||||
| 
 | 
 | ||||||
| module Hledger.Cli.Main where | module Hledger.Cli.Main where | ||||||
| 
 | 
 | ||||||
|  | import Control.Monad | ||||||
| import Data.List | import Data.List | ||||||
|  | import Text.Printf | ||||||
| 
 | 
 | ||||||
| import Hledger.Cli.Add | import Hledger.Cli.Add | ||||||
| import Hledger.Cli.Balance | import Hledger.Cli.Balance | ||||||
| @ -52,38 +54,49 @@ import Hledger.Cli.Options | |||||||
| import Hledger.Cli.Tests | import Hledger.Cli.Tests | ||||||
| import Hledger.Cli.Utils | import Hledger.Cli.Utils | ||||||
| import Hledger.Cli.Version | import Hledger.Cli.Version | ||||||
| import Hledger.Utils |  | ||||||
| import Prelude hiding (putStr, putStrLn) |  | ||||||
| import Hledger.Utils.UTF8 (putStr, putStrLn) |  | ||||||
| 
 | 
 | ||||||
| main :: IO () | main :: IO () | ||||||
| main = do | main = do | ||||||
|   (opts, args) <- parseArgumentsWith options_cli |   opts <- getHledgerOpts | ||||||
|   case validateOpts opts of |   when (debug_ opts) $ printf "%s\n" progversion >> printf "opts: %s\n" (show opts) | ||||||
|     Just err -> error' err |   runWith opts | ||||||
|     Nothing -> run opts args |  | ||||||
| 
 | 
 | ||||||
| run opts args = | runWith :: CliOpts -> IO () | ||||||
|   run opts args | runWith opts = run' opts | ||||||
|     where |     where  | ||||||
|       run opts _ |       cmd = command_ opts | ||||||
|        | Help `elem` opts             = putStr usage_cli |       run' opts | ||||||
|        | Version `elem` opts          = putStrLn $ progversionstr progname_cli |           | null cmd                                       = printModeHelpAndExit mainmode | ||||||
|        | BinaryFilename `elem` opts   = putStrLn $ binaryfilename progname_cli |           | any (cmd `isPrefixOf`) ["accounts","balance"]  = showModeHelpOr accountsmode $ withJournalDo opts balance | ||||||
|       run _ []                        = argsError "a command is required." |           | any (cmd `isPrefixOf`) ["activity","histogram"] = showModeHelpOr activitymode $ withJournalDo opts histogram | ||||||
|       run opts (cmd:args) |           | cmd `isPrefixOf` "add"                         = showModeHelpOr addmode $ withJournalDo opts add | ||||||
|        | cmd `isPrefixOf` "balance"   = withJournalDo opts args cmd balance |           | cmd `isPrefixOf` "convert"                     = showModeHelpOr convertmode $ withJournalDo opts convert | ||||||
|        | cmd `isPrefixOf` "convert"   = withJournalDo opts args cmd convert |           | any (cmd `isPrefixOf`) ["entries","print"]     = showModeHelpOr entriesmode $ withJournalDo opts print' | ||||||
|        | cmd `isPrefixOf` "print"     = withJournalDo opts args cmd print' |           | any (cmd `isPrefixOf`) ["postings","register"] = showModeHelpOr postingsmode $ withJournalDo opts register | ||||||
|        | cmd `isPrefixOf` "register"  = withJournalDo opts args cmd register |           | cmd `isPrefixOf` "stats"                       = showModeHelpOr statsmode $ withJournalDo opts stats | ||||||
|        | cmd `isPrefixOf` "histogram" = withJournalDo opts args cmd histogram |           | cmd `isPrefixOf` "test"                        = showModeHelpOr testmode $ runtests opts >> return () | ||||||
|        | cmd `isPrefixOf` "add"       = withJournalDo opts args cmd add |           | cmd `isPrefixOf` "binaryfilename"              = showModeHelpOr binaryfilenamemode $ putStrLn $ binaryfilename progname | ||||||
|        | cmd `isPrefixOf` "stats"     = withJournalDo opts args cmd stats |           | otherwise                                      = showModeHelpOr mainmode $ optserror $ "command "++cmd++" is not recognized" | ||||||
|        | cmd `isPrefixOf` "test"      = runtests opts args >> return () |       showModeHelpOr mode f = do | ||||||
|        | otherwise                    = argsError $ "command "++cmd++" is unrecognized." |         when ("help" `in_` (rawopts_ opts)) $ printModeHelpAndExit mode | ||||||
|  |         when ("version" `in_` (rawopts_ opts)) $ printVersionAndExit | ||||||
|  |         f | ||||||
| 
 | 
 | ||||||
| validateOpts :: [Opt] -> Maybe String | {- tests: | ||||||
| validateOpts opts = | 
 | ||||||
|   case parseFormatFromOpts opts of | hledger -> main help | ||||||
|     Left err -> Just $ unlines ["Invalid format", err] | hledger --help -> main help | ||||||
|     Right _ -> Nothing | hledger --help command -> command help | ||||||
|  | hledger command --help -> command help | ||||||
|  | hledger badcommand -> unrecognized command, try --help (non-zero exit) | ||||||
|  | hledger badcommand --help -> main help | ||||||
|  | hledger --help badcommand -> main help | ||||||
|  | hledger --mainflag command -> works | ||||||
|  | hledger command --mainflag -> works | ||||||
|  | hledger command --commandflag -> works | ||||||
|  | hledger command --mainflag --commandflag -> works | ||||||
|  | XX hledger --mainflag command --commandflag -> works | ||||||
|  | XX hledger --commandflag command -> works | ||||||
|  | XX hledger --commandflag command --mainflag -> works | ||||||
|  | 
 | ||||||
|  | -} | ||||||
| @ -1,234 +1,401 @@ | |||||||
| {-| | {-| | ||||||
| Command-line options for the application. | 
 | ||||||
|  | Command-line options for the hledger program, and option-parsing utilities. | ||||||
|  | 
 | ||||||
| -} | -} | ||||||
| 
 | 
 | ||||||
| module Hledger.Cli.Options | module Hledger.Cli.Options | ||||||
| where | where | ||||||
| import Data.Char (toLower) |  | ||||||
| import Data.List | import Data.List | ||||||
| import Data.Maybe | import Data.Maybe | ||||||
| import Data.Time.Calendar | import Data.Time.Calendar | ||||||
| import System.Console.GetOpt | import Safe | ||||||
|  | import System.Console.CmdArgs | ||||||
|  | import System.Console.CmdArgs.Explicit | ||||||
|  | import System.Console.CmdArgs.Text | ||||||
| import System.Environment | import System.Environment | ||||||
|  | import System.Exit | ||||||
| import Test.HUnit | import Test.HUnit | ||||||
| 
 | 
 | ||||||
| import Hledger.Data |  | ||||||
| import Hledger.Cli.Format as Format | import Hledger.Cli.Format as Format | ||||||
| import Hledger.Read (myJournalPath, myTimelogPath) | import Hledger.Cli.Reports | ||||||
|  | import Hledger.Cli.Version | ||||||
|  | import Hledger.Data | ||||||
|  | import Hledger.Read | ||||||
| import Hledger.Utils | import Hledger.Utils | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| progname_cli = "hledger" | progname = "hledger" | ||||||
|  | progversion = progversionstr progname | ||||||
| 
 | 
 | ||||||
| -- | The program name which, if we are invoked as (via symlink or | -- 1. cmdargs mode and flag definitions, for the main and subcommand modes. | ||||||
| -- renaming), causes us to default to reading the user's time log instead | -- Flag values are parsed initially to simple RawOpts to permit reuse. | ||||||
| -- of their journal. |  | ||||||
| progname_cli_time  = "hours" |  | ||||||
| 
 | 
 | ||||||
| usage_preamble_cli = | type RawOpts = [(String,String)] | ||||||
|   "Usage: hledger [OPTIONS] COMMAND [PATTERNS]\n" ++ |  | ||||||
|   "       hledger [OPTIONS] convert CSVFILE\n" ++ |  | ||||||
|   "\n" ++ |  | ||||||
|   "Reads your ~/.journal file, or another specified by $LEDGER or -f, and\n" ++ |  | ||||||
|   "runs the specified command (may be abbreviated):\n" ++ |  | ||||||
|   "\n" ++ |  | ||||||
|   "  add       - prompt for new transactions and add them to the journal\n" ++ |  | ||||||
|   "  balance   - show accounts, with balances\n" ++ |  | ||||||
|   "  convert   - show the specified CSV file as a hledger journal\n" ++ |  | ||||||
|   "  histogram - show a barchart of transactions per day or other interval\n" ++ |  | ||||||
|   "  print     - show transactions in journal format\n" ++ |  | ||||||
|   "  register  - show transactions as a register with running balance\n" ++ |  | ||||||
|   "  stats     - show various statistics for a journal\n" ++ |  | ||||||
|   "  test      - run self-tests\n" ++ |  | ||||||
|   "\n" |  | ||||||
| 
 | 
 | ||||||
| usage_options_cli = usageInfo "hledger options:" options_cli | defmode :: Mode RawOpts | ||||||
|  | defmode =   Mode { | ||||||
|  |   modeNames = [] | ||||||
|  |  ,modeHelp = "" | ||||||
|  |  ,modeHelpSuffix = [] | ||||||
|  |  ,modeValue = [] | ||||||
|  |  ,modeCheck = Right | ||||||
|  |  ,modeReform = const Nothing | ||||||
|  |  ,modeGroupFlags = toGroup [] | ||||||
|  |  ,modeArgs = Nothing | ||||||
|  |  ,modeGroupModes = toGroup [] | ||||||
|  |  } | ||||||
| 
 | 
 | ||||||
| usage_postscript_cli = | mainmode = defmode { | ||||||
|  "\n" ++ |   modeNames = [progname] | ||||||
|  "DATES can be y/m/d or smart dates like \"last month\".  PATTERNS are regular\n" ++ |  ,modeHelp = "run the specified hledger command. hledger COMMAND --help for more detail. When mixing general and command-specific flags, put them all after COMMAND." | ||||||
|  "expressions which filter by account name.  Prefix a pattern with desc: to\n" ++ |  ,modeHelpSuffix = help_postscript | ||||||
|  "filter by transaction description instead, prefix with not: to negate it.\n" ++ |  ,modeGroupFlags = Group { | ||||||
|  "When using both, not: comes last.\n" |      groupUnnamed = [] | ||||||
|  |     ,groupHidden = [] | ||||||
|  |     ,groupNamed = [(generalflagstitle, generalflags1)] | ||||||
|  |     } | ||||||
|  |  ,modeArgs = Just mainargsflag | ||||||
|  |  ,modeGroupModes = Group { | ||||||
|  |      groupUnnamed = [ | ||||||
|  |      ] | ||||||
|  |     ,groupHidden = [ | ||||||
|  |       binaryfilenamemode | ||||||
|  |      ] | ||||||
|  |     ,groupNamed = [ | ||||||
|  |       ("Misc commands", [ | ||||||
|  |         addmode | ||||||
|  |        ,convertmode | ||||||
|  |        ,testmode | ||||||
|  |        ]) | ||||||
|  |      ,("\nReport commands", [ | ||||||
|  |         accountsmode | ||||||
|  |        ,entriesmode | ||||||
|  |        ,postingsmode | ||||||
|  |        -- ,transactionsmode | ||||||
|  |        ,activitymode | ||||||
|  |        ,statsmode | ||||||
|  |        ]) | ||||||
|  |      ] | ||||||
|  |     } | ||||||
|  |  } | ||||||
| 
 | 
 | ||||||
| usage_cli = concat [ | help_postscript = [ | ||||||
|              usage_preamble_cli |   -- "DATES can be Y/M/D or smart dates like \"last month\"." | ||||||
|             ,usage_options_cli |   -- ,"PATTERNS are regular" | ||||||
|             ,usage_postscript_cli |   -- ,"expressions which filter by account name.  Prefix a pattern with desc: to" | ||||||
|             ] |   -- ,"filter by transaction description instead, prefix with not: to negate it." | ||||||
| 
 |   -- ,"When using both, not: comes last." | ||||||
| -- | Command-line options we accept. |  | ||||||
| options_cli :: [OptDescr Opt] |  | ||||||
| options_cli = [ |  | ||||||
|   Option "f" ["file"]         (ReqArg File "FILE")   "use a different journal/timelog file; - means stdin" |  | ||||||
|  ,Option "b" ["begin"]        (ReqArg Begin "DATE")  "report on transactions on or after this date" |  | ||||||
|  ,Option "e" ["end"]          (ReqArg End "DATE")    "report on transactions before this date" |  | ||||||
|  ,Option "p" ["period"]       (ReqArg Period "EXPR") ("report on transactions during the specified period\n" ++ |  | ||||||
|                                                       "and/or with the specified reporting interval\n") |  | ||||||
|  ,Option "C" ["cleared"]      (NoArg  Cleared)       "report only on cleared transactions" |  | ||||||
|  ,Option "U" ["uncleared"]    (NoArg  UnCleared)     "report only on uncleared transactions" |  | ||||||
|  ,Option "B" ["cost","basis"] (NoArg  CostBasis)     "report cost of commodities" |  | ||||||
|  ,Option ""  ["alias"]        (ReqArg Alias "ACCT=ALIAS")  "display ACCT's name as ALIAS in reports" |  | ||||||
|  ,Option ""  ["depth"]        (ReqArg Depth "N")     "hide accounts/transactions deeper than this" |  | ||||||
|  ,Option "d" ["display"]      (ReqArg Display "EXPR") ("show only transactions matching EXPR (where\n" ++ |  | ||||||
|                                                        "EXPR is 'dOP[DATE]' and OP is <, <=, =, >=, >)") |  | ||||||
|  ,Option ""  ["effective"]    (NoArg  Effective)     "use transactions' effective dates, if any" |  | ||||||
|  ,Option "E" ["empty"]        (NoArg  Empty)         "show empty/zero things which are normally elided" |  | ||||||
|  ,Option ""  ["no-elide"]     (NoArg  NoElide)       "no eliding at all, stronger than -E (eg for balance report)" |  | ||||||
|  ,Option "R" ["real"]         (NoArg  Real)          "report only on real (non-virtual) transactions" |  | ||||||
|  ,Option ""  ["flat"]         (NoArg  Flat)          "balance: show full account names, unindented" |  | ||||||
|  ,Option ""  ["drop"]         (ReqArg Drop "N")      "balance: with --flat, elide first N account name components" |  | ||||||
|  ,Option ""  ["no-total"]     (NoArg  NoTotal)       "balance: hide the final total" |  | ||||||
|  ,Option "D" ["daily"]        (NoArg  DailyOpt)      "register, stats: report by day" |  | ||||||
|  ,Option "W" ["weekly"]       (NoArg  WeeklyOpt)     "register, stats: report by week" |  | ||||||
|  ,Option "M" ["monthly"]      (NoArg  MonthlyOpt)    "register, stats: report by month" |  | ||||||
|  ,Option "Q" ["quarterly"]    (NoArg  QuarterlyOpt)  "register, stats: report by quarter" |  | ||||||
|  ,Option "Y" ["yearly"]       (NoArg  YearlyOpt)     "register, stats: report by year" |  | ||||||
|  ,Option ""  ["no-new-accounts"] (NoArg NoNewAccts)  "add: don't allow creating new accounts" |  | ||||||
|  ,Option "r" ["rules"]        (ReqArg RulesFile "FILE") "convert: rules file to use (default:JOURNAL.rules)" |  | ||||||
|  ,Option "F" ["format"]       (ReqArg ReportFormat "STR") "use STR as the format" |  | ||||||
|  ,Option "v" ["verbose"]      (NoArg  Verbose)       "show more verbose output" |  | ||||||
|  ,Option ""  ["debug"]        (NoArg  Debug)         "show extra debug output; implies verbose" |  | ||||||
|  ,Option ""  ["binary-filename"] (NoArg BinaryFilename) "show the download filename for this hledger build" |  | ||||||
|  ,Option "V" ["version"]      (NoArg  Version)       "show version information" |  | ||||||
|  ,Option "h" ["help"]         (NoArg  Help)          "show command-line usage" |  | ||||||
|  ] |  ] | ||||||
| 
 | 
 | ||||||
| -- | An option value from a command-line flag. | generalflagstitle = "\nGeneral flags" | ||||||
| data Opt =  | generalflags1 = fileflags ++ reportflags ++ helpflags | ||||||
|     File          {value::String} | generalflags2 = fileflags ++ helpflags | ||||||
|     | NoNewAccts | generalflags3 = helpflags | ||||||
|     | Begin       {value::String} |  | ||||||
|     | End         {value::String} |  | ||||||
|     | Period      {value::String} |  | ||||||
|     | Cleared |  | ||||||
|     | UnCleared |  | ||||||
|     | CostBasis |  | ||||||
|     | Alias       {value::String} |  | ||||||
|     | Depth       {value::String} |  | ||||||
|     | Display     {value::String} |  | ||||||
|     | Effective |  | ||||||
|     | Empty |  | ||||||
|     | NoElide |  | ||||||
|     | Real |  | ||||||
|     | Flat |  | ||||||
|     | Drop        {value::String} |  | ||||||
|     | NoTotal |  | ||||||
|     | DailyOpt |  | ||||||
|     | WeeklyOpt |  | ||||||
|     | MonthlyOpt |  | ||||||
|     | QuarterlyOpt |  | ||||||
|     | YearlyOpt |  | ||||||
|     | RulesFile   {value::String} |  | ||||||
|     | ReportFormat {value::String} |  | ||||||
|     | Help |  | ||||||
|     | Verbose |  | ||||||
|     | Version |  | ||||||
|     | BinaryFilename |  | ||||||
|     | Debug |  | ||||||
|     -- XXX add-on options, must be defined here for now |  | ||||||
|     -- vty |  | ||||||
|     | DebugVty |  | ||||||
|     -- web |  | ||||||
|     | BaseUrl     {value::String} |  | ||||||
|     | Port        {value::String} |  | ||||||
|     -- chart |  | ||||||
|     | ChartOutput {value::String} |  | ||||||
|     | ChartItems  {value::String} |  | ||||||
|     | ChartSize   {value::String} |  | ||||||
|     deriving (Show,Eq) |  | ||||||
| 
 | 
 | ||||||
| -- these make me nervous | fileflags = [ | ||||||
| optsWithConstructor f opts = concatMap get opts |   flagReq ["file","f"]  (\s opts -> Right $ setopt "file" s opts) "FILE" "use a different journal file; - means stdin" | ||||||
|     where get o = [o | f v == o] where v = value o |  ,flagReq ["alias"]  (\s opts -> Right $ setopt "alias" s opts)  "ACCT=ALIAS" "display ACCT's name as ALIAS in reports" | ||||||
|  |  ] | ||||||
| 
 | 
 | ||||||
| optsWithConstructors fs opts = concatMap get opts | reportflags = [ | ||||||
|     where get o = [o | any (== o) fs] |   flagReq  ["begin","b"]     (\s opts -> Right $ setopt "begin" s opts) "DATE" "report on transactions on or after this date" | ||||||
|  |  ,flagReq  ["end","e"]       (\s opts -> Right $ setopt "end" s opts) "DATE" "report on transactions before this date" | ||||||
|  |  ,flagReq  ["period","p"]    (\s opts -> Right $ setopt "period" s opts) "PERIODEXPR" "report on transactions during the specified period and/or with the specified reporting interval" | ||||||
|  |  ,flagNone ["daily","D"]     (\opts -> setboolopt "daily" opts) "report by day" | ||||||
|  |  ,flagNone ["weekly","W"]    (\opts -> setboolopt "weekly" opts) "report by week" | ||||||
|  |  ,flagNone ["monthly","M"]   (\opts -> setboolopt "monthly" opts) "report by month" | ||||||
|  |  ,flagNone ["quarterly","Q"] (\opts -> setboolopt "quarterly" opts) "report by quarter" | ||||||
|  |  ,flagNone ["yearly","Y"]    (\opts -> setboolopt "yearly" opts) "report by year" | ||||||
|  |  ,flagNone ["cleared","C"]   (\opts -> setboolopt "cleared" opts) "report only on cleared transactions" | ||||||
|  |  ,flagNone ["uncleared","U"] (\opts -> setboolopt "uncleared" opts) "report only on uncleared transactions" | ||||||
|  |  ,flagNone ["cost","B"]      (\opts -> setboolopt "cost" opts) "report cost of commodities" | ||||||
|  |  ,flagReq  ["depth"]         (\s opts -> Right $ setopt "depth" s opts) "N" "hide accounts/transactions deeper than this" | ||||||
|  |  ,flagReq  ["display","d"]   (\s opts -> Right $ setopt "display" s opts) "DISPLAYEXPR" "show only transactions matching the expr, which is 'dOP[DATE]' where OP is <, <=, =, >=, >" | ||||||
|  |  ,flagNone ["effective"]     (\opts -> setboolopt "effective" opts) "use transactions' effective dates, if any" | ||||||
|  |  ,flagNone ["empty","E"]     (\opts -> setboolopt "empty" opts) "show empty/zero things which are normally elided" | ||||||
|  |  ,flagNone ["real","R"]      (\opts -> setboolopt "real" opts) "report only on real (non-virtual) transactions" | ||||||
|  |  ] | ||||||
| 
 | 
 | ||||||
| optValuesForConstructor f opts = concatMap get opts | helpflags = [ | ||||||
|     where get o = [v | f v == o] where v = value o |   flagHelpSimple (setboolopt "help") | ||||||
|  |  ,flagNone ["debug"] (setboolopt "debug") "Show extra debug output" | ||||||
|  |  ,flagVersion (setboolopt "version") | ||||||
|  |  ] | ||||||
| 
 | 
 | ||||||
| optValuesForConstructors fs opts = concatMap get opts | mainargsflag = flagArg f "" | ||||||
|     where get o = [v | any (\f -> f v == o) fs] where v = value o |     where f s opts = let as = words' s | ||||||
|  |                          cmd = headDef "" as | ||||||
|  |                          args = drop (length cmd + 1) s | ||||||
|  |                      in Right $ setopt "command" cmd $ setopt "args" args opts | ||||||
| 
 | 
 | ||||||
| -- | Parse the command-line arguments into options and arguments using the | commandargsflag = flagArg (\s opts -> Right $ setopt "args" s opts) "[PATTERNS]" | ||||||
| -- specified option descriptors. Any smart dates in the options are |  | ||||||
| -- converted to explicit YYYY/MM/DD format based on the current time. If |  | ||||||
| -- parsing fails, raise an error, displaying the problem along with the |  | ||||||
| -- provided usage string. |  | ||||||
| parseArgumentsWith :: [OptDescr Opt] -> IO ([Opt], [String]) |  | ||||||
| parseArgumentsWith options = do |  | ||||||
|   rawargs <- map fromPlatformString `fmap` getArgs |  | ||||||
|   parseArgumentsWith' options rawargs |  | ||||||
| 
 | 
 | ||||||
| parseArgumentsWith' options rawargs = do | commandmode names = defmode {modeNames=names, modeValue=[("command",headDef "" names)]} | ||||||
|   let (opts,args,errs) = getOpt Permute options rawargs |  | ||||||
|   opts' <- fixOptDates opts |  | ||||||
|   let opts'' = if Debug `elem` opts' then Verbose:opts' else opts' |  | ||||||
|   if null errs |  | ||||||
|    then return (opts'',args) |  | ||||||
|    else argsError (concat errs) >> return ([],[]) |  | ||||||
| 
 | 
 | ||||||
| argsError :: String -> IO () | addmode = (commandmode ["add"]) { | ||||||
| argsError = ioError . userError' . (++ " Run with --help to see usage.") |   modeHelp = "prompt for new transactions and append them to the journal" | ||||||
|  |  ,modeHelpSuffix = ["Defaults come from previous similar transactions; use query patterns to restrict these."] | ||||||
|  |  ,modeArgs = Just commandargsflag | ||||||
|  |  ,modeGroupFlags = Group { | ||||||
|  |      groupUnnamed = [ | ||||||
|  |       flagNone ["no-new-accounts"]  (\opts -> setboolopt "no-new-accounts" opts) "don't allow creating new accounts" | ||||||
|  |      ] | ||||||
|  |     ,groupHidden = [] | ||||||
|  |     ,groupNamed = [(generalflagstitle, generalflags2)] | ||||||
|  |     } | ||||||
|  |  } | ||||||
| 
 | 
 | ||||||
| -- | Convert any fuzzy dates within these option values to explicit ones, | convertmode = (commandmode ["convert"]) { | ||||||
| -- based on today's date. |   modeValue = [("command","convert")] | ||||||
| fixOptDates :: [Opt] -> IO [Opt] |  ,modeHelp = "show the specified CSV file as hledger journal entries" | ||||||
| fixOptDates opts = do |  ,modeArgs = Just $ flagArg (\s opts -> Right $ setopt "args" s opts) "CSVFILE" | ||||||
|  |  ,modeGroupFlags = Group { | ||||||
|  |      groupUnnamed = [ | ||||||
|  |       flagReq ["rules-file"]  (\s opts -> Right $ setopt "rules-file" s opts) "FILE" "rules file to use (default: CSVFILE.rules)" | ||||||
|  |      ] | ||||||
|  |     ,groupHidden = [] | ||||||
|  |     ,groupNamed = [(generalflagstitle, generalflags3)] | ||||||
|  |     } | ||||||
|  |  } | ||||||
|  | 
 | ||||||
|  | testmode = (commandmode ["test"]) { | ||||||
|  |   modeHelp = "run self-tests, or just the ones matching REGEXPS" | ||||||
|  |  ,modeArgs = Just $ flagArg (\s opts -> Right $ setopt "args" s opts) "[REGEXPS]" | ||||||
|  |  ,modeGroupFlags = Group { | ||||||
|  |      groupUnnamed = [] | ||||||
|  |     ,groupHidden = [] | ||||||
|  |     ,groupNamed = [(generalflagstitle, generalflags3)] | ||||||
|  |     } | ||||||
|  |  } | ||||||
|  | 
 | ||||||
|  | accountsmode = (commandmode ["accounts","balance"]) { | ||||||
|  |   modeHelp = "(or balance) show matched accounts and their balances" | ||||||
|  |  ,modeArgs = Just commandargsflag | ||||||
|  |  ,modeGroupFlags = Group { | ||||||
|  |      groupUnnamed = [ | ||||||
|  |       flagNone ["flat"] (\opts -> setboolopt "flat" opts) "show full account names, unindented" | ||||||
|  |      ,flagReq ["drop"] (\s opts -> Right $ setopt "drop" s opts) "N" "with --flat, omit this many leading account name components" | ||||||
|  |      ,flagReq  ["format"] (\s opts -> Right $ setopt "format" s opts) "FORMATSTR" "use this custom line format" | ||||||
|  |      ,flagNone ["no-elide"] (\opts -> setboolopt "no-elide" opts) "no eliding at all, stronger than --empty" | ||||||
|  |      ,flagNone ["no-total"] (\opts -> setboolopt "no-total" opts) "don't show the final total" | ||||||
|  |      ] | ||||||
|  |     ,groupHidden = [] | ||||||
|  |     ,groupNamed = [(generalflagstitle, generalflags1)] | ||||||
|  |     } | ||||||
|  |  } | ||||||
|  | 
 | ||||||
|  | entriesmode = (commandmode ["entries","print"]) { | ||||||
|  |   modeHelp = "(or print) show matched journal entries" | ||||||
|  |  ,modeArgs = Just commandargsflag | ||||||
|  |  ,modeGroupFlags = Group { | ||||||
|  |      groupUnnamed = [] | ||||||
|  |     ,groupHidden = [] | ||||||
|  |     ,groupNamed = [(generalflagstitle, generalflags1)] | ||||||
|  |     } | ||||||
|  |  } | ||||||
|  | 
 | ||||||
|  | postingsmode = (commandmode ["postings","register"]) { | ||||||
|  |   modeHelp = "(or register) show matched postings and running total" | ||||||
|  |  ,modeArgs = Just commandargsflag | ||||||
|  |  ,modeGroupFlags = Group { | ||||||
|  |      groupUnnamed = [] | ||||||
|  |     ,groupHidden = [] | ||||||
|  |     ,groupNamed = [(generalflagstitle, generalflags1)] | ||||||
|  |     } | ||||||
|  |  } | ||||||
|  | 
 | ||||||
|  | transactionsmode = (commandmode ["transactions"]) { | ||||||
|  |   modeHelp = "show matched transactions and balance in some account(s)" | ||||||
|  |  ,modeArgs = Just commandargsflag | ||||||
|  |  ,modeGroupFlags = Group { | ||||||
|  |      groupUnnamed = [] | ||||||
|  |     ,groupHidden = [] | ||||||
|  |     ,groupNamed = [(generalflagstitle, generalflags1)] | ||||||
|  |     } | ||||||
|  |  } | ||||||
|  | 
 | ||||||
|  | activitymode = (commandmode ["activity","histogram"]) { | ||||||
|  |   modeHelp = "show a barchart of transactions per interval" | ||||||
|  |  ,modeHelpSuffix = ["The default interval is daily."] | ||||||
|  |  ,modeArgs = Just commandargsflag | ||||||
|  |  ,modeGroupFlags = Group { | ||||||
|  |      groupUnnamed = [] | ||||||
|  |     ,groupHidden = [] | ||||||
|  |     ,groupNamed = [(generalflagstitle, generalflags1)] | ||||||
|  |     } | ||||||
|  |  } | ||||||
|  | 
 | ||||||
|  | statsmode = (commandmode ["stats"]) { | ||||||
|  |   modeHelp = "show quick statistics for a journal (or part of it)" | ||||||
|  |  ,modeArgs = Just commandargsflag | ||||||
|  |  ,modeGroupFlags = Group { | ||||||
|  |      groupUnnamed = [] | ||||||
|  |     ,groupHidden = [] | ||||||
|  |     ,groupNamed = [(generalflagstitle, generalflags1)] | ||||||
|  |     } | ||||||
|  |  } | ||||||
|  | 
 | ||||||
|  | binaryfilenamemode = (commandmode ["binaryfilename"]) { | ||||||
|  |   modeHelp = "show the download filename for this hledger build, and exit" | ||||||
|  |  ,modeArgs = Nothing | ||||||
|  |  ,modeGroupFlags = Group { | ||||||
|  |      groupUnnamed = [] | ||||||
|  |     ,groupHidden = [] | ||||||
|  |     ,groupNamed = [(generalflagstitle, generalflags3)] | ||||||
|  |     } | ||||||
|  |  } | ||||||
|  | 
 | ||||||
|  | -- 2. ADT holding options used in this package and above, parsed from RawOpts. | ||||||
|  | -- This represents the command-line options that were provided, with all | ||||||
|  | -- parsing completed, but before adding defaults or derived values (XXX add) | ||||||
|  | 
 | ||||||
|  | -- cli options, used in hledger and above | ||||||
|  | data CliOpts = CliOpts { | ||||||
|  |      rawopts_         :: RawOpts | ||||||
|  |     ,command_         :: String | ||||||
|  |     ,file_            :: Maybe FilePath | ||||||
|  |     ,alias_           :: [String] | ||||||
|  |     ,debug_           :: Bool | ||||||
|  |     ,no_new_accounts_ :: Bool           -- add | ||||||
|  |     ,rules_file_      :: Maybe FilePath -- convert | ||||||
|  |     ,reportopts_      :: ReportOpts | ||||||
|  |  } deriving (Show) | ||||||
|  | 
 | ||||||
|  | defcliopts = CliOpts | ||||||
|  |     def | ||||||
|  |     def | ||||||
|  |     def | ||||||
|  |     def | ||||||
|  |     def | ||||||
|  |     def | ||||||
|  |     def | ||||||
|  |     def | ||||||
|  | 
 | ||||||
|  | instance Default CliOpts where def = defcliopts | ||||||
|  | 
 | ||||||
|  | -- | Parse raw option string values to the desired final data types. | ||||||
|  | -- Any relative smart dates will be converted to fixed dates based on | ||||||
|  | -- today's date. Parsing failures will raise an error. | ||||||
|  | toCliOpts :: RawOpts -> IO CliOpts | ||||||
|  | toCliOpts rawopts = do | ||||||
|   d <- getCurrentDay |   d <- getCurrentDay | ||||||
|   return $ map (fixopt d) opts |   return defcliopts { | ||||||
|   where |               rawopts_         = rawopts | ||||||
|     fixopt d (Begin s)   = Begin $ fixSmartDateStr d s |              ,command_         = stringopt "command" rawopts | ||||||
|     fixopt d (End s)     = End $ fixSmartDateStr d s |              ,file_            = maybestringopt "file" rawopts | ||||||
|     fixopt d (Display s) = -- hacky |              ,alias_           = listofstringopt "alias" rawopts | ||||||
|         Display $ regexReplaceBy "\\[.+?\\]" fixbracketeddatestr s |              ,debug_           = boolopt "debug" rawopts | ||||||
|         where fixbracketeddatestr s = "[" ++ fixSmartDateStr d (init $ tail s) ++ "]" |              ,no_new_accounts_ = boolopt "no-new-accounts" rawopts -- add | ||||||
|     fixopt _ o            = o |              ,rules_file_      = maybestringopt "rules-file" rawopts -- convert | ||||||
|  |              ,reportopts_ = defreportopts { | ||||||
|  |                              begin_     = maybesmartdateopt d "begin" rawopts | ||||||
|  |                             ,end_       = maybesmartdateopt d "end" rawopts | ||||||
|  |                             ,period_    = maybeperiodopt d rawopts | ||||||
|  |                             ,cleared_   = boolopt "cleared" rawopts | ||||||
|  |                             ,uncleared_ = boolopt "uncleared" rawopts | ||||||
|  |                             ,cost_      = boolopt "cost" rawopts | ||||||
|  |                             ,depth_     = maybeintopt "depth" rawopts | ||||||
|  |                             ,display_   = maybedisplayopt d rawopts | ||||||
|  |                             ,effective_ = boolopt "effective" rawopts | ||||||
|  |                             ,empty_     = boolopt "empty" rawopts | ||||||
|  |                             ,no_elide_  = boolopt "no-elide" rawopts | ||||||
|  |                             ,real_      = boolopt "real" rawopts | ||||||
|  |                             ,flat_      = boolopt "flat" rawopts -- balance | ||||||
|  |                             ,drop_      = intopt "drop" rawopts -- balance | ||||||
|  |                             ,no_total_  = boolopt "no-total" rawopts -- balance | ||||||
|  |                             ,daily_     = boolopt "daily" rawopts | ||||||
|  |                             ,weekly_    = boolopt "weekly" rawopts | ||||||
|  |                             ,monthly_   = boolopt "monthly" rawopts | ||||||
|  |                             ,quarterly_ = boolopt "quarterly" rawopts | ||||||
|  |                             ,yearly_    = boolopt "yearly" rawopts | ||||||
|  |                             ,format_    = maybestringopt "format" rawopts | ||||||
|  |                             ,patterns_  = words'' prefixes $ singleQuoteIfNeeded $ stringopt "args" rawopts | ||||||
|  |                             } | ||||||
|  |              } | ||||||
| 
 | 
 | ||||||
| -- | Figure out the overall date span we should report on, based on any | -- workaround for http://code.google.com/p/ndmitchell/issues/detail?id=457 | ||||||
| -- begin/end/period options provided. If there is a period option, the | -- just handles commonest cases | ||||||
| -- others are ignored. | moveFlagsAfterCommand ("-f":f:cmd:rest) = cmd:"-f":f:rest | ||||||
| dateSpanFromOpts :: Day -> [Opt] -> DateSpan | moveFlagsAfterCommand (first:cmd:rest) | "-f" `isPrefixOf` first = cmd:first:rest | ||||||
| dateSpanFromOpts refdate opts | moveFlagsAfterCommand as = as | ||||||
|     | not (null popts) = case parsePeriodExpr refdate $ last popts of | 
 | ||||||
|                          Right (_, s) -> s | -- | Convert possibly encoded option values to regular unicode strings. | ||||||
|                          Left e       -> parseerror e | decodeRawOpts = map (\(name,val) -> (name, fromPlatformString val)) | ||||||
|     | otherwise = DateSpan lastb laste | 
 | ||||||
|  | -- | Get all command-line options, failing on any parse errors. | ||||||
|  | getHledgerOpts :: IO CliOpts | ||||||
|  | -- getHledgerOpts = processArgs mainmode >>= return . decodeRawOpts >>= toOpts >>= checkOpts | ||||||
|  | getHledgerOpts = do | ||||||
|  |   args <- getArgs | ||||||
|  |   toCliOpts (decodeRawOpts $ processValue mainmode $ moveFlagsAfterCommand args) >>= checkCliOpts | ||||||
|  | 
 | ||||||
|  | -- utils | ||||||
|  | 
 | ||||||
|  | optserror = error' . (++ " (run with --help for usage)") | ||||||
|  | 
 | ||||||
|  | setopt name val = (++ [(name,singleQuoteIfNeeded val)]) | ||||||
|  | 
 | ||||||
|  | setboolopt name = (++ [(name,"")]) | ||||||
|  | 
 | ||||||
|  | in_ :: String -> RawOpts -> Bool | ||||||
|  | in_ name = isJust . lookup name | ||||||
|  | 
 | ||||||
|  | boolopt = in_ | ||||||
|  | 
 | ||||||
|  | maybestringopt name = maybe Nothing (Just . stripquotes) . lookup name | ||||||
|  | 
 | ||||||
|  | stringopt name = fromMaybe "" . maybestringopt name | ||||||
|  | 
 | ||||||
|  | listofstringopt name rawopts = [stripquotes v | (n,v) <- rawopts, n==name] | ||||||
|  | 
 | ||||||
|  | maybeintopt :: String -> RawOpts -> Maybe Int | ||||||
|  | maybeintopt name rawopts = | ||||||
|  |     let ms = maybestringopt name rawopts in | ||||||
|  |     case ms of Nothing -> Nothing | ||||||
|  |                Just s -> Just $ readDef (optserror $ "could not parse "++name++" number: "++s) s | ||||||
|  | 
 | ||||||
|  | intopt name = fromMaybe 0 . maybeintopt name | ||||||
|  | 
 | ||||||
|  | maybesmartdateopt :: Day -> String -> RawOpts -> Maybe Day | ||||||
|  | maybesmartdateopt d name rawopts = | ||||||
|  |         case maybestringopt name rawopts of | ||||||
|  |           Nothing -> Nothing | ||||||
|  |           Just s -> either | ||||||
|  |                     (\e -> optserror $ "could not parse "++name++" date: "++show e) | ||||||
|  |                     Just | ||||||
|  |                     $ fixSmartDateStrEither' d s | ||||||
|  | 
 | ||||||
|  | maybedisplayopt :: Day -> RawOpts -> Maybe DisplayExpr | ||||||
|  | maybedisplayopt d rawopts = | ||||||
|  |     maybe Nothing (Just . regexReplaceBy "\\[.+?\\]" fixbracketeddatestr) $ maybestringopt "display" rawopts | ||||||
|     where |     where | ||||||
|       popts = optValuesForConstructor Period opts |       fixbracketeddatestr "" = "" | ||||||
|       bopts = optValuesForConstructor Begin opts |       fixbracketeddatestr s = "[" ++ fixSmartDateStr d (init $ tail s) ++ "]" | ||||||
|       eopts = optValuesForConstructor End opts |  | ||||||
|       lastb = listtomaybeday bopts |  | ||||||
|       laste = listtomaybeday eopts |  | ||||||
|       listtomaybeday vs = if null vs then Nothing else Just $ parse $ last vs |  | ||||||
|           where parse = parsedate . fixSmartDateStr refdate |  | ||||||
| 
 | 
 | ||||||
| -- | Figure out the reporting interval, if any, specified by the options. | maybeperiodopt :: Day -> RawOpts -> Maybe (Interval,DateSpan) | ||||||
| -- If there is a period option, the others are ignored. | maybeperiodopt d rawopts = | ||||||
| intervalFromOpts :: [Opt] -> Interval |     case maybestringopt "period" rawopts of | ||||||
| intervalFromOpts opts = |       Nothing -> Nothing | ||||||
|     case (periodopts, intervalopts) of |       Just s -> either | ||||||
|       ((p:_), _)            -> case parsePeriodExpr (parsedate "0001/01/01") p of |                 (\e -> optserror $ "could not parse period option: "++show e) | ||||||
|                                 Right (i, _) -> i |                 Just | ||||||
|                                 Left e       -> parseerror e |                 $ parsePeriodExpr d s | ||||||
|       (_, (DailyOpt:_))     -> Days 1 |  | ||||||
|       (_, (WeeklyOpt:_))    -> Weeks 1 |  | ||||||
|       (_, (MonthlyOpt:_))   -> Months 1 |  | ||||||
|       (_, (QuarterlyOpt:_)) -> Quarters 1 |  | ||||||
|       (_, (YearlyOpt:_))    -> Years 1 |  | ||||||
|       (_, _)                -> NoInterval |  | ||||||
|     where |  | ||||||
|       periodopts   = reverse $ optValuesForConstructor Period opts |  | ||||||
|       intervalopts = reverse $ filter (`elem` [DailyOpt,WeeklyOpt,MonthlyOpt,QuarterlyOpt,YearlyOpt]) opts |  | ||||||
| 
 | 
 | ||||||
| rulesFileFromOpts :: [Opt] -> Maybe FilePath | -- | Do final validation of processed opts, raising an error if there is trouble. | ||||||
| rulesFileFromOpts opts = listtomaybe $ optValuesForConstructor RulesFile opts | checkCliOpts :: CliOpts -> IO CliOpts -- or pure.. | ||||||
|     where | checkCliOpts opts@CliOpts{reportopts_=ropts} = do | ||||||
|       listtomaybe [] = Nothing |   case formatFromOpts ropts of | ||||||
|       listtomaybe vs = Just $ head vs |     Left err -> optserror $ "could not parse format option: "++err | ||||||
|  |     Right _ -> return () | ||||||
|  |   return opts | ||||||
| 
 | 
 | ||||||
| -- | Default balance format string: "%20(total)  %2(depth_spacer)%-(account)" | -- | Parse any format option provided, possibly raising an error, or get | ||||||
|  | -- the default value. | ||||||
|  | formatFromOpts :: ReportOpts -> Either String [FormatString] | ||||||
|  | formatFromOpts = maybe (Right defaultBalanceFormatString) parseFormatString . format_ | ||||||
|  | 
 | ||||||
|  | -- | Default line format for balance report: "%20(total)  %2(depth_spacer)%-(account)" | ||||||
| defaultBalanceFormatString :: [FormatString] | defaultBalanceFormatString :: [FormatString] | ||||||
| defaultBalanceFormatString = [ | defaultBalanceFormatString = [ | ||||||
|       FormatField False (Just 20) Nothing Total |       FormatField False (Just 20) Nothing Total | ||||||
| @ -237,81 +404,14 @@ defaultBalanceFormatString = [ | |||||||
|     , FormatField True Nothing Nothing Format.Account |     , FormatField True Nothing Nothing Format.Account | ||||||
|     ] |     ] | ||||||
| 
 | 
 | ||||||
| -- | Parses the --format string to either an error message or a format string. |  | ||||||
| parseFormatFromOpts :: [Opt] -> Either String [FormatString] |  | ||||||
| parseFormatFromOpts opts = listtomaybe $ optValuesForConstructor ReportFormat opts |  | ||||||
|     where |  | ||||||
|       listtomaybe :: [String] -> Either String [FormatString] |  | ||||||
|       listtomaybe [] = Right defaultBalanceFormatString |  | ||||||
|       listtomaybe vs = parseFormatString $ head vs |  | ||||||
| 
 |  | ||||||
| -- | Returns the format string. If the string can't be parsed it fails with error'. |  | ||||||
| formatFromOpts :: [Opt] -> [FormatString] |  | ||||||
| formatFromOpts opts = case parseFormatFromOpts opts of |  | ||||||
|     Left err -> error' err |  | ||||||
|     Right format -> format |  | ||||||
| 
 |  | ||||||
| -- | Get the value of the (last) depth option, if any. |  | ||||||
| depthFromOpts :: [Opt] -> Maybe Int |  | ||||||
| depthFromOpts opts = listtomaybeint $ optValuesForConstructor Depth opts |  | ||||||
|     where |  | ||||||
|       listtomaybeint [] = Nothing |  | ||||||
|       listtomaybeint vs = Just $ read $ last vs |  | ||||||
| 
 |  | ||||||
| -- | Get the value of the (last) drop option, if any, otherwise 0. |  | ||||||
| dropFromOpts :: [Opt] -> Int |  | ||||||
| dropFromOpts opts = fromMaybe 0 $ listtomaybeint $ optValuesForConstructor Drop opts |  | ||||||
|     where |  | ||||||
|       listtomaybeint [] = Nothing |  | ||||||
|       listtomaybeint vs = Just $ read $ last vs |  | ||||||
| 
 |  | ||||||
| -- | Get the value of the (last) display option, if any. |  | ||||||
| displayExprFromOpts :: [Opt] -> Maybe String |  | ||||||
| displayExprFromOpts opts = listtomaybe $ optValuesForConstructor Display opts |  | ||||||
|     where |  | ||||||
|       listtomaybe [] = Nothing |  | ||||||
|       listtomaybe vs = Just $ last vs |  | ||||||
| 
 |  | ||||||
| -- | Get the value of the (last) baseurl option, if any. |  | ||||||
| baseUrlFromOpts :: [Opt] -> Maybe String |  | ||||||
| baseUrlFromOpts opts = listtomaybe $ optValuesForConstructor BaseUrl opts |  | ||||||
|     where |  | ||||||
|       listtomaybe [] = Nothing |  | ||||||
|       listtomaybe vs = Just $ last vs |  | ||||||
| 
 |  | ||||||
| -- | Get the value of the (last) port option, if any. |  | ||||||
| portFromOpts :: [Opt] -> Maybe Int |  | ||||||
| portFromOpts opts = listtomaybeint $ optValuesForConstructor Port opts |  | ||||||
|     where |  | ||||||
|       listtomaybeint [] = Nothing |  | ||||||
|       listtomaybeint vs = Just $ read $ last vs |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| -- | Get a maybe boolean representing the last cleared/uncleared option if any. |  | ||||||
| clearedValueFromOpts opts | null os = Nothing |  | ||||||
|                           | last os == Cleared = Just True |  | ||||||
|                           | otherwise = Just False |  | ||||||
|     where os = optsWithConstructors [Cleared,UnCleared] opts |  | ||||||
| 
 |  | ||||||
| -- | Detect which date we will report on, based on --effective. |  | ||||||
| whichDateFromOpts :: [Opt] -> WhichDate |  | ||||||
| whichDateFromOpts opts = if Effective `elem` opts then EffectiveDate else ActualDate |  | ||||||
| 
 |  | ||||||
| -- | Were we invoked as \"hours\" ? |  | ||||||
| usingTimeProgramName :: IO Bool |  | ||||||
| usingTimeProgramName = do |  | ||||||
|   progname <- getProgName |  | ||||||
|   return $ map toLower progname == progname_cli_time |  | ||||||
| 
 |  | ||||||
| -- | Get the journal file path from options, an environment variable, or a default | -- | Get the journal file path from options, an environment variable, or a default | ||||||
| journalFilePathFromOpts :: [Opt] -> IO String | journalFilePathFromOpts :: CliOpts -> IO String | ||||||
| journalFilePathFromOpts opts = do | journalFilePathFromOpts opts = do | ||||||
|   istimequery <- usingTimeProgramName |   f <- myJournalPath | ||||||
|   f <- if istimequery then myTimelogPath else myJournalPath |   return $ fromMaybe f $ file_ opts | ||||||
|   return $ last $ f : optValuesForConstructor File opts |  | ||||||
| 
 | 
 | ||||||
| aliasesFromOpts :: [Opt] -> [(AccountName,AccountName)] | aliasesFromOpts :: CliOpts -> [(AccountName,AccountName)] | ||||||
| aliasesFromOpts opts = map parseAlias $ optValuesForConstructor Alias opts | aliasesFromOpts = map parseAlias . alias_ | ||||||
|     where |     where | ||||||
|       -- similar to ledgerAlias |       -- similar to ledgerAlias | ||||||
|       parseAlias :: String -> (AccountName,AccountName) |       parseAlias :: String -> (AccountName,AccountName) | ||||||
| @ -322,57 +422,11 @@ aliasesFromOpts opts = map parseAlias $ optValuesForConstructor Alias opts | |||||||
|             alias' = case alias of ('=':rest) -> rest |             alias' = case alias of ('=':rest) -> rest | ||||||
|                                    _ -> orig |                                    _ -> orig | ||||||
| 
 | 
 | ||||||
| -- | Gather filter pattern arguments into a list of account patterns and a | printModeHelpAndExit mode = putStrLn progversion >> putStr help >> exitSuccess | ||||||
| -- list of description patterns. We interpret pattern arguments as |     where help = showText defaultWrap $ helpText HelpFormatDefault mode | ||||||
| -- follows: those prefixed with "desc:" are description patterns, all |  | ||||||
| -- others are account patterns; also patterns prefixed with "not:" are |  | ||||||
| -- negated. not: should come after desc: if both are used. |  | ||||||
| parsePatternArgs :: [String] -> ([String],[String]) |  | ||||||
| parsePatternArgs args = (as, ds') |  | ||||||
|     where |  | ||||||
|       descprefix = "desc:" |  | ||||||
|       (ds, as) = partition (descprefix `isPrefixOf`) args |  | ||||||
|       ds' = map (drop (length descprefix)) ds |  | ||||||
| 
 | 
 | ||||||
| -- | Convert application options to the library's generic filter specification. | printVersionAndExit = putStrLn progversion >> exitSuccess | ||||||
| optsToFilterSpec :: [Opt] -> [String] -> Day -> FilterSpec |  | ||||||
| optsToFilterSpec opts args d = FilterSpec { |  | ||||||
|                                 datespan=dateSpanFromOpts d opts |  | ||||||
|                                ,cleared=clearedValueFromOpts opts |  | ||||||
|                                ,real=Real `elem` opts |  | ||||||
|                                ,empty=Empty `elem` opts |  | ||||||
|                                ,acctpats=apats |  | ||||||
|                                ,descpats=dpats |  | ||||||
|                                ,depth = depthFromOpts opts |  | ||||||
|                                } |  | ||||||
|     where (apats,dpats) = parsePatternArgs args |  | ||||||
| 
 |  | ||||||
| -- currentLocalTimeFromOpts opts = listtomaybe $ optValuesForConstructor CurrentLocalTime opts |  | ||||||
| --     where |  | ||||||
| --       listtomaybe [] = Nothing |  | ||||||
| --       listtomaybe vs = Just $ last vs |  | ||||||
| 
 | 
 | ||||||
| tests_Hledger_Cli_Options = TestList | tests_Hledger_Cli_Options = TestList | ||||||
|  [ |  [ | ||||||
|   "dateSpanFromOpts" ~: do |  | ||||||
|     let todaysdate = parsedate "2008/11/26" |  | ||||||
|     let gives = is . show . dateSpanFromOpts todaysdate |  | ||||||
|     [] `gives` "DateSpan Nothing Nothing" |  | ||||||
|     [Begin "2008", End "2009"] `gives` "DateSpan (Just 2008-01-01) (Just 2009-01-01)" |  | ||||||
|     [Period "in 2008"] `gives` "DateSpan (Just 2008-01-01) (Just 2009-01-01)" |  | ||||||
|     [Begin "2005", End "2007",Period "in 2008"] `gives` "DateSpan (Just 2008-01-01) (Just 2009-01-01)" |  | ||||||
| 
 |  | ||||||
|   ,"intervalFromOpts" ~: do |  | ||||||
|     let gives = is . intervalFromOpts |  | ||||||
|     [] `gives` NoInterval |  | ||||||
|     [DailyOpt] `gives` Days 1 |  | ||||||
|     [WeeklyOpt] `gives` Weeks 1 |  | ||||||
|     [MonthlyOpt] `gives` Months 1 |  | ||||||
|     [QuarterlyOpt] `gives` Quarters 1 |  | ||||||
|     [YearlyOpt] `gives` Years 1 |  | ||||||
|     [Period "weekly"] `gives` Weeks 1 |  | ||||||
|     [Period "monthly"] `gives` Months 1 |  | ||||||
|     [Period "quarterly"] `gives` Quarters 1 |  | ||||||
|     [WeeklyOpt, Period "yearly"] `gives` Years 1 |  | ||||||
| 
 |  | ||||||
|  ] |  ] | ||||||
|  | |||||||
| @ -18,15 +18,14 @@ import Hledger.Cli.Options | |||||||
| import Hledger.Cli.Reports | import Hledger.Cli.Reports | ||||||
| 
 | 
 | ||||||
| -- | Print journal transactions in standard format. | -- | Print journal transactions in standard format. | ||||||
| print' :: [Opt] -> [String] -> Journal -> IO () | print' :: CliOpts -> Journal -> IO () | ||||||
| print' opts args j = do | print' CliOpts{reportopts_=ropts} j = do | ||||||
|   d <- getCurrentDay |   d <- getCurrentDay | ||||||
|   putStr $ showTransactions opts (optsToFilterSpec opts args d) j |   putStr $ showTransactions ropts (optsToFilterSpec ropts d) j | ||||||
| 
 | 
 | ||||||
| showTransactions :: [Opt] -> FilterSpec -> Journal -> String | showTransactions :: ReportOpts -> FilterSpec -> Journal -> String | ||||||
| showTransactions opts fspec j = entriesReportAsText opts fspec $ entriesReport opts fspec j | showTransactions opts fspec j = entriesReportAsText opts fspec $ entriesReport opts fspec j | ||||||
| 
 | 
 | ||||||
| entriesReportAsText :: [Opt] -> FilterSpec -> EntriesReport -> String | entriesReportAsText :: ReportOpts -> FilterSpec -> EntriesReport -> String | ||||||
| entriesReportAsText opts _ items = concatMap (showTransactionForPrint effective) items | entriesReportAsText opts _ items = concatMap (showTransactionForPrint (effective_ opts)) items | ||||||
|     where effective = Effective `elem` opts |  | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -25,13 +25,13 @@ import Hledger.Cli.Reports | |||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| -- | Print a (posting) register report. | -- | Print a (posting) register report. | ||||||
| register :: [Opt] -> [String] -> Journal -> IO () | register :: CliOpts -> Journal -> IO () | ||||||
| register opts args j = do | register CliOpts{reportopts_=ropts} j = do | ||||||
|   d <- getCurrentDay |   d <- getCurrentDay | ||||||
|   putStr $ postingsReportAsText opts $ postingsReport opts (optsToFilterSpec opts args d) j |   putStr $ postingsReportAsText ropts $ postingsReport ropts (optsToFilterSpec ropts d) j | ||||||
| 
 | 
 | ||||||
| -- | Render a register report as plain text suitable for console output. | -- | Render a register report as plain text suitable for console output. | ||||||
| postingsReportAsText :: [Opt] -> PostingsReport -> String | postingsReportAsText :: ReportOpts -> PostingsReport -> String | ||||||
| postingsReportAsText opts = unlines . map (postingsReportItemAsText opts) . snd | postingsReportAsText opts = unlines . map (postingsReportItemAsText opts) . snd | ||||||
| 
 | 
 | ||||||
| -- | Render one register report line item as plain text. Eg: | -- | Render one register report line item as plain text. Eg: | ||||||
| @ -41,7 +41,7 @@ postingsReportAsText opts = unlines . map (postingsReportItemAsText opts) . snd | |||||||
| -- ^ displayed for first postings^ | -- ^ displayed for first postings^ | ||||||
| --   only, otherwise blank | --   only, otherwise blank | ||||||
| -- @ | -- @ | ||||||
| postingsReportItemAsText :: [Opt] -> PostingsReportItem -> String | postingsReportItemAsText :: ReportOpts -> PostingsReportItem -> String | ||||||
| postingsReportItemAsText _ (dd, p, b) = concatTopPadded [datedesc, pstr, " ", bal] | postingsReportItemAsText _ (dd, p, b) = concatTopPadded [datedesc, pstr, " ", bal] | ||||||
|     where |     where | ||||||
|       datedesc = case dd of Nothing -> replicate datedescwidth ' ' |       datedesc = case dd of Nothing -> replicate datedescwidth ' ' | ||||||
| @ -57,7 +57,7 @@ postingsReportItemAsText _ (dd, p, b) = concatTopPadded [datedesc, pstr, " ", ba | |||||||
|       bal = padleft 12 (showMixedAmountOrZeroWithoutPrice b) |       bal = padleft 12 (showMixedAmountOrZeroWithoutPrice b) | ||||||
| 
 | 
 | ||||||
| -- XXX | -- XXX | ||||||
| showPostingWithBalanceForVty showtxninfo p b = postingsReportItemAsText [] $ mkpostingsReportItem showtxninfo p b | showPostingWithBalanceForVty showtxninfo p b = postingsReportItemAsText defreportopts $ mkpostingsReportItem showtxninfo p b | ||||||
| 
 | 
 | ||||||
| tests_Hledger_Cli_Register :: Test | tests_Hledger_Cli_Register :: Test | ||||||
| tests_Hledger_Cli_Register = TestList | tests_Hledger_Cli_Register = TestList | ||||||
|  | |||||||
| @ -1,3 +1,4 @@ | |||||||
|  | {-# LANGUAGE RecordWildCards #-} | ||||||
| {-| | {-| | ||||||
| 
 | 
 | ||||||
| Generate several common kinds of report from a journal, as \"*Report\" - | Generate several common kinds of report from a journal, as \"*Report\" - | ||||||
| @ -9,6 +10,17 @@ on the command-line options, should move to hledger-lib later. | |||||||
| -} | -} | ||||||
| 
 | 
 | ||||||
| module Hledger.Cli.Reports ( | module Hledger.Cli.Reports ( | ||||||
|  |   ReportOpts(..), | ||||||
|  |   DisplayExpr, | ||||||
|  |   FormatStr, | ||||||
|  |   defreportopts, | ||||||
|  |   dateSpanFromOpts, | ||||||
|  |   intervalFromOpts, | ||||||
|  |   clearedValueFromOpts, | ||||||
|  |   whichDateFromOpts, | ||||||
|  |   journalSelectingDateFromOpts, | ||||||
|  |   journalSelectingAmountFromOpts, | ||||||
|  |   optsToFilterSpec, | ||||||
|   -- * Entries report |   -- * Entries report | ||||||
|   EntriesReport, |   EntriesReport, | ||||||
|   EntriesReportItem, |   EntriesReportItem, | ||||||
| @ -42,14 +54,138 @@ import Data.Ord | |||||||
| import Data.Time.Calendar | import Data.Time.Calendar | ||||||
| import Data.Tree | import Data.Tree | ||||||
| import Safe (headMay, lastMay) | import Safe (headMay, lastMay) | ||||||
|  | import System.Console.CmdArgs  -- for defaults support | ||||||
| import Test.HUnit | import Test.HUnit | ||||||
| import Text.ParserCombinators.Parsec | import Text.ParserCombinators.Parsec | ||||||
| import Text.Printf | import Text.Printf | ||||||
| 
 | 
 | ||||||
| import Hledger.Data | import Hledger.Data | ||||||
| import Hledger.Utils | import Hledger.Utils | ||||||
| import Hledger.Cli.Options | -- import Hledger.Cli.Utils | ||||||
| import Hledger.Cli.Utils | 
 | ||||||
|  | -- report options, used in hledger-lib and above | ||||||
|  | data ReportOpts = ReportOpts { | ||||||
|  |      begin_          :: Maybe Day | ||||||
|  |     ,end_            :: Maybe Day | ||||||
|  |     ,period_         :: Maybe (Interval,DateSpan) | ||||||
|  |     ,cleared_        :: Bool | ||||||
|  |     ,uncleared_      :: Bool | ||||||
|  |     ,cost_           :: Bool | ||||||
|  |     ,depth_          :: Maybe Int | ||||||
|  |     ,display_        :: Maybe DisplayExpr | ||||||
|  |     ,effective_      :: Bool | ||||||
|  |     ,empty_          :: Bool | ||||||
|  |     ,no_elide_       :: Bool | ||||||
|  |     ,real_           :: Bool | ||||||
|  |     ,flat_           :: Bool -- balance | ||||||
|  |     ,drop_           :: Int  -- balance | ||||||
|  |     ,no_total_       :: Bool -- balance | ||||||
|  |     ,daily_          :: Bool | ||||||
|  |     ,weekly_         :: Bool | ||||||
|  |     ,monthly_        :: Bool | ||||||
|  |     ,quarterly_      :: Bool | ||||||
|  |     ,yearly_         :: Bool | ||||||
|  |     ,format_         :: Maybe FormatStr | ||||||
|  |     ,patterns_       :: [String] | ||||||
|  |  } deriving (Show) | ||||||
|  | 
 | ||||||
|  | type DisplayExpr = String | ||||||
|  | type FormatStr = String | ||||||
|  | 
 | ||||||
|  | defreportopts = ReportOpts | ||||||
|  |     def | ||||||
|  |     def | ||||||
|  |     def | ||||||
|  |     def | ||||||
|  |     def | ||||||
|  |     def | ||||||
|  |     def | ||||||
|  |     def | ||||||
|  |     def | ||||||
|  |     def | ||||||
|  |     def | ||||||
|  |     def | ||||||
|  |     def | ||||||
|  |     def | ||||||
|  |     def | ||||||
|  |     def | ||||||
|  |     def | ||||||
|  |     def | ||||||
|  |     def | ||||||
|  |     def | ||||||
|  |     def | ||||||
|  |     def | ||||||
|  | 
 | ||||||
|  | instance Default ReportOpts where def = defreportopts | ||||||
|  | 
 | ||||||
|  | -- | Figure out the date span we should report on, based on any | ||||||
|  | -- begin/end/period options provided. A period option will cause begin and | ||||||
|  | -- end options to be ignored. | ||||||
|  | dateSpanFromOpts :: Day -> ReportOpts -> DateSpan | ||||||
|  | dateSpanFromOpts _ ReportOpts{..} = | ||||||
|  |     case period_ of Just (_,span) -> span | ||||||
|  |                     Nothing -> DateSpan begin_ end_ | ||||||
|  | 
 | ||||||
|  | -- | Figure out the reporting interval, if any, specified by the options. | ||||||
|  | -- --period overrides --daily overrides --weekly overrides --monthly etc. | ||||||
|  | intervalFromOpts :: ReportOpts -> Interval | ||||||
|  | intervalFromOpts ReportOpts{..} = | ||||||
|  |     case period_ of | ||||||
|  |       Just (interval,_) -> interval | ||||||
|  |       Nothing -> i | ||||||
|  |           where i | daily_ = Days 1 | ||||||
|  |                   | weekly_ = Weeks 1 | ||||||
|  |                   | monthly_ = Months 1 | ||||||
|  |                   | quarterly_ = Quarters 1 | ||||||
|  |                   | yearly_ = Years 1 | ||||||
|  |                   | otherwise =  NoInterval | ||||||
|  | 
 | ||||||
|  | -- | Get a maybe boolean representing the last cleared/uncleared option if any. | ||||||
|  | clearedValueFromOpts :: ReportOpts -> Maybe Bool | ||||||
|  | clearedValueFromOpts ReportOpts{..} | cleared_   = Just True | ||||||
|  |                                     | uncleared_ = Just False | ||||||
|  |                                     | otherwise  = Nothing | ||||||
|  | 
 | ||||||
|  | -- | Detect which date we will report on, based on --effective. | ||||||
|  | whichDateFromOpts :: ReportOpts -> WhichDate | ||||||
|  | whichDateFromOpts ReportOpts{..} = if effective_ then EffectiveDate else ActualDate | ||||||
|  | 
 | ||||||
|  | -- | Convert this journal's transactions' primary date to either the | ||||||
|  | -- actual or effective date, as per options. | ||||||
|  | journalSelectingDateFromOpts :: ReportOpts -> Journal -> Journal | ||||||
|  | journalSelectingDateFromOpts opts = journalSelectingDate (whichDateFromOpts opts) | ||||||
|  | 
 | ||||||
|  | -- | Convert this journal's postings' amounts to the cost basis amounts if | ||||||
|  | -- specified by options. | ||||||
|  | journalSelectingAmountFromOpts :: ReportOpts -> Journal -> Journal | ||||||
|  | journalSelectingAmountFromOpts opts | ||||||
|  |     | cost_ opts = journalConvertAmountsToCost | ||||||
|  |     | otherwise = id | ||||||
|  | 
 | ||||||
|  | -- | Convert application options to the library's generic filter specification. | ||||||
|  | optsToFilterSpec :: ReportOpts -> Day -> FilterSpec | ||||||
|  | optsToFilterSpec opts@ReportOpts{..} d = FilterSpec { | ||||||
|  |                                 datespan=dateSpanFromOpts d opts | ||||||
|  |                                ,cleared= clearedValueFromOpts opts | ||||||
|  |                                ,real=real_ | ||||||
|  |                                ,empty=empty_ | ||||||
|  |                                ,acctpats=apats | ||||||
|  |                                ,descpats=dpats | ||||||
|  |                                ,depth = depth_ | ||||||
|  |                                } | ||||||
|  |     where (apats,dpats) = parsePatternArgs patterns_ | ||||||
|  | 
 | ||||||
|  | -- | Gather filter pattern arguments into a list of account patterns and a | ||||||
|  | -- list of description patterns. We interpret pattern arguments as | ||||||
|  | -- follows: those prefixed with "desc:" are description patterns, all | ||||||
|  | -- others are account patterns; also patterns prefixed with "not:" are | ||||||
|  | -- negated. not: should come after desc: if both are used. | ||||||
|  | parsePatternArgs :: [String] -> ([String],[String]) | ||||||
|  | parsePatternArgs args = (as, ds') | ||||||
|  |     where | ||||||
|  |       descprefix = "desc:" | ||||||
|  |       (ds, as) = partition (descprefix `isPrefixOf`) args | ||||||
|  |       ds' = map (drop (length descprefix)) ds | ||||||
| 
 | 
 | ||||||
| ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | ||||||
| 
 | 
 | ||||||
| @ -60,7 +196,7 @@ type EntriesReport = [EntriesReportItem] | |||||||
| type EntriesReportItem = Transaction | type EntriesReportItem = Transaction | ||||||
| 
 | 
 | ||||||
| -- | Select transactions for an entries report. | -- | Select transactions for an entries report. | ||||||
| entriesReport :: [Opt] -> FilterSpec -> Journal -> EntriesReport | entriesReport :: ReportOpts -> FilterSpec -> Journal -> EntriesReport | ||||||
| entriesReport opts fspec j = sortBy (comparing tdate) $ jtxns $ filterJournalTransactions fspec j' | entriesReport opts fspec j = sortBy (comparing tdate) $ jtxns $ filterJournalTransactions fspec j' | ||||||
|     where |     where | ||||||
|       j' = journalSelectingDateFromOpts opts $ journalSelectingAmountFromOpts opts j |       j' = journalSelectingDateFromOpts opts $ journalSelectingAmountFromOpts opts j | ||||||
| @ -79,12 +215,12 @@ type PostingsReportItem = (Maybe (Day, String) -- transaction date and descripti | |||||||
| 
 | 
 | ||||||
| -- | Select postings from the journal and add running balance and other | -- | Select postings from the journal and add running balance and other | ||||||
| -- information to make a postings report. Used by eg hledger's register command. | -- information to make a postings report. Used by eg hledger's register command. | ||||||
| postingsReport :: [Opt] -> FilterSpec -> Journal -> PostingsReport | postingsReport :: ReportOpts -> FilterSpec -> Journal -> PostingsReport | ||||||
| postingsReport opts fspec j = (totallabel, postingsReportItems ps nullposting startbal (+)) | postingsReport opts fspec j = (totallabel, postingsReportItems ps nullposting startbal (+)) | ||||||
|     where |     where | ||||||
|       ps | interval == NoInterval = displayableps |       ps | interval == NoInterval = displayableps | ||||||
|          | otherwise              = summarisePostingsByInterval interval depth empty filterspan displayableps |          | otherwise              = summarisePostingsByInterval interval depth empty filterspan displayableps | ||||||
|       (precedingps, displayableps, _) = postingsMatchingDisplayExpr (displayExprFromOpts opts) |       (precedingps, displayableps, _) = postingsMatchingDisplayExpr (display_ opts) | ||||||
|                                         $ depthClipPostings depth |                                         $ depthClipPostings depth | ||||||
|                                         $ journalPostings |                                         $ journalPostings | ||||||
|                                         $ filterJournalPostings fspec{depth=Nothing} |                                         $ filterJournalPostings fspec{depth=Nothing} | ||||||
| @ -93,7 +229,7 @@ postingsReport opts fspec j = (totallabel, postingsReportItems ps nullposting st | |||||||
|                                         j |                                         j | ||||||
|       startbal = sumPostings precedingps |       startbal = sumPostings precedingps | ||||||
|       filterspan = datespan fspec |       filterspan = datespan fspec | ||||||
|       (interval, depth, empty) = (intervalFromOpts opts, depthFromOpts opts, Empty `elem` opts) |       (interval, depth, empty) = (intervalFromOpts opts, depth_ opts, empty_ opts) | ||||||
| 
 | 
 | ||||||
| totallabel = "Total" | totallabel = "Total" | ||||||
| balancelabel = "Balance" | balancelabel = "Balance" | ||||||
| @ -238,7 +374,7 @@ triBalance (_,_,_,_,_,Mixed a) = case a of [] -> "0" | |||||||
| -- "postingsReport" except it uses matchers and transaction-based report | -- "postingsReport" except it uses matchers and transaction-based report | ||||||
| -- items and the items are most recent first. Used by eg hledger-web's | -- items and the items are most recent first. Used by eg hledger-web's | ||||||
| -- journal view. | -- journal view. | ||||||
| journalTransactionsReport :: [Opt] -> Journal -> Matcher -> TransactionsReport | journalTransactionsReport :: ReportOpts -> Journal -> Matcher -> TransactionsReport | ||||||
| journalTransactionsReport _ Journal{jtxns=ts} m = (totallabel, items) | journalTransactionsReport _ Journal{jtxns=ts} m = (totallabel, items) | ||||||
|    where |    where | ||||||
|      ts' = sortBy (comparing tdate) $ filter (not . null . tpostings) $ map (filterTransactionPostings m) ts |      ts' = sortBy (comparing tdate) $ filter (not . null . tpostings) $ map (filterTransactionPostings m) ts | ||||||
| @ -261,16 +397,16 @@ journalTransactionsReport _ Journal{jtxns=ts} m = (totallabel, items) | |||||||
| -- Currently, reporting intervals are not supported, and report items are | -- Currently, reporting intervals are not supported, and report items are | ||||||
| -- most recent first. Used by eg hledger-web's account register view. | -- most recent first. Used by eg hledger-web's account register view. | ||||||
| -- | -- | ||||||
| accountTransactionsReport :: [Opt] -> Journal -> Matcher -> Matcher -> TransactionsReport | accountTransactionsReport :: ReportOpts -> Journal -> Matcher -> Matcher -> TransactionsReport | ||||||
| accountTransactionsReport opts j m thisacctmatcher = (label, items) | accountTransactionsReport opts j m thisacctmatcher = (label, items) | ||||||
|  where |  where | ||||||
|      -- transactions affecting this account, in date order |      -- transactions affecting this account, in date order | ||||||
|      ts = sortBy (comparing tdate) $ filter (matchesTransaction thisacctmatcher) $ jtxns j |      ts = sortBy (comparing tdate) $ filter (matchesTransaction thisacctmatcher) $ jtxns j | ||||||
|      -- starting balance: if we are filtering by a start date and nothing else, |      -- starting balance: if we are filtering by a start date and nothing else, | ||||||
|      -- the sum of postings to this account before that date; otherwise zero. |      -- the sum of postings to this account before that date; otherwise zero. | ||||||
|      (startbal,label) | matcherIsNull m                    = (nullmixedamt,        balancelabel) |      (startbal,label) | matcherIsNull m                           = (nullmixedamt,        balancelabel) | ||||||
|                       | matcherIsStartDateOnly effective m = (sumPostings priorps, balancelabel) |                       | matcherIsStartDateOnly (effective_ opts) m = (sumPostings priorps, balancelabel) | ||||||
|                       | otherwise                          = (nullmixedamt,        totallabel) |                       | otherwise                                 = (nullmixedamt,        totallabel) | ||||||
|                       where |                       where | ||||||
|                         priorps = -- ltrace "priorps" $ |                         priorps = -- ltrace "priorps" $ | ||||||
|                                   filter (matchesPosting |                                   filter (matchesPosting | ||||||
| @ -278,8 +414,7 @@ accountTransactionsReport opts j m thisacctmatcher = (label, items) | |||||||
|                                            MatchAnd [thisacctmatcher, tostartdatematcher])) |                                            MatchAnd [thisacctmatcher, tostartdatematcher])) | ||||||
|                                          $ transactionsPostings ts |                                          $ transactionsPostings ts | ||||||
|                         tostartdatematcher = MatchDate True (DateSpan Nothing startdate) |                         tostartdatematcher = MatchDate True (DateSpan Nothing startdate) | ||||||
|                         startdate = matcherStartDate effective m |                         startdate = matcherStartDate (effective_ opts) m | ||||||
|                         effective = Effective `elem` opts |  | ||||||
|      items = reverse $ accountTransactionsReportItems m (Just thisacctmatcher) startbal negate ts |      items = reverse $ accountTransactionsReportItems m (Just thisacctmatcher) startbal negate ts | ||||||
| 
 | 
 | ||||||
| -- | Generate transactions report items from a list of transactions, | -- | Generate transactions report items from a list of transactions, | ||||||
| @ -344,25 +479,25 @@ type AccountsReportItem = (AccountName  -- full account name | |||||||
| -- | Select accounts, and get their balances at the end of the selected | -- | Select accounts, and get their balances at the end of the selected | ||||||
| -- period, and misc. display information, for an accounts report. Used by | -- period, and misc. display information, for an accounts report. Used by | ||||||
| -- eg hledger's balance command. | -- eg hledger's balance command. | ||||||
| accountsReport :: [Opt] -> FilterSpec -> Journal -> AccountsReport | accountsReport :: ReportOpts -> FilterSpec -> Journal -> AccountsReport | ||||||
| accountsReport opts filterspec j = accountsReport' opts j (journalToLedger filterspec) | accountsReport opts filterspec j = accountsReport' opts j (journalToLedger filterspec) | ||||||
| 
 | 
 | ||||||
| -- | Select accounts, and get their balances at the end of the selected | -- | Select accounts, and get their balances at the end of the selected | ||||||
| -- period, and misc. display information, for an accounts report. Like | -- period, and misc. display information, for an accounts report. Like | ||||||
| -- "accountsReport" but uses the new matchers. Used by eg hledger-web's | -- "accountsReport" but uses the new matchers. Used by eg hledger-web's | ||||||
| -- accounts sidebar. | -- accounts sidebar. | ||||||
| accountsReport2 :: [Opt] -> Matcher -> Journal -> AccountsReport | accountsReport2 :: ReportOpts -> Matcher -> Journal -> AccountsReport | ||||||
| accountsReport2 opts matcher j = accountsReport' opts j (journalToLedger2 matcher) | accountsReport2 opts matcher j = accountsReport' opts j (journalToLedger2 matcher) | ||||||
| 
 | 
 | ||||||
| -- Accounts report helper. | -- Accounts report helper. | ||||||
| accountsReport' :: [Opt] -> Journal -> (Journal -> Ledger) -> AccountsReport | accountsReport' :: ReportOpts -> Journal -> (Journal -> Ledger) -> AccountsReport | ||||||
| accountsReport' opts j jtol = (items, total) | accountsReport' opts j jtol = (items, total) | ||||||
|     where |     where | ||||||
|       items = map mkitem interestingaccts |       items = map mkitem interestingaccts | ||||||
|       interestingaccts | NoElide `elem` opts = acctnames |       interestingaccts | no_elide_ opts = acctnames | ||||||
|                        | otherwise = filter (isInteresting opts l) acctnames |                        | otherwise = filter (isInteresting opts l) acctnames | ||||||
|       acctnames = sort $ tail $ flatten $ treemap aname accttree |       acctnames = sort $ tail $ flatten $ treemap aname accttree | ||||||
|       accttree = ledgerAccountTree (fromMaybe 99999 $ depthFromOpts opts) l |       accttree = ledgerAccountTree (fromMaybe 99999 $ depth_ opts) l | ||||||
|       total = sum $ map abalance $ ledgerTopAccounts l |       total = sum $ map abalance $ ledgerTopAccounts l | ||||||
|       l =  jtol $ journalSelectingDateFromOpts opts $ journalSelectingAmountFromOpts opts j |       l =  jtol $ journalSelectingDateFromOpts opts $ journalSelectingAmountFromOpts opts j | ||||||
| 
 | 
 | ||||||
| @ -370,14 +505,14 @@ accountsReport' opts j jtol = (items, total) | |||||||
|       mkitem :: AccountName -> AccountsReportItem |       mkitem :: AccountName -> AccountsReportItem | ||||||
|       mkitem a = (a, adisplay, indent, abal) |       mkitem a = (a, adisplay, indent, abal) | ||||||
|           where |           where | ||||||
|             adisplay | Flat `elem` opts = a |             adisplay | flat_ opts = a | ||||||
|                      | otherwise = accountNameFromComponents $ reverse (map accountLeafName ps) ++ [accountLeafName a] |                      | otherwise = accountNameFromComponents $ reverse (map accountLeafName ps) ++ [accountLeafName a] | ||||||
|                 where ps = takeWhile boring parents where boring = not . (`elem` interestingparents) |                 where ps = takeWhile boring parents where boring = not . (`elem` interestingparents) | ||||||
|             indent | Flat `elem` opts = 0 |             indent | flat_ opts = 0 | ||||||
|                    | otherwise = length interestingparents |                    | otherwise = length interestingparents | ||||||
|             interestingparents = filter (`elem` interestingaccts) parents |             interestingparents = filter (`elem` interestingaccts) parents | ||||||
|             parents = parentAccountNames a |             parents = parentAccountNames a | ||||||
|             abal | Flat `elem` opts = exclusiveBalance acct |             abal | flat_ opts = exclusiveBalance acct | ||||||
|                  | otherwise = abalance acct |                  | otherwise = abalance acct | ||||||
|                  where acct = ledgerAccount l a |                  where acct = ledgerAccount l a | ||||||
| 
 | 
 | ||||||
| @ -386,24 +521,24 @@ exclusiveBalance = sumPostings . apostings | |||||||
| 
 | 
 | ||||||
| -- | Is the named account considered interesting for this ledger's accounts report, | -- | Is the named account considered interesting for this ledger's accounts report, | ||||||
| -- following the eliding style of ledger's balance command ? | -- following the eliding style of ledger's balance command ? | ||||||
| isInteresting :: [Opt] -> Ledger -> AccountName -> Bool | isInteresting :: ReportOpts -> Ledger -> AccountName -> Bool | ||||||
| isInteresting opts l a | Flat `elem` opts = isInterestingFlat opts l a | isInteresting opts l a | flat_ opts = isInterestingFlat opts l a | ||||||
|                        | otherwise = isInterestingIndented opts l a |                        | otherwise = isInterestingIndented opts l a | ||||||
| 
 | 
 | ||||||
| isInterestingFlat :: [Opt] -> Ledger -> AccountName -> Bool | isInterestingFlat :: ReportOpts -> Ledger -> AccountName -> Bool | ||||||
| isInterestingFlat opts l a = notempty || emptyflag | isInterestingFlat opts l a = notempty || emptyflag | ||||||
|     where |     where | ||||||
|       acct = ledgerAccount l a |       acct = ledgerAccount l a | ||||||
|       notempty = not $ isZeroMixedAmount $ exclusiveBalance acct |       notempty = not $ isZeroMixedAmount $ exclusiveBalance acct | ||||||
|       emptyflag = Empty `elem` opts |       emptyflag = empty_ opts | ||||||
| 
 | 
 | ||||||
| isInterestingIndented :: [Opt] -> Ledger -> AccountName -> Bool | isInterestingIndented :: ReportOpts -> Ledger -> AccountName -> Bool | ||||||
| isInterestingIndented opts l a | isInterestingIndented opts l a | ||||||
|     | numinterestingsubs==1 && not atmaxdepth = notlikesub |     | numinterestingsubs==1 && not atmaxdepth = notlikesub | ||||||
|     | otherwise = notzero || emptyflag |     | otherwise = notzero || emptyflag | ||||||
|     where |     where | ||||||
|       atmaxdepth = isJust d && Just (accountNameLevel a) == d where d = depthFromOpts opts |       atmaxdepth = isJust d && Just (accountNameLevel a) == d where d = depth_ opts | ||||||
|       emptyflag = Empty `elem` opts |       emptyflag = empty_ opts | ||||||
|       acct = ledgerAccount l a |       acct = ledgerAccount l a | ||||||
|       notzero = not $ isZeroMixedAmount inclbalance where inclbalance = abalance acct |       notzero = not $ isZeroMixedAmount inclbalance where inclbalance = abalance acct | ||||||
|       notlikesub = not $ isZeroMixedAmount exclbalance where exclbalance = sumPostings $ apostings acct |       notlikesub = not $ isZeroMixedAmount exclbalance where exclbalance = sumPostings $ apostings acct | ||||||
|  | |||||||
| @ -15,6 +15,7 @@ import Text.Printf | |||||||
| import qualified Data.Map as Map | import qualified Data.Map as Map | ||||||
| 
 | 
 | ||||||
| import Hledger.Cli.Options | import Hledger.Cli.Options | ||||||
|  | import Hledger.Cli.Reports | ||||||
| import Hledger.Data | import Hledger.Data | ||||||
| import Prelude hiding (putStr) | import Prelude hiding (putStr) | ||||||
| import Hledger.Utils.UTF8 (putStr) | import Hledger.Utils.UTF8 (putStr) | ||||||
| @ -22,19 +23,19 @@ import Hledger.Utils.UTF8 (putStr) | |||||||
| 
 | 
 | ||||||
| -- like Register.summarisePostings | -- like Register.summarisePostings | ||||||
| -- | Print various statistics for the journal. | -- | Print various statistics for the journal. | ||||||
| stats :: [Opt] -> [String] -> Journal -> IO () | stats :: CliOpts -> Journal -> IO () | ||||||
| stats opts args j = do | stats CliOpts{reportopts_=reportopts_} j = do | ||||||
|   d <- getCurrentDay |   d <- getCurrentDay | ||||||
|   let filterspec = optsToFilterSpec opts args d |   let filterspec = optsToFilterSpec reportopts_ d | ||||||
|       l = journalToLedger filterspec j |       l = journalToLedger filterspec j | ||||||
|       reportspan = (ledgerDateSpan l) `orDatesFrom` (datespan filterspec) |       reportspan = (ledgerDateSpan l) `orDatesFrom` (datespan filterspec) | ||||||
|       intervalspans = splitSpan (intervalFromOpts opts) reportspan |       intervalspans = splitSpan (intervalFromOpts reportopts_) reportspan | ||||||
|       showstats = showLedgerStats opts args l d |       showstats = showLedgerStats l d | ||||||
|       s = intercalate "\n" $ map showstats intervalspans |       s = intercalate "\n" $ map showstats intervalspans | ||||||
|   putStr s |   putStr s | ||||||
| 
 | 
 | ||||||
| showLedgerStats :: [Opt] -> [String] -> Ledger -> Day -> DateSpan -> String | showLedgerStats :: Ledger -> Day -> DateSpan -> String | ||||||
| showLedgerStats _ _ l today span = | showLedgerStats l today span = | ||||||
|     unlines (map (uncurry (printf fmt)) stats) |     unlines (map (uncurry (printf fmt)) stats) | ||||||
|     where |     where | ||||||
|       fmt = "%-" ++ show w1 ++ "s: %-" ++ show w2 ++ "s" |       fmt = "%-" ++ show w1 ++ "s: %-" ++ show w2 ++ "s" | ||||||
|  | |||||||
| @ -38,22 +38,22 @@ import Hledger.Utils | |||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| -- | Run unit tests and exit with success or failure. | -- | Run unit tests and exit with success or failure. | ||||||
| runtests :: [Opt] -> [String] -> IO () | runtests :: CliOpts -> IO () | ||||||
| runtests opts args = do | runtests opts = do | ||||||
|   (hunitcounts,_) <- runtests' opts args |   (hunitcounts,_) <- runtests' opts | ||||||
|   if errors hunitcounts > 0 || (failures hunitcounts > 0) |   if errors hunitcounts > 0 || (failures hunitcounts > 0) | ||||||
|    then exitFailure |    then exitFailure | ||||||
|    else exitWith ExitSuccess |    else exitWith ExitSuccess | ||||||
| 
 | 
 | ||||||
| -- | Run unit tests and exit on failure. | -- | Run unit tests and exit on failure. | ||||||
| runTestsOrExit :: [Opt] -> [String] -> IO () | runTestsOrExit :: CliOpts -> IO () | ||||||
| runTestsOrExit opts args = do | runTestsOrExit opts = do | ||||||
|   (hunitcounts,_) <- runtests' opts args |   (hunitcounts,_) <- runtests' opts | ||||||
|   when (errors hunitcounts > 0 || (failures hunitcounts > 0)) $ exitFailure |   when (errors hunitcounts > 0 || (failures hunitcounts > 0)) $ exitFailure | ||||||
| 
 | 
 | ||||||
| runtests' :: Num b => t -> [String] -> IO (Counts, b) | runtests' :: Num b => CliOpts -> IO (Counts, b) | ||||||
| runtests' _ args = liftM (flip (,) 0) $ runTestTT ts | runtests' opts = liftM (flip (,) 0) $ runTestTT ts | ||||||
|     where |     where | ||||||
|       ts = TestList $ filter matchname $ tflatten tests_Hledger_Cli  -- show flat test names |       ts = TestList $ filter matchname $ tflatten tests_Hledger_Cli  -- show flat test names | ||||||
|       -- ts = tfilter matchname $ TestList tests -- show hierarchical test names |       -- ts = tfilter matchname $ TestList tests -- show hierarchical test names | ||||||
|       matchname = matchpats args . tname |       matchname = matchpats (patterns_ $ reportopts_ opts) . tname | ||||||
|  | |||||||
| @ -10,8 +10,6 @@ module Hledger.Cli.Utils | |||||||
|     ( |     ( | ||||||
|      withJournalDo, |      withJournalDo, | ||||||
|      readJournal', |      readJournal', | ||||||
|      journalSelectingDateFromOpts, |  | ||||||
|      journalSelectingAmountFromOpts, |  | ||||||
|      journalReload, |      journalReload, | ||||||
|      journalReloadIfChanged, |      journalReloadIfChanged, | ||||||
|      journalFileIsNewer, |      journalFileIsNewer, | ||||||
| @ -25,10 +23,10 @@ module Hledger.Cli.Utils | |||||||
|     ) |     ) | ||||||
| where | where | ||||||
| import Control.Exception | import Control.Exception | ||||||
| import Control.Monad |  | ||||||
| import Data.List | import Data.List | ||||||
| import Data.Maybe | import Data.Maybe | ||||||
| import Safe (readMay) | import Safe (readMay) | ||||||
|  | import System.Console.CmdArgs | ||||||
| import System.Directory (getModificationTime, getDirectoryContents, copyFile) | import System.Directory (getModificationTime, getDirectoryContents, copyFile) | ||||||
| import System.Exit | import System.Exit | ||||||
| import System.FilePath ((</>), splitFileName, takeDirectory) | import System.FilePath ((</>), splitFileName, takeDirectory) | ||||||
| @ -46,34 +44,22 @@ import Hledger.Utils | |||||||
| 
 | 
 | ||||||
| -- | Parse the user's specified journal file and run a hledger command on | -- | Parse the user's specified journal file and run a hledger command on | ||||||
| -- it, or throw an error. | -- it, or throw an error. | ||||||
| withJournalDo :: [Opt] -> [String] -> String -> ([Opt] -> [String] -> Journal -> IO ()) -> IO () | withJournalDo :: CliOpts -> (CliOpts -> Journal -> IO ()) -> IO () | ||||||
| withJournalDo opts args _ cmd = do | withJournalDo opts cmd = do | ||||||
|   -- We kludgily read the file before parsing to grab the full text, unless |   -- We kludgily read the file before parsing to grab the full text, unless | ||||||
|   -- it's stdin, or it doesn't exist and we are adding. We read it strictly |   -- it's stdin, or it doesn't exist and we are adding. We read it strictly | ||||||
|   -- to let the add command work. |   -- to let the add command work. | ||||||
|   journalFilePathFromOpts opts >>= readJournalFile Nothing >>= |   journalFilePathFromOpts opts >>= readJournalFile Nothing >>= | ||||||
|     either error' (cmd opts args . journalApplyAliases (aliasesFromOpts opts)) |     either error' (cmd opts . journalApplyAliases (aliasesFromOpts opts)) | ||||||
| 
 | 
 | ||||||
| -- -- | Get a journal from the given string and options, or throw an error. | -- -- | Get a journal from the given string and options, or throw an error. | ||||||
| -- readJournalWithOpts :: [Opt] -> String -> IO Journal | -- readJournalWithOpts :: CliOpts -> String -> IO Journal | ||||||
| -- readJournalWithOpts opts s = readJournal Nothing s >>= either error' return | -- readJournalWithOpts opts s = readJournal Nothing s >>= either error' return | ||||||
| 
 | 
 | ||||||
| -- | Get a journal from the given string, or throw an error. | -- | Get a journal from the given string, or throw an error. | ||||||
| readJournal' :: String -> IO Journal | readJournal' :: String -> IO Journal | ||||||
| readJournal' s = readJournal Nothing s >>= either error' return | readJournal' s = readJournal Nothing s >>= either error' return | ||||||
| 
 | 
 | ||||||
| -- | Convert this journal's transactions' primary date to either the |  | ||||||
| -- actual or effective date, as per options. |  | ||||||
| journalSelectingDateFromOpts :: [Opt] -> Journal -> Journal |  | ||||||
| journalSelectingDateFromOpts opts = journalSelectingDate (whichDateFromOpts opts) |  | ||||||
| 
 |  | ||||||
| -- | Convert this journal's postings' amounts to the cost basis amounts if |  | ||||||
| -- specified by options. |  | ||||||
| journalSelectingAmountFromOpts :: [Opt] -> Journal -> Journal |  | ||||||
| journalSelectingAmountFromOpts opts |  | ||||||
|     | CostBasis `elem` opts = journalConvertAmountsToCost |  | ||||||
|     | otherwise             = id |  | ||||||
| 
 |  | ||||||
| -- | Re-read a journal from its data file, or return an error string. | -- | Re-read a journal from its data file, or return an error string. | ||||||
| journalReload :: Journal -> IO (Either String Journal) | journalReload :: Journal -> IO (Either String Journal) | ||||||
| journalReload j = readJournalFile Nothing $ journalFilePath j | journalReload j = readJournalFile Nothing $ journalFilePath j | ||||||
| @ -83,14 +69,14 @@ journalReload j = readJournalFile Nothing $ journalFilePath j | |||||||
| -- stdin). The provided options are mostly ignored. Return a journal or | -- stdin). The provided options are mostly ignored. Return a journal or | ||||||
| -- the error message while reading it, and a flag indicating whether it | -- the error message while reading it, and a flag indicating whether it | ||||||
| -- was re-read or not. | -- was re-read or not. | ||||||
| journalReloadIfChanged :: [Opt] -> Journal -> IO (Either String Journal, Bool) | journalReloadIfChanged :: CliOpts -> Journal -> IO (Either String Journal, Bool) | ||||||
| journalReloadIfChanged opts j = do | journalReloadIfChanged _ j = do | ||||||
|   let maybeChangedFilename f = do newer <- journalSpecifiedFileIsNewer j f |   let maybeChangedFilename f = do newer <- journalSpecifiedFileIsNewer j f | ||||||
|                                   return $ if newer then Just f else Nothing |                                   return $ if newer then Just f else Nothing | ||||||
|   changedfiles <- catMaybes `fmap` mapM maybeChangedFilename (journalFilePaths j) |   changedfiles <- catMaybes `fmap` mapM maybeChangedFilename (journalFilePaths j) | ||||||
|   if not $ null changedfiles |   if not $ null changedfiles | ||||||
|    then do |    then do | ||||||
|      when (Verbose `elem` opts) $ printf "%s has changed, reloading\n" (head changedfiles) |      whenLoud $ printf "%s has changed, reloading\n" (head changedfiles) | ||||||
|      jE <- journalReload j |      jE <- journalReload j | ||||||
|      return (jE, True) |      return (jE, True) | ||||||
|    else |    else | ||||||
|  | |||||||
| @ -60,6 +60,7 @@ library | |||||||
|                   hledger-lib == 0.15 |                   hledger-lib == 0.15 | ||||||
|                  ,base >= 3 && < 5 |                  ,base >= 3 && < 5 | ||||||
|                  ,containers |                  ,containers | ||||||
|  |                  ,cmdargs >= 0.7   && < 0.8 | ||||||
|                  ,csv |                  ,csv | ||||||
|                  ,directory |                  ,directory | ||||||
|                  ,filepath |                  ,filepath | ||||||
| @ -110,6 +111,7 @@ executable hledger | |||||||
|                   hledger-lib == 0.15 |                   hledger-lib == 0.15 | ||||||
|                  ,base >= 3 && < 5 |                  ,base >= 3 && < 5 | ||||||
|                  ,containers |                  ,containers | ||||||
|  |                  ,cmdargs >= 0.7   && < 0.8 | ||||||
|                  ,csv |                  ,csv | ||||||
|                  ,directory |                  ,directory | ||||||
|                  ,filepath |                  ,filepath | ||||||
|  | |||||||
| @ -1,5 +1,5 @@ | |||||||
| # Conversion from CSV to Ledger with in-field and out-field | # Conversion from CSV to Ledger with in-field and out-field | ||||||
| rm -rf unused.journal$$ convert.rules$$; printf 'base-account Assets:MyAccount\ndate-field 0\ndate-format %%d/%%Y/%%m\ndescription-field 1\nin-field 2\nout-field 3\ncurrency $\n' >convert.rules$$ ; touch unused.journal$$ ; bin/hledger -f unused.journal$$ convert --rules convert.rules$$ - ; rm -rf *$$ | rm -rf convert.rules$$; printf 'base-account Assets:MyAccount\ndate-field 0\ndate-format %%d/%%Y/%%m\ndescription-field 1\nin-field 2\nout-field 3\ncurrency $\n' >convert.rules$$ ; bin/hledger convert --rules-file convert.rules$$ - ; rm -rf *$$ | ||||||
| <<< | <<< | ||||||
| 10/2009/09,Flubber Co,50, | 10/2009/09,Flubber Co,50, | ||||||
| 11/2009/09,Flubber Co,,50 | 11/2009/09,Flubber Co,,50 | ||||||
|  | |||||||
| @ -1,5 +1,5 @@ | |||||||
| # Conversion from CSV to Ledger | # Conversion from CSV to Ledger | ||||||
| rm -rf input.rules; printf 'base-account Assets:MyAccount\ndate-field 0\ndate-format %%d/%%Y/%%m\ndescription-field 1\namount-field 2\ncurrency $\n' > input.rules ; printf '10/2009/09,Flubber Co,50' > input.csv$$ ; touch unused.journal$$ ; bin/hledger -f unused.journal$$ convert input.csv$$ ; rm -rf input.rules *$$ | rm -rf input.rules; printf 'base-account Assets:MyAccount\ndate-field 0\ndate-format %%d/%%Y/%%m\ndescription-field 1\namount-field 2\ncurrency $\n' > input.rules ; printf '10/2009/09,Flubber Co,50' > input.csv$$ ; bin/hledger convert input.csv$$ ; rm -rf input.rules *$$ | ||||||
| >>> | >>> | ||||||
| 2009/09/10 Flubber Co | 2009/09/10 Flubber Co | ||||||
|     income:unknown            $-50 |     income:unknown            $-50 | ||||||
|  | |||||||
| @ -12,8 +12,8 @@ bin/hledger -f- print | |||||||
| 
 | 
 | ||||||
| >>>=0 | >>>=0 | ||||||
| 
 | 
 | ||||||
| # 2. convert to cost basis | # 2. convert to cost | ||||||
| bin/hledger -f- print -B | bin/hledger -f- print --cost | ||||||
| <<< | <<< | ||||||
| 2011/01/01 | 2011/01/01 | ||||||
|     expenses:foreign currency       €100 @ $1.35 |     expenses:foreign currency       €100 @ $1.35 | ||||||
| @ -135,7 +135,7 @@ bin/hledger -f - balance -B | |||||||
|                    0 |                    0 | ||||||
| >>>=0 | >>>=0 | ||||||
| # 10. transaction in two commodities should balance out properly | # 10. transaction in two commodities should balance out properly | ||||||
| bin/hledger -f - balance --basis | bin/hledger -f - balance --cost | ||||||
| <<< | <<< | ||||||
| 2011/01/01 x | 2011/01/01 x | ||||||
|   a  10£ @@ 16$ |   a  10£ @@ 16$ | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user