diff --git a/doc/FILES.md b/doc/FILES.md index 9bb3c0832..7879c00c4 100644 --- a/doc/FILES.md +++ b/doc/FILES.md @@ -450,6 +450,7 @@ src/hledger/ Register.md Rewrite.md Roi.md + Run.md Stats.md Tags.md Test.md diff --git a/hledger/Hledger/Cli.hs b/hledger/Hledger/Cli.hs index e224e5963..975911cff 100644 --- a/hledger/Hledger/Cli.hs +++ b/hledger/Hledger/Cli.hs @@ -118,6 +118,7 @@ import Hledger import Hledger.Cli.CliOptions import Hledger.Cli.Conf import Hledger.Cli.Commands +import Hledger.Cli.Commands.Run import Hledger.Cli.DocFiles import Hledger.Cli.Utils import Hledger.Cli.Version @@ -417,7 +418,10 @@ main = withGhcDebug' $ do ensureJournalFileExists . NE.head =<< journalFilePathFromOpts opts withJournalDo opts (cmdaction opts) - -- 6.5.4. all other builtin commands - read the journal and if successful run the command with it + -- 6.5.4. run needs findBuiltinCommands passed to it to avoid circular dependency in the code + | cmdname == "run" -> do withJournalDo opts $ Hledger.Cli.Commands.Run.run findBuiltinCommand opts + + -- 6.5.5. all other builtin commands - read the journal and if successful run the command with it | otherwise -> withJournalDo opts $ cmdaction opts -- 6.6. external addon command found - run it, diff --git a/hledger/Hledger/Cli/Commands.hs b/hledger/Hledger/Cli/Commands.hs index 6c5fd0a86..f4b30ebed 100644 --- a/hledger/Hledger/Cli/Commands.hs +++ b/hledger/Hledger/Cli/Commands.hs @@ -46,6 +46,7 @@ module Hledger.Cli.Commands ( ,module Hledger.Cli.Commands.Print ,module Hledger.Cli.Commands.Register ,module Hledger.Cli.Commands.Rewrite + ,module Hledger.Cli.Commands.Run ,module Hledger.Cli.Commands.Stats ,module Hledger.Cli.Commands.Tags ) @@ -91,6 +92,7 @@ import Hledger.Cli.Commands.Print import Hledger.Cli.Commands.Register import Hledger.Cli.Commands.Rewrite import Hledger.Cli.Commands.Roi +import Hledger.Cli.Commands.Run import Hledger.Cli.Commands.Stats import Hledger.Cli.Commands.Tags import Hledger.Cli.Utils (tests_Cli_Utils) @@ -126,6 +128,7 @@ builtinCommands = [ ,(registermode , register) ,(rewritemode , rewrite) ,(roimode , roi) + ,(runmode , run') ,(statsmode , stats) ,(tagsmode , tags) ,(testmode , testcmd) @@ -253,6 +256,7 @@ commandsList progversion othercmds = ," balance (bal) show balance changes, end balances, gains, budgets.." ,"+lots show a commodity's lots" -- hledger-lots ," roi show return on investments" + ," run run multiple commands from a file (EXPERIMENTAL)" ,"" -----------------------------------------80------------------------------------- ,bold' "CHARTS (bar charts, line graphs..)" diff --git a/hledger/Hledger/Cli/Commands/Run.hs b/hledger/Hledger/Cli/Commands/Run.hs new file mode 100644 index 000000000..f34cf1b65 --- /dev/null +++ b/hledger/Hledger/Cli/Commands/Run.hs @@ -0,0 +1,112 @@ +{-| + +The @run@ command allows you to run multiple commands via REPL or from the supplied file(s). + +-} + +{-# LANGUAGE MultiWayIf #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TemplateHaskell #-} + +module Hledger.Cli.Commands.Run ( + runmode + ,run + ,run' +) where + +import qualified Data.Text as T +import qualified Data.Text.IO as T +import System.Console.CmdArgs.Explicit as C ( Mode ) +import Hledger +import Hledger.Cli.CliOptions + +import Control.Monad (forM_) +import Control.Monad.IO.Class (liftIO) +import Control.Monad.Extra (concatMapM) + +import System.Directory (doesFileExist) +import System.Console.Haskeline + +import Safe (headMay) + +-- | Command line options for this command. +runmode = hledgerCommandMode + $(embedFileRelative "Hledger/Cli/Commands/Run.txt") + ( + [] + ) + cligeneralflagsgroups1 + hiddenflags + ([], Just $ argsFlag "[COMMANDS_FILE1 COMMANDS_FILE2 ...]") + +-- | The fake run command introduced to break circular dependency +run' :: CliOpts -> Journal -> IO () +run' _opts _j = return () + +-- | The actual run command. +run :: (String -> Maybe (Mode RawOpts, CliOpts -> Journal -> IO ())) -> CliOpts -> Journal -> IO () +run findBuiltinCommand CliOpts{rawopts_=rawopts} j = do + let args = dbg1 "args" $ listofstringopt "args" rawopts + case args of + [] -> runREPL findBuiltinCommand j + maybeFile:_ -> do + -- Check if arguments could be interpreted as files. + -- If not, assume that they are files + isFile <- doesFileExist maybeFile + case isFile of + True -> runFromFiles findBuiltinCommand args j + False -> runFromArgs findBuiltinCommand args j + +runFromFiles :: (String -> Maybe (Mode RawOpts, CliOpts -> Journal -> IO ())) -> [String] -> Journal -> IO () +runFromFiles findBuiltinCommand inputfiles j = do + dbg1IO "inputfiles" inputfiles + -- read commands from all the inputfiles + commands <- (flip concatMapM) inputfiles $ \f -> do + dbg1IO "reading commands" f + lines . T.unpack <$> T.readFile f + + forM_ commands (runCommand findBuiltinCommand j . parseCommand) + +runFromArgs :: (String -> Maybe (Mode RawOpts, CliOpts -> Journal -> IO ())) -> [String] -> Journal -> IO () +runFromArgs findBuiltinCommand args j = do + -- read commands from all the inputfiles + let commands = dbg1 "commands from args" $ splitAtElement "--" args + forM_ commands (runCommand findBuiltinCommand j) + +-- When commands are passed on the command line, shell will parse them for us +-- When commands are read from file, we need to split the line into command and arguments +parseCommand :: String -> [String] +parseCommand line = + -- # begins a comment, ignore everything after # + takeWhile (not. ((Just '#')==) . headMay) $ words' (strip line) + +runCommand :: (String -> Maybe (Mode RawOpts, CliOpts -> Journal -> IO ())) -> Journal -> [String] -> IO () +runCommand findBuiltinCommand j cmdline = do + dbg1IO "running command" cmdline + -- # begins a comment, ignore everything after # + case cmdline of + "echo":args -> putStrLn $ unwords $ args + cmdname:args -> + case findBuiltinCommand cmdname of + Nothing -> putStrLn $ unwords (cmdname:args) + Just (cmdmode,cmdaction) -> do + opts <- getHledgerCliOpts' cmdmode args + cmdaction opts j + [] -> return () + +runREPL :: (String -> Maybe (Mode RawOpts, CliOpts -> Journal -> IO ())) -> Journal -> IO () +runREPL findBuiltinCommand j = do + putStrLn "Enter hledger commands, or 'help' for help." + runInputT defaultSettings loop + where + loop :: InputT IO () + loop = do + minput <- getInputLine "% " + case minput of + Nothing -> return () + Just "quit" -> return () + Just "exit" -> return () + Just input -> do + liftIO $ runCommand findBuiltinCommand j $ parseCommand input + loop diff --git a/hledger/Hledger/Cli/Commands/Run.md b/hledger/Hledger/Cli/Commands/Run.md new file mode 100644 index 000000000..3bce630ee --- /dev/null +++ b/hledger/Hledger/Cli/Commands/Run.md @@ -0,0 +1,62 @@ +## run + +Runs a sequence of hledger commands on the same input file(s), either interactively or as a script. + +This command is EXPERIMENTAL and syntax could change in the future. + +```flags +Flags: +no command-specific flags +``` + +The commands will run more quickly than if run individually, because the input files would be parsed only once. + +"run" has three ways of invocation: +- when invoked without arguments, it start a read-eval-print loop (REPL) where you can enter commands interactively. To exit REPL, use "exit" or "quit", or send EOF. + +- when file names are given to "run", it will read commands from these files, in order. + +- lastly, commands could be specified directly on the command line. All commands (including the very first one) should be preceded by argument "--" + +Syntax of the commands (either in the file, or in REPL) is intentionally simple: +- each line is a single hledger command +- lines that can't be interpreted as hledger commands are printed out as-is +- empty lines are skipped +- everything after `#` is considered to be a comment and will be ignored, and will not be printed out +- `echo ` will print out text, even if it could be recognized as a hledger command + +You can use single quotes or double quotes to quote aguments that need quoting. + +You can use `#!/usr/bin/env hledger run` in the first line of the file to make it a runnable script. If this complains about "binary `hledger run` not found", use `/usr/bin/env -S hledger run`. + +### Caveats: + +- If you meant to provide file name as an argument, but made a mistake and a gave file name that does not exist, "run" will attempt to interpret it as a command. + +- Numeric flags like `-3` do not work, use long form `--depth 3` + +### Examples: + +To start the REPL: +```cli +hledger run +``` + +To provide commands on the command line, separate them with `--`: +```cli +hledger run -f some.journal -- balance assets --depth 2 -- balance liabilities --depth 3 --transpose +``` + +To provide commands in the file, as a runnable scripts: +```cli +#!/usr/bin/env -S hledger run +echo "List of accounts" +accounts + +echo "Assets" +balance assets --depth 2 + +echo "Liabilities" +balance liabilities --depth 3 --transpose +``` + diff --git a/hledger/Hledger/Cli/Commands/Run.txt b/hledger/Hledger/Cli/Commands/Run.txt new file mode 100644 index 000000000..23d9c088f --- /dev/null +++ b/hledger/Hledger/Cli/Commands/Run.txt @@ -0,0 +1,67 @@ +run + +Runs a sequence of hledger commands on the same input file(s), either +interactively or as a script. + +This command is EXPERIMENTAL and syntax could change in the future. + +Flags: +no command-specific flags + +The commands will run more quickly than if run individually, because the +input files would be parsed only once. + +"run" has three ways of invocation: - when invoked without arguments, it +start a read-eval-print loop (REPL) where you can enter commands +interactively. To exit REPL, use "exit" or "quit", or send EOF. + +- when file names are given to "run", it will read commands from these + files, in order. + +- lastly, commands could be specified directly on the command line. + All commands (including the very first one) should be preceded by + argument "--" + +Syntax of the commands (either in the file, or in REPL) is intentionally +simple: - each line is a single hledger command - lines that can't be +interpreted as hledger commands are printed out as-is - empty lines are +skipped - everything after # is considered to be a comment and will be +ignored, and will not be printed out - echo will print out text, +even if it could be recognized as a hledger command + +You can use single quotes or double quotes to quote aguments that need +quoting. + +You can use #!/usr/bin/env hledger run in the first line of the file to +make it a runnable script. If this complains about "binary hledger run +not found", use /usr/bin/env -S hledger run. + +Caveats: + +- If you meant to provide file name as an argument, but made a mistake + and a gave file name that does not exist, "run" will attempt to + interpret it as a command. + +- Numeric flags like -3 do not work, use long form --depth 3 + +Examples: + +To start the REPL: + +hledger run + +To provide commands on the command line, separate them with --: + +hledger run -f some.journal -- balance assets --depth 2 -- balance liabilities --depth 3 --transpose + +To provide commands in the file, as a runnable scripts: + +#!/usr/bin/env -S hledger run +echo "List of accounts" +accounts + +echo "Assets" +balance assets --depth 2 + +echo "Liabilities" +balance liabilities --depth 3 --transpose diff --git a/hledger/hledger.cabal b/hledger/hledger.cabal index d51c9b02f..5d027ec80 100644 --- a/hledger/hledger.cabal +++ b/hledger/hledger.cabal @@ -84,6 +84,7 @@ extra-source-files: Hledger/Cli/Commands/Register.txt Hledger/Cli/Commands/Rewrite.txt Hledger/Cli/Commands/Roi.txt + Hledger/Cli/Commands/Run.txt Hledger/Cli/Commands/Stats.txt Hledger/Cli/Commands/Tags.txt Hledger/Cli/Commands/Test.txt @@ -140,6 +141,7 @@ library Hledger.Cli.Commands.Register Hledger.Cli.Commands.Rewrite Hledger.Cli.Commands.Roi + Hledger.Cli.Commands.Run Hledger.Cli.Commands.Stats Hledger.Cli.Commands.Tags Hledger.Cli.CompoundBalanceCommand diff --git a/hledger/package.yaml b/hledger/package.yaml index 585528ec9..2c877f34f 100644 --- a/hledger/package.yaml +++ b/hledger/package.yaml @@ -83,6 +83,7 @@ extra-source-files: - Hledger/Cli/Commands/Register.txt - Hledger/Cli/Commands/Rewrite.txt - Hledger/Cli/Commands/Roi.txt +- Hledger/Cli/Commands/Run.txt - Hledger/Cli/Commands/Stats.txt - Hledger/Cli/Commands/Tags.txt - Hledger/Cli/Commands/Test.txt @@ -201,6 +202,7 @@ library: - Hledger.Cli.Commands.Register - Hledger.Cli.Commands.Rewrite - Hledger.Cli.Commands.Roi + - Hledger.Cli.Commands.Run - Hledger.Cli.Commands.Stats - Hledger.Cli.Commands.Tags - Hledger.Cli.CompoundBalanceCommand