{-# LANGUAGE ExistentialQuantification #-} -- | Some pre-built command line parsers. One is a simple command line -- parser that can parse options that take an optional argument, one -- or two arguments, or a variable number of arguments. For sample -- code that uses this parser, see -- "System.Console.MultiArg.SampleParser". -- -- Another parser is provided for multi-mode programs that are similar -- to @git@ or @darcs@. module System.Console.MultiArg.SimpleParser ( -- * Interspersion control Intersperse (Intersperse, StopOptions) -- * The parser , simple , simpleWithHelp -- * Parsing multi-mode command lines , Mode(..) , modes , modesWithHelp ) where import Data.Either (partitionEithers) import qualified System.Console.MultiArg.Combinator as C import qualified System.Console.MultiArg.GetArgs as GetArgs import qualified System.Console.MultiArg.Prim as P import qualified Control.Monad.Exception.Synchronous as Ex import System.Exit (exitFailure, exitSuccess) import qualified System.IO as IO import Control.Applicative ( many, (<|>), optional, (<$), (<*>), (<*), (<$>)) import Data.List (find) import Data.Maybe (catMaybes, fromJust) import qualified Data.Set as Set -- | What to do after encountering the first non-option, -- non-option-argument word on the command line? In either case, no -- more options are parsed after a stopper. data Intersperse = Intersperse -- ^ Additional options are allowed on the command line after -- encountering the first positional argument. For example, if @a@ -- and @b@ are options, in the command line @-a posarg -b@, @b@ will -- be parsed as an option. If @b@ is /not/ an option and the same -- command line is entered, then @-b@ will result in an error -- because @-b@ starts with a hyphen and therefore \"looks like\" an -- option. | StopOptions -- ^ No additional options will be parsed after encountering the -- first positional argument. For example, if @a@ and @b@ are -- options, in the command line @-a posarg -b@, @b@ will be parsed -- as a positional argument rather than as an option. -- | Parse a command line. simple :: Intersperse -- ^ What to do after encountering the first positional argument -> [C.OptSpec a] -- ^ All possible options -> (String -> a) -- ^ How to handle positional arguments. This function is applied to -- the appropriate string every time the parser encounters a -- positional argument. -> [String] -- ^ The command line to parse. This function correctly handles -- Unicode strings; however, because 'System.Environment.getArgs' -- does not always correctly handle Unicode strings, consult the -- documentation in 'System.Console.MultiArg.GetArgs' and consider -- using the functions in there if there is any chance that you will -- be parsing command lines that have non-ASCII strings. -> Ex.Exceptional P.Error [a] simple i os p as = let optParser = C.parseOption os parser = case i of Intersperse -> parseIntersperse optParser p StopOptions -> parseStopOpts optParser p in P.parse as parser parseOptsNoIntersperse :: P.Parser a -> P.Parser [a] parseOptsNoIntersperse p = P.manyTill p e where e = P.end <|> nonOpt nonOpt = P.lookAhead next next = (() <$ P.nonOptionPosArg) <|> P.stopper parseStopOpts :: P.Parser a -> (String -> a) -> P.Parser [a] parseStopOpts optParser p = (\opts args -> opts ++ map p args) <$> parseOptsNoIntersperse optParser <* optional P.stopper <*> many P.nextWord -- | @parseIntersperse o p@ parses options and positional arguments, -- where o is a parser that parses options, and p is a function that, -- when applied to a string, returns the appropriate type. parseIntersperse :: P.Parser a -> (String -> a) -> P.Parser [a] parseIntersperse optParser p = let pa = (Just . p) <$> P.nonOptionPosArg po = Just <$> optParser ps = Nothing <$ P.stopper parser = po <|> ps <|> pa in catMaybes <$> P.manyTill parser P.end -- -- simpleWithHelp -- -- | Wraps the OptSpec passed into to simpleWithHelp. data ArgWrap a = HelpArg | OtherArg a data ArgWrapResult a = NeedsHelp String | NoHelp a instance Functor ArgWrap where fmap _ HelpArg = HelpArg fmap f (OtherArg a) = OtherArg $ f a partitionHelp :: [ArgWrap a] -> (DoHelp, [a]) partitionHelp xs = (not . null . fst $ r, snd r) where toEither h = case h of HelpArg -> Left () OtherArg a -> Right a r = partitionEithers . map toEither $ xs -- | Shows help and exits successfully if that was requested; -- otherwise, returns the parsed command line options. extractOpts :: String -> (String -> String) -> [ArgWrap a] -> IO [a] extractOpts pn hlp opts = let (doHelp, os) = partitionHelp opts in if doHelp then putStr (hlp pn) >> exitSuccess else return os -- | Parses a simple command line (that is, one without modes) in the -- IO monad. Gets the arguments for you using 'getArgs'. In addition -- to the arguments you provide for 'simple', you also provide online -- help. This function adds @-h@ and @--help@ options and shows help -- if the user entered one of these options anywhere on the command -- line. If help is shown, the program exits successfully. In -- addition, it will print a message to standard error if parsing the -- command line fails and then exit unsuccessfully. simpleWithHelp :: (String -> String) -- ^ Help message. Printed as is, so it can be one line or have many -- lines. It should however have a final end-of-line character. The -- function is applied to the name of the program (which is -- retrieved at runtime.) -> Intersperse -- ^ What to do after encountering the first positional argument -> [C.OptSpec a] -- ^ All possible options. Do not add a @-h@ or @--help@ option; -- these are added for you. -> (String -> a) -- ^ How to handle positional arguments. This function is applied to -- the appropriate string every time the parser encounters a -- positional argument. -> IO [a] -- ^ If help is requested, the program will print it and exit -- successfully. If there was an error parsing the command line, the -- program will print an error message and exit -- unsuccessfully. Otherwise, the parsed arguments are returned. simpleWithHelp h i os p = do let os' = addHelpOpt os as <- GetArgs.getArgs pn <- GetArgs.getProgName let exResult = simple i os' (fmap OtherArg p) as rs <- case exResult of Ex.Exception e -> do IO.hPutStr IO.stderr (C.formatError pn e) enterForHelp pn exitFailure Ex.Success g -> return g extractOpts pn h rs -- | Display a simple enter-h-for-help message. enterForHelp :: String -- ^ Program name -> IO () enterForHelp pn = let s = "\nEnter \"" ++ pn ++ " -h\" for help.\n" in IO.hPutStr IO.stderr s -- -- Mode parsing -- -- | Provides information on each mode that you wish to parse. data Mode result = forall b. Mode { mName :: String -- ^ How the user identifies the mode on the command line. For -- example, with @git@ this would be @commit@, @pull@, etc. , mIntersperse :: Intersperse -- ^ Each mode may have options and positional arguments; may -- these be interspersed? , mOpts :: [C.OptSpec b] -- ^ Options for this mode , mPosArgs :: String -> b -- ^ How to parse positional arguments for this mode , mProcess :: [b] -> result -- ^ Processes the options after they have been parsed. , mHelp :: String -> String -- ^ Help string for this mode. This is used only in -- 'modesWithHelp'; 'modes' ignores this. This is displayed on -- screen exactly as is, so be sure to include the necessary -- trailing newline. The function is applied to the name of the -- program (which is retrieved at runtime.) } instance Functor Mode where fmap f (Mode nm i os pa p h) = Mode nm i os pa (f . p) h type DoHelp = Bool modesWithHelpPure :: String -- ^ Program name -> (String -> String) -- ^ Global help -> [C.OptSpec a] -> ([a] -> Either ([String] -> result) [Mode result]) -> [String] -> Ex.Exceptional P.Error (ArgWrapResult (ArgWrapResult result)) modesWithHelpPure pn getHlp globals lsToEi ss = P.parse ss $ do let globals' = addHelpOpt globals gs <- P.manyTill (C.parseOption globals') endOrNonOpt let (needsHelp, gblOs) = partitionHelp gs if needsHelp then return (NeedsHelp (getHlp pn)) else NoHelp <$> case lsToEi gblOs of Left parsePosArgs -> (NoHelp . parsePosArgs) <$> many P.nextWord <* P.end Right mds -> do let modeWords = Set.fromList . map mName $ mds (_, w) <- P.matchApproxWord modeWords let cmd = fromJust . find (\c -> mName c == w) $ mds processModeWithHelp pn cmd modesNoHelp :: [C.OptSpec a] -> ([a] -> Either ([String] -> result) [Mode result]) -> [String] -> Ex.Exceptional P.Error result modesNoHelp globals lsToEi ss = P.parse ss $ do gs <- P.manyTill (C.parseOption globals) endOrNonOpt case lsToEi gs of Left parsePosArgs -> parsePosArgs <$> many P.nextWord <* P.end Right mds -> do let modeWords = Set.fromList . map mName $ mds (_, w) <- P.matchApproxWord modeWords let cmd = fromJust . find (\c -> mName c == w) $ mds processModeNoHelp cmd processModeWithHelp :: String -- ^ Program name -> Mode result -> P.Parser (ArgWrapResult result) processModeWithHelp pn (Mode _ i os pa p h) = do let prsr = case i of Intersperse -> parseIntersperse StopOptions -> parseStopOpts os' = addHelpOpt os rs <- prsr (C.parseOption os') (fmap OtherArg pa) <* P.end let (needsHelp, parsedOpts) = partitionHelp rs return $ if needsHelp then NeedsHelp (h pn) else NoHelp $ p parsedOpts processModeNoHelp :: Mode result -> P.Parser result processModeNoHelp (Mode _ i os pa p _) = do let prsr = case i of Intersperse -> parseIntersperse StopOptions -> parseStopOpts rs <- prsr (C.parseOption os) pa <* P.end return $ p rs addHelpOpt :: [C.OptSpec a] -> [C.OptSpec (ArgWrap a)] addHelpOpt os = let helpOpt = C.OptSpec ["help"] "h" (C.NoArg HelpArg) origOpts = fmap (fmap OtherArg) os in helpOpt : origOpts -- | Parses a command line that may feature options followed by a -- mode followed by more options and then followed by positional -- arguments. modes :: [C.OptSpec a] -- ^ Global options. These come after the program name but before -- the mode name. -> ([a] -> Either ([String] -> result) [Mode result]) -- ^ This function will be applied to the result of parsing the -- global options. The function must return a @Left@ if you do not -- want to parse any modes at all. This can be useful if one of the -- global options was something like @--help@ or @--version@ and so -- you do not need to see any mode. The function returned in the -- @Left@ will be applied to a list of all remaining command-line -- arguments after the global options. -> [String] -- ^ The command line to parse (presumably from 'getArgs') -> Ex.Exceptional P.Error result -- ^ Returns an Exception if an error was encountered when parsing -- any part of the command line (either the global options or the -- mode.) Otherwise, returns the result. modes = modesNoHelp -- | Like 'modes', but runs in the IO monad. Gets the command line -- arguments for you. This function adds the options @-h@ and -- @--help@, both in the global options and in the options for each -- mode. If @-h@ or @--help@ is entered in the global options, the -- global help is shown and the program exits successfully; similarly, -- if help is requested for a particular mode, that mode's help is -- shown and the program exits successfully. -- -- If an error occurs in the processing of the command line, an error -- message is printed and the program exits with a failure. modesWithHelp :: (String -> String) -- ^ Global help. This is a function that, when applied to the name -- of the program (which is retrieved at runtime), returns a help -- string. This is output exactly as is, so include any necessary -- trailing newlines. -> [C.OptSpec a] -- ^ Global options. These come after the program name but before -- the mode name. Do not add options for @-h@ or @--help@; these are -- added automatically. -> ([a] -> Either ([String] -> result) [Mode result]) -- ^ This function will be applied to the result of parsing the -- global options. The function must return a @Left@ if you do not -- want to parse any modes at all. This can be useful if one of the -- global options was something like @--version@ and so -- you do not need to see any mode. The function returned in the -- @Left@ will be applied to a list of all remaining command-line -- arguments after the global options. -> IO result modesWithHelp hlp glbls lsToEi = do as <- GetArgs.getArgs pn <- GetArgs.getProgName case modesWithHelpPure pn hlp glbls lsToEi as of Ex.Exception e -> do IO.hPutStr IO.stderr $ C.formatError pn e enterForHelp pn exitFailure Ex.Success g -> case g of NeedsHelp h -> putStr h >> exitSuccess NoHelp g2 -> case g2 of NeedsHelp h -> putStr h >> exitSuccess NoHelp g3 -> return g3 -- | Looks at the next word. Succeeds if it is a non-option, or if we -- are at the end of input. Fails otherwise. endOrNonOpt :: P.Parser () endOrNonOpt = (P.lookAhead P.nonOptionPosArg >> return ()) <|> P.end