diff --git a/hledger/Hledger/Cli/Commands/Run.hs b/hledger/Hledger/Cli/Commands/Run.hs index 000f4f772..b0864d23e 100644 --- a/hledger/Hledger/Cli/Commands/Run.hs +++ b/hledger/Hledger/Cli/Commands/Run.hs @@ -46,16 +46,22 @@ runmode = hledgerCommandMode ) cligeneralflagsgroups1 hiddenflags - ([], Just $ argsFlag "[COMMANDS_FILE1 COMMANDS_FILE2 ...]") + ([], Just $ argsFlag "[COMMANDS_FILE1 COMMANDS_FILE2 ...] OR [command1 args... -- command2 args... -- command3 args...]") --- | The fake run command introduced to break circular dependency +-- | The fake run 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 () -- | The actual run command. run :: (String -> Maybe (Mode RawOpts, CliOpts -> Journal -> IO ())) -> CliOpts -> Journal -> IO () run findBuiltinCommand CliOpts{rawopts_=rawopts} j = do - -- Add current journal to cache + -- Add current journal to cache, so that any commands that dont specify `-f` could fetch it from there addJournalToCache j defaultJournalKey let args = dbg1 "args" $ listofstringopt "args" rawopts case args of @@ -68,6 +74,7 @@ run findBuiltinCommand CliOpts{rawopts_=rawopts} j = do True -> runFromFiles findBuiltinCommand args False -> runFromArgs findBuiltinCommand args +-- | Run commands from files given to "run". runFromFiles :: (String -> Maybe (Mode RawOpts, CliOpts -> Journal -> IO ())) -> [String] -> IO () runFromFiles findBuiltinCommand inputfiles = do dbg1IO "inputfiles" inputfiles @@ -78,6 +85,7 @@ runFromFiles findBuiltinCommand inputfiles = do forM_ commands (runCommand findBuiltinCommand . parseCommand) +-- | Run commands from command line arguments given to "run". runFromArgs :: (String -> Maybe (Mode RawOpts, CliOpts -> Journal -> IO ())) -> [String] -> IO () runFromArgs findBuiltinCommand args = do -- read commands from all the inputfiles @@ -91,26 +99,27 @@ parseCommand line = -- # begins a comment, ignore everything after # takeWhile (not. ((Just '#')==) . headMay) $ words' (strip line) +-- | Take a single command line (from file, or REPL, or "--"-surrounded block of the args), and run it. runCommand :: (String -> Maybe (Mode RawOpts, CliOpts -> Journal -> IO ())) -> [String] -> IO () runCommand findBuiltinCommand cmdline = do dbg1IO "runCommand for" 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 - -- Allow "run" to call "run" - let cmdaction' = if cmdname == "run" then run findBuiltinCommand else cmdaction - -- Normally expandArgsAt is done by the Cli.hs, but it stops at the first '--', so we need - -- to do it here as well to make sure that each command can use @ARGFILEs - args' <- expandArgsAt args - dbg1IO "runCommand final args" (cmdname,args') - opts <- getHledgerCliOpts' cmdmode args' - withJournalCached opts (cmdaction' opts) + 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' <- expandArgsAt args + dbg1IO "runCommand final args" (cmdname,args') + opts <- getHledgerCliOpts' cmdmode args' + withJournalCached opts (cmdaction' opts) [] -> return () +-- | Run an interactive REPL. runREPL :: (String -> Maybe (Mode RawOpts, CliOpts -> Journal -> IO ())) -> IO () runREPL findBuiltinCommand = do putStrLn "Enter hledger commands, or 'quit' for help." @@ -127,13 +136,15 @@ runREPL findBuiltinCommand = do liftIO $ runCommand findBuiltinCommand $ parseCommand input loop -{-# NOINLINE journalCache #-} +-- | Cache of all journals that have been read by commands given to "run", +-- keyed by the fully-expanded filename. journalCache :: MVar (Map.Map String Journal) journalCache = unsafePerformIO $ newMVar Map.empty +{-# NOINLINE journalCache #-} --- | Key used to cache the journal given in the arguments to 'run'. +-- | Key used to cache the journal given in the arguments to 'run' itself. defaultJournalKey :: String -defaultJournalKey = "journal specified in args of run" +defaultJournalKey = "journal specified in the args of run" addJournalToCache :: Journal -> String -> IO () addJournalToCache j key = modifyMVar_ journalCache $ \cache -> @@ -141,15 +152,20 @@ addJournalToCache j key = modifyMVar_ journalCache $ \cache -> Just _ -> return cache Nothing -> return $ Map.insert key j cache +-- | Similar to `withJournal`, but uses caches all the journals it reads. withJournalCached :: CliOpts -> (Journal -> IO ()) -> IO () withJournalCached cliopts cmd = do mbjournalpaths <- journalFilePathFromOptsNoDefault cliopts let journalpaths = case mbjournalpaths of - Nothing -> NE.fromList [defaultJournalKey] + Nothing -> + -- If the command does not have -f args, it will use the default journal + -- that was supplied to the "run" itself + NE.fromList [defaultJournalKey] Just paths -> paths j <- journalTransform cliopts . sconcat <$> mapM (readAndCacheJournalFile (inputopts_ cliopts)) journalpaths cmd j where + -- | Read a journal file, caching it if it has not been read before. readAndCacheJournalFile :: InputOpts -> PrefixedFilePath -> IO Journal readAndCacheJournalFile iopts "-" = do dbg1IO "readAndCacheJournalFile using stdin, not cached" "-"