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.
This commit is contained in:
Simon Michael 2014-04-27 18:48:35 -07:00
parent 791f6fdd15
commit f3c90a1351
2 changed files with 62 additions and 41 deletions

View File

@ -45,6 +45,7 @@ import Safe
import System.Console.CmdArgs.Explicit as C import System.Console.CmdArgs.Explicit as C
import System.Environment import System.Environment
import System.Exit import System.Exit
import System.FilePath
import System.Process import System.Process
import Text.Printf import Text.Printf
@ -64,7 +65,7 @@ import Hledger.Cli.Tests
import Hledger.Cli.Utils import Hledger.Cli.Utils
import Hledger.Cli.Version import Hledger.Cli.Version
import Hledger.Data.Dates (getCurrentDay) 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.Reports.ReportOptions (dateSpanFromOpts, intervalFromOpts, queryFromOpts)
import Hledger.Utils 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 -- | Parse hledger CLI options from these command line arguments and
-- add-on command names, or raise any error. -- add-on command names, or raise any error.
argsToCliOpts :: [String] -> [String] -> IO CliOpts argsToCliOpts :: [String] -> [String] -> IO CliOpts
@ -198,30 +209,25 @@ main = do
dbgM "raw args before command" argsbeforecmd dbgM "raw args before command" argsbeforecmd
dbgM "raw args after command" argsaftercmd dbgM "raw args after command" argsaftercmd
-- search PATH for add-ons -- Search PATH for add-ons, excluding any that match built-in names.
-- XXX -- The precise addon names (including file extension) are used for command
-- disallow addons matching builtin commands ? -- parsing, and the display names are used for displaying the commands list.
-- $ hledger print (addonPreciseNames', addonDisplayNames') <- hledgerAddons
-- hledger: Ambiguous mode 'print', could be any of: print print let addonPreciseNames = filter (not . (`elem` builtinCommandNames) . dropExtension) addonPreciseNames'
-- allow invocation with extension even when it's hidden ? let addonDisplayNames = filter (not . (`elem` builtinCommandNames)) addonDisplayNames'
-- $ hledger print.hs
-- hledger: command print.hs is not recognized, run with no command to see a list
addons <- getHledgerAddonCommands
-- parse arguments with cmdargs -- parse arguments with cmdargs
opts <- argsToCliOpts args addons opts <- argsToCliOpts args addonPreciseNames
-- select an action and run it. -- select an action and run it.
let let
cmd = command_ opts -- the full matched internal or external command name, if any cmd = command_ opts -- the full matched internal or external command name, if any
isInternalCommand = not (null cmd) && not (cmd `elem` addons) -- probably isInternalCommand = cmd `elem` builtinCommandNames -- not (null cmd) && not (cmd `elem` addons)
isExternalCommand = not (null cmd) && cmd `elem` addons -- probably isExternalCommand = not (null cmd) && cmd `elem` addonPreciseNames -- probably
isBadCommand = not (null rawcmd) && null cmd isBadCommand = not (null rawcmd) && null cmd
hasHelp args = any (`elem` args) ["--help","-h","-?"] hasHelp args = any (`elem` args) ["--help","-h","-?"]
hasVersion = ("--version" `elem`) hasVersion = ("--version" `elem`)
mainmode' = mainmode addons generalHelp = putStr $ showModeHelp $ mainmode addonDisplayNames
generalHelp = putStr $ showModeHelp mainmode'
version = putStrLn prognameandversion version = putStrLn prognameandversion
badCommandError = error' ("command "++rawcmd++" is not recognized, run with no command to see a list") >> exitFailure 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 f `orShowHelp` mode = if hasHelp args then putStr (showModeHelp mode) else f

View File

