diff --git a/doc/FILES.md b/doc/FILES.md index 7879c00c4..039d611a0 100644 --- a/doc/FILES.md +++ b/doc/FILES.md @@ -449,6 +449,7 @@ src/hledger/ README.md Register.md Rewrite.md + Repl.md Roi.md Run.md Stats.md diff --git a/hledger/Hledger/Cli.hs b/hledger/Hledger/Cli.hs index 314558c0a..ed25d3525 100644 --- a/hledger/Hledger/Cli.hs +++ b/hledger/Hledger/Cli.hs @@ -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 diff --git a/hledger/Hledger/Cli/Commands.hs b/hledger/Hledger/Cli/Commands.hs index 485f24b2d..aec2a74fb 100644 --- a/hledger/Hledger/Cli/Commands.hs +++ b/hledger/Hledger/Cli/Commands.hs @@ -136,7 +136,8 @@ builtinCommands = [ ,(registermode , register) ,(rewritemode , rewrite) ,(roimode , roi) - ,(runmode , run') + ,(runmode , runOrReplStub) + ,(replmode , runOrReplStub) ,(statsmode , stats) ,(tagsmode , tags) ,(testmode , testcmd) diff --git a/hledger/Hledger/Cli/Commands/Repl.md b/hledger/Hledger/Cli/Commands/Repl.md new file mode 100644 index 000000000..614cb8d56 --- /dev/null +++ b/hledger/Hledger/Cli/Commands/Repl.md @@ -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 ` 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 +``` diff --git a/hledger/Hledger/Cli/Commands/Repl.txt b/hledger/Hledger/Cli/Commands/Repl.txt new file mode 100644 index 000000000..7f36e5b8c --- /dev/null +++ b/hledger/Hledger/Cli/Commands/Repl.txt @@ -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 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 diff --git a/hledger/Hledger/Cli/Commands/Run.hs b/hledger/Hledger/Cli/Commands/Run.hs index 8e2b20bdc..2aaf19f76 100644 --- a/hledger/Hledger/Cli/Commands/Run.hs +++ b/hledger/Hledger/Cli/Commands/Run.hs @@ -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 diff --git a/hledger/Hledger/Cli/Commands/Run.md b/hledger/Hledger/Cli/Commands/Run.md index f61f7d488..6a73170ba 100644 --- a/hledger/Hledger/Cli/Commands/Run.md +++ b/hledger/Hledger/Cli/Commands/Run.md @@ -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 ` 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" diff --git a/hledger/Hledger/Cli/Commands/Run.txt b/hledger/Hledger/Cli/Commands/Run.txt index c491cdae3..d92f481ae 100644 --- a/hledger/Hledger/Cli/Commands/Run.txt +++ b/hledger/Hledger/Cli/Commands/Run.txt @@ -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 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 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"