From f3c90a1351f7b15042ee029f76d6ee98ebf1f87a Mon Sep 17 00:00:00 2001 From: Simon Michael Date: Sun, 27 Apr 2014 18:48:35 -0700 Subject: [PATCH] addons: simplify and firm up add-on command parsing Drop the special case where we hide an add-on's source version if a compiled version is also present. Better to be simple and explicit. Improve robustness of command parsing, eg "hledger addon.hs" will now work even though the command is listed as "addon". And ignore any add-ons which would shadow a built-in command (or any of the official command aliases displayed in the command list, like "bal" and "reg"). Built-ins may not be replaced by an add-on. --- hledger/Hledger/Cli/Main.hs | 38 +++++++++++--------- hledger/Hledger/Cli/Options.hs | 65 +++++++++++++++++++++------------- 2 files changed, 62 insertions(+), 41 deletions(-) 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