@ -46,7 +46,7 @@ module Hledger.Cli.Options (
formatFromOpts, formatFromOpts,
-- * Other utils -- * Other utils
getHledgerAddonCommands, hledgerAddons,
-- * Tests -- * Tests
tests_Hledger_Cli_Options tests_Hledger_Cli_Options
@ -65,7 +65,7 @@ import System.Console.CmdArgs.Text
import System.Directory import System.Directory
import System.Environment import System.Environment
import System.Exit (exitSuccess) import System.Exit (exitSuccess)
import System.FilePath (takeExtension) import System.FilePath
import Test.HUnit import Test.HUnit
import Text.ParserCombinators.Parsec as P import Text.ParserCombinators.Parsec as P
@ -197,8 +197,8 @@ showModeHelp = (showText defaultWrap :: [Text] -> String) .
-- | Add command aliases to the command's help string. -- | Add command aliases to the command's help string.
withAliases :: String -> [String] -> String withAliases :: String -> [String] -> String
s `withAliases` [] = s
s `withAliases` as = s ++ " (" ++ intercalate ", " as ++ ")" s `withAliases` as = s ++ " (" ++ intercalate ", " as ++ ")"
-- s `withAliases` [] = s
-- s `withAliases` (a:[]) = s ++ " (alias: " ++ a ++ ")" -- s `withAliases` (a:[]) = s ++ " (alias: " ++ a ++ ")"
-- s `withAliases` as = s ++ " (aliases: " ++ intercalate ", " as ++ ")" -- s `withAliases` as = s ++ " (aliases: " ++ intercalate ", " as ++ ")"
@ -395,30 +395,45 @@ widthp = (string "auto" >> return Auto)
-- Other utils -- Other utils
-- | Like getHledgerExesInPath, but convert the filenames to unique add-on names for the commands list. -- | Get the sorted unique precise names and display names of hledger
-- An add-on name is the filename without the "hledger-" prefix, and usually without the file extension. -- add-ons found in the current user's PATH. The precise names are the
-- Exceptions: -- add-on's filename with the "hledger-" prefix removed. The display
-- - when there are multiple filenames differing only by file extension, their extensions are preserved -- names have the file extension removed also, except when it's needed
-- - when there are two variants, one with .[l]hs extension and one with none or .exe, omit the former. -- for disambiguation.
-- --
getHledgerAddonCommands :: IO [String] -- -- Also when there are exactly two similar names, one with the .hs or
getHledgerAddonCommands = do -- -- .lhs extension and the other with the .exe extension or no
exes <- getHledgerExesInPath -- -- extension - presumably source and compiled versions of a haskell
let stripprefix = drop (length progname + 1) -- -- script - we exclude the source version.
addons = map stripprefix exes --
groups = groupBy (\a b -> stripAddonExtension a == stripAddonExtension b) addons -- This function can return add-on names which shadow built-in command
stripext [f] = [stripAddonExtension f] -- names, but hledger will ignore these.
stripext [f,f2] | takeExtension f `elem` ["",".exe"] && takeExtension f2 `elem` [".hs",".lhs"] = [stripAddonExtension f] --
stripext fs = fs hledgerAddons :: IO ([String],[String])
addons' = concatMap stripext groups hledgerAddons = do
return addons' 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 -- | Get the sorted unique filenames of all hledger-* executables in
-- in the current user's PATH (files, named hledger-*, with either no -- the current user's PATH. Currently these are: files in any of the
-- extension (and no periods in the name) or one of the addonExtensions). -- PATH directories, named hledger-*, with either no extension (and no
-- If there is any problem, return the empty list. -- periods in the name) or one of the addonExtensions. Limitations:
getHledgerExesInPath :: IO [String] -- we do not currently check that the file is really a file (not eg a
getHledgerExesInPath = do -- directory) or whether it has execute permission.
hledgerExecutablesInPath :: IO [String]
hledgerExecutablesInPath = do
pathdirs <- regexSplit "[:;]" `fmap` getEnvSafe "PATH" pathdirs <- regexSplit "[:;]" `fmap` getEnvSafe "PATH"
pathfiles <- concat `fmap` mapM getDirectoryContentsSafe pathdirs pathfiles <- concat `fmap` mapM getDirectoryContentsSafe pathdirs
return $ nub $ sort $ filter isHledgerExeName pathfiles return $ nub $ sort $ filter isHledgerExeName pathfiles