run: split run and repl, fix journal passed to nested "run"s

This commit is contained in:
Dmitry Astapov 2025-02-27 21:01:08 +00:00 committed by Simon Michael
parent a7116a8b0f
commit 61faca16e4
8 changed files with 173 additions and 74 deletions

View File

@ -449,6 +449,7 @@ src/hledger/
README.md README.md
Register.md Register.md
Rewrite.md Rewrite.md
Repl.md
Roi.md Roi.md
Run.md Run.md
Stats.md Stats.md

View File

@ -429,8 +429,9 @@ main = withGhcDebug' $ do
ensureJournalFileExists . NE.head =<< journalFilePathFromOpts opts ensureJournalFileExists . NE.head =<< journalFilePathFromOpts opts
withJournalDo opts (cmdaction opts) withJournalDo opts (cmdaction opts)
-- 6.5.4. run needs findBuiltinCommands passed to it to avoid circular dependency in the code -- 6.5.4. "run" and "repl" need findBuiltinCommands passed to it to avoid circular dependency in the code
| cmdname == "run" -> do withJournalDo opts $ Hledger.Cli.Commands.Run.run findBuiltinCommand opts | cmdname == "run" -> Hledger.Cli.Commands.Run.run Nothing findBuiltinCommand opts
| cmdname == "repl" -> Hledger.Cli.Commands.Run.repl findBuiltinCommand opts
-- 6.5.5. all other builtin commands - read the journal and if successful run the command with it -- 6.5.5. all other builtin commands - read the journal and if successful run the command with it
| otherwise -> withJournalDo opts $ cmdaction opts | otherwise -> withJournalDo opts $ cmdaction opts

View File

