-- | -- Module : Console.Options.Flags -- License : BSD-style -- Maintainer : Vincent Hanquez -- Stability : experimental -- Portability : Good -- -- Simple flag parsers -- module Console.Options.Flags ( parseFlags , FlagDesc(..) , FlagFragments(..) , Flag , FlagArgValidation(..) , FlagArgDesc(..) , FlagError(..) -- * fragments , FlagFrag(FlagShort, FlagLong, FlagDescription) , flattenFragments ) where import Control.Applicative import Control.Monad import Console.Options.Nid import Data.List import Data.Monoid import Basement.Compat.Semigroup -- | Result of validation of flag value. data FlagArgValidation = FlagArgValid -- ^ Validation success | FlagArgInvalid String -- ^ Validation failed with reason -- | How to parse a specific flag data FlagDesc = FlagDesc { flagFragments :: FlagFragments , flagNid :: Nid -- ^ flag number. internal value , flagArg :: FlagArgDesc -- ^ parser for the argument to an flag , flagArgValidate :: String -> FlagArgValidation -- ^ if the argument doesn't validate, return the error message associated, otherwise Nothing , flagArity :: Int } -- | Fragment of flag definition. -- -- Use the monoid approach to concat flags together -- e.g. -- > FlagShort 'o' <> FlagLong "option" data FlagFrag = FlagShort Char -- ^ short option e.g. '-a' | FlagLong String -- ^ long option e.g. "--aaaa" | FlagDescription String -- ^ description of this flag. -- | FlagDefault String | FlagMany [FlagFrag] deriving (Show,Eq) -- | Flatten fragments list into a final product to consume data FlagFragments = FlagFragments { flagShort :: Maybe Char -- ^ short flag parser 'o' , flagLong :: Maybe String -- ^ long flag "flag" , flagDescription :: Maybe String -- ^ Description of this "flag" --, flagDefault :: Maybe String -- ^ Has a default } -- | Produce the result structure after processing all the fragment list. flattenFragments :: FlagFrag -> FlagFragments flattenFragments frags = foldl' flat startVal $ case frags of FlagMany l -> l _ -> [frags] where startVal = FlagFragments Nothing Nothing Nothing flat ff (FlagShort f) = ff { flagShort = Just f } flat ff (FlagLong f) = ff { flagLong = Just f } flat ff (FlagDescription f) = ff { flagDescription = Just f } flat acc (FlagMany l) = foldl' flat acc l instance Semigroup FlagFrag where (<>) (FlagMany l1) (FlagMany l2) = FlagMany (l1 ++ l2) (<>) (FlagMany l1) o = FlagMany (l1 ++ [o]) (<>) o (FlagMany l2) = FlagMany (o : l2) (<>) o1 o2 = FlagMany [o1,o2] instance Monoid FlagFrag where mempty = FlagMany [] mappend (FlagMany l1) (FlagMany l2) = FlagMany (l1 ++ l2) mappend (FlagMany l1) o = FlagMany (l1 ++ [o]) mappend o (FlagMany l2) = FlagMany (o : l2) mappend o1 o2 = FlagMany [o1,o2] -- | Whether a flag has an argument, an optional one or always an argument data FlagArgDesc = FlagArgNone | FlagArgMaybe | FlagArgHave deriving (Show,Eq) data Matching a = NoMatching | Matching a | MatchingWithArg a String -- | the state of parsing the command line arguments data ParseState a = ParseState [Flag] -- Args : in reverse order [String] -- Unparsed: in reverse order [FlagError] -- errors: in reverse order -- | Flag return value after parsing made of a unique number and an optional value type Flag = (Nid, Maybe String) -- | Flag error with the description of the flag, the index of the element causing the error, and the error itself. data FlagError = FlagError FlagDesc Int String -- | Parse flags parseFlags :: [FlagDesc] -> [String] -> ([Flag], [String], [FlagError]) parseFlags flagParsers = loop (ParseState [] [] []) [1..] where loop :: ParseState a -> [Int] -> [String] -> ([Flag], [String], [FlagError]) loop _ [] _ = error "impossible case" loop (ParseState os us ers) _ [] = (reverse os, reverse us, reverse ers) loop (ParseState os us ers) (i:is) (a:as) = case a of '-':'-':[] -> (reverse os, reverse us ++ as, reverse ers) '-':'-':long -> loop (processFlag (findLong long)) is as '-':short:[] -> loop (processFlag (findShort short)) is as _ -> loop (ParseState os (a:us) ers) is as where processFlag NoMatching = ParseState os (a:us) ers processFlag (Matching opt) = case flagArg opt of FlagArgNone -> ParseState ((flagNid opt, Nothing) : os) us ers FlagArgMaybe -> ParseState ((flagNid opt, Nothing) : os) us ers FlagArgHave -> case as of [] -> let e = mkFlagError opt "required argument missing" in ParseState os (a:us) (e:ers) (x:_) -> case (flagArgValidate opt) x of FlagArgValid -> ParseState ((flagNid opt, Just x):os) us ers FlagArgInvalid optErr -> let e = mkFlagError opt ("invalid argument: " ++ optErr) in ParseState os us (e:ers) processFlag (MatchingWithArg opt arg) = case flagArg opt of FlagArgNone -> let e = mkFlagError opt "invalid argument, expecting no argument" -- fixme: tell which flag in ParseState os (a:us) (e:ers) FlagArgMaybe -> case (flagArgValidate opt) arg of FlagArgValid -> ParseState ((flagNid opt, Just arg):os) us ers FlagArgInvalid optErr -> let e = mkFlagError opt ("invalid argument: " ++ optErr) in ParseState os us (e:ers) FlagArgHave -> case (flagArgValidate opt) arg of FlagArgValid -> ParseState ((flagNid opt, Just arg):os) us ers FlagArgInvalid optErr -> let e = mkFlagError opt ("invalid argument: " ++ optErr) in ParseState os us (e:ers) mkFlagError opt s = FlagError opt i s findShort short = findRetArg (flagShortMatch short) flagParsers findLong long = findRetArg (flagLongMatch long) flagParsers flagShortMatch toMatch opt = maybe NoMatching (\x -> if x == toMatch then Matching opt else NoMatching) $ flagShort $ flagFragments opt flagLongMatch toMatch opt = maybe NoMatching match $ flagLong $ flagFragments opt where match optLong | leftPart == optLong && null rightPart = Matching opt | leftPart == optLong && isPrefixOf "=" rightPart = MatchingWithArg opt (drop 1 rightPart) | otherwise = NoMatching where (leftPart,rightPart) = splitAt (length optLong) toMatch findRetArg _ [] = NoMatching findRetArg f (opt:opts) = case f opt of NoMatching -> findRetArg f opts r -> r