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
Register.md
Rewrite.md
Repl.md
Roi.md
Run.md
Stats.md

View File

@ -429,8 +429,9 @@ main = withGhcDebug' $ do
ensureJournalFileExists . NE.head =<< journalFilePathFromOpts opts
withJournalDo opts (cmdaction opts)
-- 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.4. "run" and "repl" need findBuiltinCommands passed to it to avoid circular dependency in the code
| 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
| otherwise -> withJournalDo opts $ cmdaction opts

View File

@ -136,7 +136,8 @@ builtinCommands = [
,(registermode , register)
,(rewritemode , rewrite)
,(roimode , roi)
,(runmode , run')
,(runmode , runOrReplStub)
,(replmode , runOrReplStub)
,(statsmode , stats)
,(tagsmode , tags)
,(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 (
runmode
,run
,run'
,replmode
,repl
,runOrReplStub
) where
import qualified Data.Map.Strict as Map
@ -46,23 +48,30 @@ runmode = hledgerCommandMode
hiddenflags
([], 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
-- 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
-- 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
-- will call "run" (see below), passing it `findBuiltinCommand`, thus breaking circular dependency.
run' :: CliOpts -> Journal -> IO ()
run' _opts _j = return ()
runOrReplStub :: CliOpts -> Journal -> IO ()
runOrReplStub _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
if args == []
then runREPL j findBuiltinCommand
else do
run :: Maybe Journal -> (String -> Maybe (Mode RawOpts, CliOpts -> Journal -> IO ())) -> CliOpts -> IO ()
run defaultJournalOverride findBuiltinCommand cliopts@CliOpts{rawopts_=rawopts} = do
withJournalCached defaultJournalOverride cliopts $ \j -> do
let args = dbg1 "args" $ listofstringopt "args" rawopts
-- Check if arguments could be interpreted as files.
-- If not, assume that they are commands specified directly on the command line
allAreFiles <- and <$> mapM (doesFileExist . snd . splitReaderPrefix) args
@ -70,6 +79,12 @@ run findBuiltinCommand CliOpts{rawopts_=rawopts} j = do
True -> runFromFiles 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".
runFromFiles :: Journal -> (String -> Maybe (Mode RawOpts, CliOpts -> Journal -> IO ())) -> [String] -> IO ()
runFromFiles defaultJrnl findBuiltinCommand inputfiles = do
@ -105,14 +120,15 @@ runCommand defaultJrnl findBuiltinCommand cmdline = do
case findBuiltinCommand cmdname of
Nothing -> putStrLn $ unwords (cmdname:args)
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
-- to do it here as well to make sure that each command can use @ARGFILEs
args' <- replaceNumericFlags <$> expandArgsAt args
dbg1IO "runCommand final args" (cmdname,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 ()
-- | Run an interactive REPL.
@ -139,14 +155,19 @@ journalCache = unsafePerformIO $ newMVar Map.empty
{-# NOINLINE journalCache #-}
-- | Similar to `withJournal`, but uses caches all the journals it reads.
withJournalCached :: Journal -> CliOpts -> (Journal -> IO ()) -> IO ()
withJournalCached defaultJrnl cliopts cmd = do
mbjournalpaths <- journalFilePathFromOptsNoDefault cliopts
j <- case mbjournalpaths of
Nothing -> return defaultJrnl -- use the journal given to the "run" itself
Just journalpaths -> journalTransform cliopts . sconcat <$> mapM (readAndCacheJournalFile (inputopts_ cliopts)) journalpaths
withJournalCached :: Maybe Journal -> CliOpts -> (Journal -> IO ()) -> IO ()
withJournalCached defaultJournalOverride cliopts cmd = do
j <- case defaultJournalOverride of
Nothing -> journalFilePathFromOpts cliopts >>= readFiles
Just defaultJrnl -> do
mbjournalpaths <- journalFilePathFromOptsNoDefault cliopts
case mbjournalpaths of
Nothing -> return defaultJrnl -- use the journal given to the "run" itself
Just journalpaths -> readFiles journalpaths
cmd j
where
readFiles journalpaths =
journalTransform cliopts . sconcat <$> mapM (readAndCacheJournalFile (inputopts_ cliopts)) journalpaths
-- | Read a journal file, caching it if it has not been read before.
readAndCacheJournalFile :: InputOpts -> PrefixedFilePath -> IO Journal
readAndCacheJournalFile iopts fp | snd (splitReaderPrefix fp) == "-" = do

View File

@ -1,6 +1,6 @@
## 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.
@ -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.
"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.
"run" has two ways of invocation:
- 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:
- each line is a single hledger command
Syntax of the command is intentionally simple:
- 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
- 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
- `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.
@ -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.
- `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:
To start the REPL:
```cli
hledger run
```
or
```cli
hledger run -f some.journal
```
To provide commands on the command line, separate them with `--`:
```cli
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:
```cli
#!/usr/bin/env -S hledger run
echo "List of accounts"
#!/usr/bin/env -S hledger run -f some.journal
echo "List of accounts in some.journal"
accounts
echo "Assets"
echo "Assets of some.journal"
balance assets --depth 2
echo "Liabilities from /some/other.journal"

View File

@ -1,7 +1,7 @@
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.
@ -11,25 +11,23 @@ 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.
"run" has two ways of invocation:
- 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: - 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 - run is a valid
command to give use as well, so you can have run call run if you want
to.
Syntax of the command is intentionally simple: - 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 - 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 - 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.
@ -49,19 +47,11 @@ Caveats:
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
input for that particular command. All the input files would be
cached, and would be read only once.
Examples:
To start the REPL:
hledger run
or
hledger run -f some.journal
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
@ -72,11 +62,11 @@ and finally will run stats on some.journal
To provide commands in the file, as a runnable scripts:
#!/usr/bin/env -S hledger run
echo "List of accounts"
#!/usr/bin/env -S hledger run -f some.journal
echo "List of accounts in some.journal"
accounts
echo "Assets"
echo "Assets of some.journal"
balance assets --depth 2
echo "Liabilities from /some/other.journal"