diff --git a/hledger/Hledger/Cli/Main.hs b/hledger/Hledger/Cli/Main.hs index edd3eb5b5..9ad23a811 100644 --- a/hledger/Hledger/Cli/Main.hs +++ b/hledger/Hledger/Cli/Main.hs @@ -45,6 +45,7 @@ import Safe import System.Console.CmdArgs.Explicit as C import System.Environment import System.Exit +import System.FilePath import System.Process import Text.Printf @@ -64,7 +65,7 @@ import Hledger.Cli.Tests import Hledger.Cli.Utils import Hledger.Cli.Version import Hledger.Data.Dates (getCurrentDay) -import Hledger.Data.RawOptions (optserror) +import Hledger.Data.RawOptions (RawOpts, optserror) import Hledger.Reports.ReportOptions (dateSpanFromOpts, intervalFromOpts, queryFromOpts) import Hledger.Utils @@ -125,6 +126,16 @@ oldconvertmode = (defCommandMode ["convert"]) { } } +builtinCommands :: [Mode RawOpts] +builtinCommands = + let gs = modeGroupModes $ mainmode [] + in concatMap snd (groupNamed gs) + ++ groupUnnamed gs + ++ groupHidden gs + +builtinCommandNames :: [String] +builtinCommandNames = concatMap modeNames builtinCommands + -- | Parse hledger CLI options from these command line arguments and -- add-on command names, or raise any error. argsToCliOpts :: [String] -> [String] -> IO CliOpts @@ -198,30 +209,25 @@ main = do dbgM "raw args before command" argsbeforecmd dbgM "raw args after command" argsaftercmd - -- search PATH for add-ons - -- XXX - -- disallow addons matching builtin commands ? - -- $ hledger print - -- hledger: Ambiguous mode 'print', could be any of: print print - -- allow invocation with extension even when it's hidden ? - -- $ hledger print.hs - -- hledger: command print.hs is not recognized, run with no command to see a list - - addons <- getHledgerAddonCommands + -- Search PATH for add-ons, excluding any that match built-in names. + -- The precise addon names (including file extension) are used for command + -- parsing, and the display names are used for displaying the commands list. + (addonPreciseNames', addonDisplayNames') <- hledgerAddons + let addonPreciseNames = filter (not . (`elem` builtinCommandNames) . dropExtension) addonPreciseNames' + let addonDisplayNames = filter (not . (`elem` builtinCommandNames)) addonDisplayNames' -- parse arguments with cmdargs - opts <- argsToCliOpts args addons + opts <- argsToCliOpts args addonPreciseNames -- select an action and run it. let cmd = command_ opts -- the full matched internal or external command name, if any - isInternalCommand = not (null cmd) && not (cmd `elem` addons) -- probably - isExternalCommand = not (null cmd) && cmd `elem` addons -- probably + isInternalCommand = cmd `elem` builtinCommandNames -- not (null cmd) && not (cmd `elem` addons) + isExternalCommand = not (null cmd) && cmd `elem` addonPreciseNames -- probably isBadCommand = not (null rawcmd) && null cmd hasHelp args = any (`elem` args) ["--help","-h","-?"] hasVersion = ("--version" `elem`) - mainmode' = mainmode addons - generalHelp = putStr $ showModeHelp mainmode' + generalHelp = putStr $ showModeHelp $ mainmode addonDisplayNames version = putStrLn prognameandversion badCommandError = error' ("command "++rawcmd++" is not recognized, run with no command to see a list") >> exitFailure f `orShowHelp` mode = if hasHelp args then putStr (showModeHelp mode) else f diff --git a/hledger/Hledger/Cli/Options.hs b/hledger/Hledger/Cli/Options.hs index d7220f655..7201d2691 100644 --- a/hledger/Hledger/Cli/Options.hs +++ b/hledger/Hledger/Cli/Options.hs @@ -46,7 +46,7 @@ module Hledger.Cli.Options ( formatFromOpts, -- * Other utils - getHledgerAddonCommands, + hledgerAddons, -- * Tests tests_Hledger_Cli_Options @@ -65,7 +65,7 @@ import System.Console.CmdArgs.Text import System.Directory import System.Environment import System.Exit (exitSuccess) -import System.FilePath (takeExtension) +import System.FilePath import Test.HUnit import Text.ParserCombinators.Parsec as P @@ -197,8 +197,8 @@ showModeHelp = (showText defaultWrap :: [Text] -> String) . -- | Add command aliases to the command's help string. withAliases :: String -> [String] -> String +s `withAliases` [] = s s `withAliases` as = s ++ " (" ++ intercalate ", " as ++ ")" --- s `withAliases` [] = s -- s `withAliases` (a:[]) = s ++ " (alias: " ++ a ++ ")" -- s `withAliases` as = s ++ " (aliases: " ++ intercalate ", " as ++ ")" @@ -395,30 +395,45 @@ widthp = (string "auto" >> return Auto) -- Other utils --- | Like getHledgerExesInPath, but convert the filenames to unique add-on names for the commands list. --- An add-on name is the filename without the "hledger-" prefix, and usually without the file extension. --- Exceptions: --- - when there are multiple filenames differing only by file extension, their extensions are preserved --- - when there are two variants, one with .[l]hs extension and one with none or .exe, omit the former. +-- | Get the sorted unique precise names and display names of hledger +-- add-ons found in the current user's PATH. The precise names are the +-- add-on's filename with the "hledger-" prefix removed. The display +-- names have the file extension removed also, except when it's needed +-- for disambiguation. -- -getHledgerAddonCommands :: IO [String] -getHledgerAddonCommands = do - exes <- getHledgerExesInPath - let stripprefix = drop (length progname + 1) - addons = map stripprefix exes - groups = groupBy (\a b -> stripAddonExtension a == stripAddonExtension b) addons - stripext [f] = [stripAddonExtension f] - stripext [f,f2] | takeExtension f `elem` ["",".exe"] && takeExtension f2 `elem` [".hs",".lhs"] = [stripAddonExtension f] - stripext fs = fs - addons' = concatMap stripext groups - return addons' +-- -- Also when there are exactly two similar names, one with the .hs or +-- -- .lhs extension and the other with the .exe extension or no +-- -- extension - presumably source and compiled versions of a haskell +-- -- script - we exclude the source version. +-- +-- This function can return add-on names which shadow built-in command +-- names, but hledger will ignore these. +-- +hledgerAddons :: IO ([String],[String]) +hledgerAddons = do + exes <- hledgerExecutablesInPath + let precisenames = -- concatMap dropRedundant $ + -- groupBy (\a b -> dropExtension a == dropExtension b) $ + map stripprefix exes + let displaynames = concatMap stripext $ + groupBy (\a b -> dropExtension a == dropExtension b) $ + precisenames + return (precisenames, displaynames) + where + stripprefix = drop (length progname + 1) + -- dropRedundant [f,f2] | takeExtension f `elem` ["",".exe"] && takeExtension f2 `elem` [".hs",".lhs"] = [f] + -- dropRedundant fs = fs + stripext [f] = [dropExtension f] + stripext fs = fs --- | Get the sorted unique filenames of all hledger executables --- in the current user's PATH (files, named hledger-*, with either no --- extension (and no periods in the name) or one of the addonExtensions). --- If there is any problem, return the empty list. -getHledgerExesInPath :: IO [String] -getHledgerExesInPath = do +-- | Get the sorted unique filenames of all hledger-* executables in +-- the current user's PATH. Currently these are: files in any of the +-- PATH directories, named hledger-*, with either no extension (and no +-- periods in the name) or one of the addonExtensions. Limitations: +-- we do not currently check that the file is really a file (not eg a +-- directory) or whether it has execute permission. +hledgerExecutablesInPath :: IO [String] +hledgerExecutablesInPath = do pathdirs <- regexSplit "[:;]" `fmap` getEnvSafe "PATH" pathfiles <- concat `fmap` mapM getDirectoryContentsSafe pathdirs return $ nub $ sort $ filter isHledgerExeName pathfiles