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:
		
							parent
							
								
									791f6fdd15
								
							
						
					
					
						commit
						f3c90a1351
					
				| @ -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 | ||||
|  | ||||
| @ -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 | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user