@ -136,7 +136,8 @@ builtinCommands = [
,(registermode , register) ,(registermode , register)
,(rewritemode , rewrite) ,(rewritemode , rewrite)
,(roimode , roi) ,(roimode , roi)
,(runmode , run') ,(runmode , runOrReplStub)
,(replmode , runOrReplStub)
,(statsmode , stats) ,(statsmode , stats)
,(tagsmode , tags) ,(tagsmode , tags)
,(testmode , testcmd) ,(testmode , testcmd)

View File

@ -0,0 +1,45 @@
## repl
Runs hledger commands interactively.
This command is EXPERIMENTAL and could change in the future.
```flags
Flags:
no command-specific flags
```
This command starts a read-eval-print loop (REPL) where you can enter commands interactively. To exit REPL, use "exit" or "quit", or send EOF.
It could also accept commands from standard input, if you pipe commands into it.
The commands will run more quickly than if run individually, because the input files would be parsed only once.
Syntax of the commands 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.
### Caveats:
- `Repl`, like any other command, will load the input file(s) (specified by `LEDGER_JOURNAL` or by `-f` arguments). The contents of those files would be used by all the commands that `repl` runs. If you want a particular command to use a different input file, you can use `-f` flag for that particular command. This will override (not add) the input for that particular command. All the input files would be cached, and would be read only once.
### Examples:
To start the REPL:
```cli
hledger repl
```
or
```cli
hledger repl -f some.journal
```
To pipe commands into REPL:
```cli
(echo "files"; echo "stats") | hledger repl -f some.journal
```

View File

@ -0,0 +1,51 @@
repl
Runs hledger commands interactively.
This command is EXPERIMENTAL and could change in the future.
Flags:
no command-specific flags
This command starts a read-eval-print loop (REPL) where you can enter
commands interactively. To exit REPL, use "exit" or "quit", or send EOF.
It could also accept commands from standard input, if you pipe commands
into it.
The commands will run more quickly than if run individually, because the
input files would be parsed only once.
Syntax of the commands 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.
Caveats:
- Repl, like any other command, will load the input file(s) (specified
by LEDGER_JOURNAL or by -f arguments). The contents of those files
would be used by all the commands that repl runs. If you want a
particular command to use a different input file, you can use -f
flag for that particular command. This will override (not add) the
input for that particular command. All the input files would be
cached, and would be read only once.
Examples:
To start the REPL:
hledger repl
or
hledger repl -f some.journal
To pipe commands into REPL:
(echo "files"; echo "stats") | hledger repl -f some.journal

View File

@ -12,7 +12,9 @@ The @run@ command allows you to run multiple commands via REPL or from the suppl
module Hledger.Cli.Commands.Run ( module Hledger.Cli.Commands.Run (
runmode runmode
,run ,run
,run' ,replmode
,repl
,runOrReplStub
) where ) where
import qualified Data.Map.Strict as Map import qualified Data.Map.Strict as Map
@ -46,23 +48,30 @@ runmode = hledgerCommandMode
hiddenflags hiddenflags
([], Just $ argsFlag "[COMMANDS_FILE1 COMMANDS_FILE2 ...] OR [command1 args... -- command2 args... -- command3 args...]") ([], Just $ argsFlag "[COMMANDS_FILE1 COMMANDS_FILE2 ...] OR [command1 args... -- command2 args... -- command3 args...]")
-- | The fake run command introduced to break circular dependency. replmode = hledgerCommandMode
$(embedFileRelative "Hledger/Cli/Commands/Repl.txt")
(
[]
)
cligeneralflagsgroups1
hiddenflags
([], Nothing)
-- | The fake run/repl command introduced to break circular dependency.
-- This module needs access to `findBuiltinCommand`, which is defined in Hledger.Cli.Commands -- This module needs access to `findBuiltinCommand`, which is defined in Hledger.Cli.Commands
-- However, Hledger.Cli.Commands imports this module, which creates circular dependency. -- However, Hledger.Cli.Commands imports this module, which creates circular dependency.
-- We expose this do-nothing function so that it could be included in the list of all commands inside -- We expose this do-nothing function so that it could be included in the list of all commands inside
-- Hledger.Cli.Commands and ensure that "run" is recognized as a valid command by the Hledger.Cli top-level -- Hledger.Cli.Commands and ensure that "run" is recognized as a valid command by the Hledger.Cli top-level
-- command line parser. That parser, however, would not call run'. It has a special case for "run", and -- command line parser. That parser, however, would not call run'. It has a special case for "run", and
-- will call "run" (see below), passing it `findBuiltinCommand`, thus breaking circular dependency. -- will call "run" (see below), passing it `findBuiltinCommand`, thus breaking circular dependency.
run' :: CliOpts -> Journal -> IO () runOrReplStub :: CliOpts -> Journal -> IO ()
run' _opts _j = return () runOrReplStub _opts _j = return ()
-- | The actual run command. -- | The actual run command.
run :: (String -> Maybe (Mode RawOpts, CliOpts -> Journal -> IO ())) -> CliOpts -> Journal -> IO () run :: Maybe Journal -> (String -> Maybe (Mode RawOpts, CliOpts -> Journal -> IO ())) -> CliOpts -> IO ()
run findBuiltinCommand CliOpts{rawopts_=rawopts} j = do run defaultJournalOverride findBuiltinCommand cliopts@CliOpts{rawopts_=rawopts} = do
let args = dbg1 "args" $ listofstringopt "args" rawopts withJournalCached defaultJournalOverride cliopts $ \j -> do
if args == [] let args = dbg1 "args" $ listofstringopt "args" rawopts
then runREPL j findBuiltinCommand
else do
-- Check if arguments could be interpreted as files. -- Check if arguments could be interpreted as files.
-- If not, assume that they are commands specified directly on the command line -- If not, assume that they are commands specified directly on the command line
allAreFiles <- and <$> mapM (doesFileExist . snd . splitReaderPrefix) args allAreFiles <- and <$> mapM (doesFileExist . snd . splitReaderPrefix) args
@ -70,6 +79,12 @@ run findBuiltinCommand CliOpts{rawopts_=rawopts} j = do
True -> runFromFiles j findBuiltinCommand args True -> runFromFiles j findBuiltinCommand args
False -> runFromArgs j findBuiltinCommand args False -> runFromArgs j findBuiltinCommand args
-- | The actual repl command.
repl :: (String -> Maybe (Mode RawOpts, CliOpts -> Journal -> IO ())) -> CliOpts -> IO ()
repl findBuiltinCommand cliopts = do
withJournalCached Nothing cliopts $ \j -> do
runREPL j findBuiltinCommand
-- | Run commands from files given to "run". -- | Run commands from files given to "run".
runFromFiles :: Journal -> (String -> Maybe (Mode RawOpts, CliOpts -> Journal -> IO ())) -> [String] -> IO () runFromFiles :: Journal -> (String -> Maybe (Mode RawOpts, CliOpts -> Journal -> IO ())) -> [String] -> IO ()
runFromFiles defaultJrnl findBuiltinCommand inputfiles = do runFromFiles defaultJrnl findBuiltinCommand inputfiles = do
@ -105,14 +120,15 @@ runCommand defaultJrnl findBuiltinCommand cmdline = do
case findBuiltinCommand cmdname of case findBuiltinCommand cmdname of
Nothing -> putStrLn $ unwords (cmdname:args) Nothing -> putStrLn $ unwords (cmdname:args)
Just (cmdmode,cmdaction) -> do Just (cmdmode,cmdaction) -> do
-- Allow "run" to call "run"
let cmdaction' = if cmdname == "run" then run findBuiltinCommand else cmdaction
-- Even though expandArgsAt is done by the Cli.hs, it stops at the first '--', so we need -- Even though expandArgsAt is done by the Cli.hs, it stops at the first '--', so we need
-- to do it here as well to make sure that each command can use @ARGFILEs -- to do it here as well to make sure that each command can use @ARGFILEs
args' <- replaceNumericFlags <$> expandArgsAt args args' <- replaceNumericFlags <$> expandArgsAt args
dbg1IO "runCommand final args" (cmdname,args') dbg1IO "runCommand final args" (cmdname,args')
opts <- getHledgerCliOpts' cmdmode args' opts <- getHledgerCliOpts' cmdmode args'
withJournalCached defaultJrnl opts (cmdaction' opts) withJournalCached (Just defaultJrnl) opts $ \j -> do
if cmdname == "run" -- allow "run" to call "run"
then run (Just j) findBuiltinCommand opts
else cmdaction opts j
[] -> return () [] -> return ()
-- | Run an interactive REPL. -- | Run an interactive REPL.
@ -139,14 +155,19 @@ journalCache = unsafePerformIO $ newMVar Map.empty
{-# NOINLINE journalCache #-} {-# NOINLINE journalCache #-}
-- | Similar to `withJournal`, but uses caches all the journals it reads. -- | Similar to `withJournal`, but uses caches all the journals it reads.
withJournalCached :: Journal -> CliOpts -> (Journal -> IO ()) -> IO () withJournalCached :: Maybe Journal -> CliOpts -> (Journal -> IO ()) -> IO ()
withJournalCached defaultJrnl cliopts cmd = do withJournalCached defaultJournalOverride cliopts cmd = do
mbjournalpaths <- journalFilePathFromOptsNoDefault cliopts j <- case defaultJournalOverride of
j <- case mbjournalpaths of Nothing -> journalFilePathFromOpts cliopts >>= readFiles
Nothing -> return defaultJrnl -- use the journal given to the "run" itself Just defaultJrnl -> do
Just journalpaths -> journalTransform cliopts . sconcat <$> mapM (readAndCacheJournalFile (inputopts_ cliopts)) journalpaths mbjournalpaths <- journalFilePathFromOptsNoDefault cliopts
case mbjournalpaths of
Nothing -> return defaultJrnl -- use the journal given to the "run" itself
Just journalpaths -> readFiles journalpaths
cmd j cmd j
where where
readFiles journalpaths =
journalTransform cliopts . sconcat <$> mapM (readAndCacheJournalFile (inputopts_ cliopts)) journalpaths
-- | Read a journal file, caching it if it has not been read before. -- | Read a journal file, caching it if it has not been read before.
readAndCacheJournalFile :: InputOpts -> PrefixedFilePath -> IO Journal readAndCacheJournalFile :: InputOpts -> PrefixedFilePath -> IO Journal
readAndCacheJournalFile iopts fp | snd (splitReaderPrefix fp) == "-" = do readAndCacheJournalFile iopts fp | snd (splitReaderPrefix fp) == "-" = do

View File

@ -1,6 +1,6 @@
## run ## run
Runs a sequence of hledger commands on the same input file(s), either interactively or as a script. Runs a sequence of hledger commands on the same input file(s), taking them from the command line or from file(s).
This command is EXPERIMENTAL and syntax could change in the future. This command is EXPERIMENTAL and syntax could change in the future.
@ -11,20 +11,19 @@ no command-specific flags
The commands will run more quickly than if run individually, because the input files would be parsed only once. 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: "run" has two 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. - when all positional arguments of "run" are valid file names, "run" will read commands from these files, in order: `run -f some.journal file1.txt file2.txt file3.txt`.
- lastly, commands could be specified directly on the command line. All commands (including the very first one) should be preceded by argument "--" - commands could be specified directly on the command line. All commands (including the very first one) should be preceded by argument "--": `run -f some.journal -- cmd1 -- cmd2 -- cmd3`.
Syntax of the commands (either in the file, or in REPL) is intentionally simple: Syntax of the command is intentionally simple:
- each line is a single hledger command - each line read from a file is a single hledger command
- lines that can't be interpreted as hledger commands are printed out as-is - lines that can't be interpreted as hledger commands are printed out as-is
- empty lines are skipped - empty lines are skipped
- everything after `#` is considered to be a comment and will be ignored, and will not be printed out - 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 - `echo <text>` will print out text, even if it could be recognized as a hledger command
- `run` is a valid command to give use as well, so you can have `run` call `run` if you want to. - `run` is a valid command to use as well, so you can have `run` call `run` if you want to.
You can use single quotes or double quotes to quote aguments that need quoting. You can use single quotes or double quotes to quote aguments that need quoting.
@ -34,20 +33,10 @@ You can use `#!/usr/bin/env hledger run` in the first line of the file to make i
- 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. - 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.
- `Run`, like any other command, will load the input file(s) (specified by `LEDGER_JOURNAL` or by `-f` arguments). The contents of those files would be used by all the commands that `run` runs. If you want a particular command to use a different input file, you can use `-f` flag for that particular command. This will override (not add) the input for that particular command. All the files read would be cached, and would be read only once. - `Run`, like any other command, will load the input file(s) (specified by `LEDGER_JOURNAL` or by `-f` arguments). The contents of those files would be used by all the commands that `run` runs. If you want a particular command to use a different input file, you can use `-f` flag for that particular command. This will override (not add) the input for that particular command. All the input files would be cached, and would be read only once.
### Examples: ### Examples:
To start the REPL:
```cli
hledger run
```
or
```cli
hledger run -f some.journal
```
To provide commands on the command line, separate them with `--`: To provide commands on the command line, separate them with `--`:
```cli ```cli
hledger run -f some.journal -- balance assets --depth 2 -- balance liabilities -f /some/other.journal --depth 3 --transpose -- stats hledger run -f some.journal -- balance assets --depth 2 -- balance liabilities -f /some/other.journal --depth 3 --transpose -- stats
@ -56,11 +45,11 @@ This would load `some.journal`, run `balance assets --depth 2` on it, then run `
To provide commands in the file, as a runnable scripts: To provide commands in the file, as a runnable scripts:
```cli ```cli
#!/usr/bin/env -S hledger run #!/usr/bin/env -S hledger run -f some.journal
echo "List of accounts" echo "List of accounts in some.journal"
accounts accounts
echo "Assets" echo "Assets of some.journal"
balance assets --depth 2 balance assets --depth 2
echo "Liabilities from /some/other.journal" echo "Liabilities from /some/other.journal"

View File

@ -1,7 +1,7 @@
run run
Runs a sequence of hledger commands on the same input file(s), either Runs a sequence of hledger commands on the same input file(s), taking
interactively or as a script. them from the command line or from file(s).
This command is EXPERIMENTAL and syntax could change in the future. This command is EXPERIMENTAL and syntax could change in the future.
@ -11,25 +11,23 @@ no command-specific flags
The commands will run more quickly than if run individually, because the The commands will run more quickly than if run individually, because the
input files would be parsed only once. input files would be parsed only once.
"run" has three ways of invocation: - when invoked without arguments, it "run" has two ways of invocation:
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 - when all positional arguments of "run" are valid file names, "run"
files, in order. will read commands from these files, in order:
run -f some.journal file1.txt file2.txt file3.txt.
- lastly, commands could be specified directly on the command line. - commands could be specified directly on the command line. All
All commands (including the very first one) should be preceded by commands (including the very first one) should be preceded by
argument "--" argument "--": run -f some.journal -- cmd1 -- cmd2 -- cmd3.
Syntax of the commands (either in the file, or in REPL) is intentionally Syntax of the command is intentionally simple: - each line read from a
simple: - each line is a single hledger command - lines that can't be file is a single hledger command - lines that can't be interpreted as
interpreted as hledger commands are printed out as-is - empty lines are hledger commands are printed out as-is - empty lines are skipped -
skipped - everything after # is considered to be a comment and will be everything after # is considered to be a comment and will be ignored,
ignored, and will not be printed out - echo <text> will print out text, and will not be printed out - echo <text> will print out text, even if
even if it could be recognized as a hledger command - run is a valid it could be recognized as a hledger command - run is a valid command to
command to give use as well, so you can have run call run if you want use as well, so you can have run call run if you want to.
to.
You can use single quotes or double quotes to quote aguments that need You can use single quotes or double quotes to quote aguments that need
quoting. quoting.
@ -49,19 +47,11 @@ Caveats:
would be used by all the commands that run runs. If you want a would be used by all the commands that run runs. If you want a
particular command to use a different input file, you can use -f particular command to use a different input file, you can use -f
flag for that particular command. This will override (not add) the flag for that particular command. This will override (not add) the
input for that particular command. All the files read would be input for that particular command. All the input files would be
cached, and would be read only once. cached, and would be read only once.
Examples: Examples:
To start the REPL:
hledger run
or
hledger run -f some.journal
To provide commands on the command line, separate them with --: To provide commands on the command line, separate them with --:
hledger run -f some.journal -- balance assets --depth 2 -- balance liabilities -f /some/other.journal --depth 3 --transpose -- stats hledger run -f some.journal -- balance assets --depth 2 -- balance liabilities -f /some/other.journal --depth 3 --transpose -- stats
@ -72,11 +62,11 @@ and finally will run stats on some.journal
To provide commands in the file, as a runnable scripts: To provide commands in the file, as a runnable scripts:
#!/usr/bin/env -S hledger run #!/usr/bin/env -S hledger run -f some.journal
echo "List of accounts" echo "List of accounts in some.journal"
accounts accounts
echo "Assets" echo "Assets of some.journal"
balance assets --depth 2 balance assets --depth 2
echo "Liabilities from /some/other.journal" echo "Liabilities from /some/other.journal"