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.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

View File

@ -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