feat:run: run multiple commands efficiently, as a script or at a REPL (Dmitry Astapov, #2323)

This commit is contained in:
Dmitry Astapov 2025-02-14 02:13:07 +00:00 committed by GitHub
parent 9597210556
commit d84050ec05
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 255 additions and 1 deletions

View File

@ -450,6 +450,7 @@ src/hledger/
Register.md
Rewrite.md
Roi.md
Run.md
Stats.md
Tags.md
Test.md

View File

@ -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,

View File

@ -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..)"

View File

@ -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

View File

@ -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 <text>` 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
```

View File

@ -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 <text> 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

View File

@ -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

View File

@ -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