module System.Console.Docopt.UsageParse
where
import qualified Data.Map as M
import Data.Ord (comparing)
import GHC.Exts (Down(..))
import Data.List (nub, sortBy, maximumBy, dropWhile, dropWhileEnd)
import System.Console.Docopt.ParseUtils
import System.Console.Docopt.Types
flatten :: Pattern a -> Pattern a
flatten (Sequence (x:[])) = x
flatten (OneOf (x:[])) = x
flatten (Unordered (x:[])) = x
flatten x = x
flatSequence :: [Pattern a] -> Pattern a
flatSequence = flatten . Sequence
flatOneOf :: [Pattern a] -> Pattern a
flatOneOf = flatten . OneOf
trimEmptyLines :: String -> String
trimEmptyLines s = trimmed s ++ "\n"
where
isNewline = (== '\n')
trimmed = dropWhile isNewline . dropWhileEnd isNewline
pLine :: CharParser OptInfoMap OptPattern
pLine = flatten . OneOf <$> pSeq `sepBy1` (inlineSpaces >> pipe)
where pSeq = Sequence <$> (pExp `sepEndBy` inlineSpaces1)
pExpSeq :: CharParser OptInfoMap OptPattern
pExpSeq = flatten . Sequence <$> (pExp `sepEndBy1` inlineSpaces1)
pOptGroup :: CharParser OptInfoMap [OptPattern]
pOptGroup = pGroup '[' pExpSeq ']'
pReqGroup :: CharParser OptInfoMap [OptPattern]
pReqGroup = pGroup '(' pExpSeq ')'
saveOptionsExpectVal :: (a -> Option) -> [(a, Bool)] -> CharParser OptInfoMap ()
saveOptionsExpectVal t pairs = updateState $ \st -> foldl save st pairs
where save infomap (name, optExpectsVal) = M.alter alterFn opt infomap
where opt = t name
alterFn oldval = Just $ case oldval of
Just oldinfo -> oldinfo {expectsVal = optExpectsVal || expectsVal oldinfo}
Nothing -> (fromSynList [opt]) {expectsVal = optExpectsVal}
pShortOption :: CharParser OptInfoMap (Char, Bool)
pShortOption = try $ do char '-'
ch <- letter
expectsVal <- pOptionArgument
return (ch, expectsVal)
pStackedShortOption :: CharParser OptInfoMap OptPattern
pStackedShortOption = try $ do
char '-'
chars <- many1 letter
lastExpectsVal <- pOptionArgument
let (firstChars, lastChar) = (init chars, last chars)
firstPairs = map (\x -> (x,False)) firstChars
lastPair = (lastChar, lastExpectsVal)
saveOptionsExpectVal ShortOption (firstPairs ++ [lastPair])
case length chars of
0 -> fail ""
1 -> return $ Atom . ShortOption $ head chars
_ -> return $ Unordered $ map (Atom . ShortOption) chars
pLongOption :: CharParser OptInfoMap (Name, Bool)
pLongOption = try $ do
string "--"
name <- many1 $ oneOf alphanumerics
expectsVal <- pOptionArgument
return (name, expectsVal)
pAnyOption :: CharParser OptInfoMap String
pAnyOption = try (string "options")
pOptionArgument :: CharParser OptInfoMap Bool
pOptionArgument = option False $ try $ do
try (char '=') <|> try inlineSpace
notFollowedBy (char '-')
try pArgument <|> try (many1 $ oneOf alphanumerics)
return True
pArgument :: CharParser OptInfoMap String
pArgument = try bracketStyle <|> try upperStyle
where bracketStyle = do
open <- char '<'
name <- many $ oneOf alphanumSpecial
close <- char '>'
return name
upperStyle = do
first <- oneOf uppers
rest <- many $ oneOf $ uppers ++ numerics
return $ first:rest
pCommand :: CharParser OptInfoMap String
pCommand = many1 (oneOf alphanumerics)
repeatable :: CharParser OptInfoMap OptPattern -> CharParser OptInfoMap OptPattern
repeatable p = do
expct <- p
tryRepeat <- (try (optional inlineSpace >> ellipsis) >> return Repeated) <|> return id
return (tryRepeat expct)
pExp :: CharParser OptInfoMap OptPattern
pExp = inlineSpaces >> repeatable value
where value = flatOneOf <$> pReqGroup
<|> flatten . Sequence . map Optional <$> try (betweenS "[" "]" pExp)
<|> Optional . flatten . OneOf <$> pOptGroup
<|> return (Atom AnyOption) <* pAnyOption
<|> pStackedShortOption
<|> do (name, expectsVal) <- pLongOption
saveOptionsExpectVal LongOption [(name, expectsVal)]
return $ Atom $ LongOption name
<|> Atom . Argument <$> pArgument
<|> Atom . Command <$> pCommand
pUsageHeader :: CharParser OptInfoMap String
pUsageHeader = caseInsensitive "Usage:"
pUsageLine :: CharParser OptInfoMap OptPattern
pUsageLine =
try $ do
inlineSpaces
many1 (satisfy (not . isSpace))
pLine
pUsagePatterns :: CharParser OptInfoMap OptPattern
pUsagePatterns = do
many (notFollowedBy pUsageHeader >> anyChar)
pUsageHeader
optionalEndline
usageLines <- pUsageLine `sepEndBy` endline
return $ flatten . OneOf $ usageLines
begOptionLine :: CharParser OptInfoMap String
begOptionLine = inlineSpaces >> lookAhead (char '-') >> return "-"
pOptSynonyms :: CharParser OptInfoMap ([Option], Bool)
pOptSynonyms = do inlineSpaces
pairs <- p `sepEndBy1` (optional (char ',') >> inlineSpace)
let options = map fst pairs
expectsVal = any snd pairs
return (options, expectsVal)
where p = (\(c, ev) -> (ShortOption c, ev)) <$> pShortOption
<|> (\(s, ev) -> (LongOption s, ev)) <$> pLongOption
pDefaultTag :: CharParser OptInfoMap String
pDefaultTag = do
caseInsensitive "[default:"
inlineSpaces
def <- many (noneOf "]")
char ']'
return def
pOptDefault :: CharParser OptInfoMap (Maybe String)
pOptDefault = do
skipUntil (pDefaultTag <|> (newline >> begOptionLine))
optionMaybe pDefaultTag
pOptDescription :: CharParser OptInfoMap ()
pOptDescription = try $ do
(syns, expectsVal) <- pOptSynonyms
def <- pOptDefault
skipUntil (newline >> begOptionLine)
updateState $ \infomap ->
let optinfo = (fromSynList syns) {defaultVal = def, expectsVal = expectsVal}
saveOptInfo infomap expct = M.insert expct optinfo infomap
in foldl saveOptInfo infomap syns
return ()
pOptDescriptions :: CharParser OptInfoMap OptInfoMap
pOptDescriptions = do
skipUntil (newline >> begOptionLine)
optional newline
optional $ pOptDescription `sepEndBy` endline
getState
pDocopt :: CharParser OptInfoMap OptFormat
pDocopt = do
optPattern <- pUsagePatterns
optInfoMap <- pOptDescriptions
let optPattern' = eagerSort $ expectSynonyms optInfoMap optPattern
saveCanRepeat pat el minfo = case minfo of
(Just info) -> Just $ info {isRepeated = canRepeat pat el}
(Nothing) -> Just $ (fromSynList []) {isRepeated = canRepeat pat el}
optInfoMap' = alterAllWithKey (saveCanRepeat optPattern') (atoms optPattern') optInfoMap
return (optPattern', optInfoMap')
expectSynonyms :: OptInfoMap -> OptPattern -> OptPattern
expectSynonyms oim (Sequence exs) = Sequence $ map (expectSynonyms oim) exs
expectSynonyms oim (OneOf exs) = OneOf $ map (expectSynonyms oim) exs
expectSynonyms oim (Unordered exs) = Unordered $ map (expectSynonyms oim) exs
expectSynonyms oim (Optional ex) = Optional $ expectSynonyms oim ex
expectSynonyms oim (Repeated ex) = Repeated $ expectSynonyms oim ex
expectSynonyms oim a@(Atom atom) = case atom of
e@(Command ex) -> a
e@(Argument ex) -> a
e@AnyOption -> flatten $ Unordered $ nub $ map Atom $ concatMap synonyms (M.elems oim)
e@(LongOption ex) ->
case synonyms <$> e `M.lookup` oim of
Just syns -> flatten . OneOf $ map Atom syns
Nothing -> a
e@(ShortOption c) ->
case synonyms <$> e `M.lookup` oim of
Just syns -> flatten . OneOf $ map Atom syns
Nothing -> a
canRepeat :: Eq a => Pattern a -> a -> Bool
canRepeat pat target =
case pat of
(Sequence ps) -> canRepeatInside ps || (atomicOccurrences ps > 1)
(OneOf ps) -> canRepeatInside ps
(Unordered ps) -> canRepeatInside ps || (atomicOccurrences ps > 1)
(Optional p) -> canRepeat p target
(Repeated p) -> target `elem` atoms pat
(Atom a) -> False
where canRepeatInside = any (`canRepeat` target)
atomicOccurrences ps = length $ filter (== target) $ atoms $ Sequence ps
compareOptSpecificity :: Option -> Option -> Ordering
compareOptSpecificity optA optB = case optA of
LongOption a -> case optB of
LongOption b -> comparingFirst length a b
_ -> GT
ShortOption a -> case optB of
LongOption b -> LT
ShortOption b -> compare a b
_ -> GT
Command a -> case optB of
LongOption b -> LT
ShortOption b -> LT
Command b -> comparingFirst length a b
_ -> GT
Argument a -> case optB of
AnyOption -> GT
Argument b -> comparingFirst length a b
_ -> LT
AnyOption -> case optB of
AnyOption -> EQ
_ -> LT
where
comparingFirst :: (Ord a, Ord b) => (a -> b) -> a -> a -> Ordering
comparingFirst p a1 a2 =
case compare (p a1) (p a2) of
EQ -> compare a1 a2
o -> o
eagerSort :: OptPattern -> OptPattern
eagerSort pat = case pat of
OneOf ps -> OneOf $ map innerSort ps
a -> innerSort a
where
innerSort ipat = case ipat of
Sequence ps -> Sequence $ map innerSort ps
OneOf ps -> OneOf $ map innerSort
. sortBy (comparing $ Down . maxLength)
. sortBy (comparing representativeAtom)
$ ps
Unordered ps -> Unordered $ map innerSort ps
Optional p -> Optional $ innerSort p
Repeated p -> Repeated $ innerSort p
a@(Atom _) -> a
representativeAtom :: OptPattern -> Option
representativeAtom p = case p of
Sequence ps -> if null ps then AnyOption else representativeAtom $ head ps
OneOf ps -> maximumBy compareOptSpecificity . map representativeAtom $ ps
Unordered ps -> maximumBy compareOptSpecificity . map representativeAtom $ ps
Optional p -> representativeAtom p
Repeated p -> representativeAtom p
Atom a -> a
maxLength :: OptPattern -> Int
maxLength p = case p of
Sequence ps -> sum $ map maxLength ps
OneOf ps -> maximum $ map maxLength ps
Unordered ps -> sum $ map maxLength ps
Optional p -> maxLength p
Repeated p -> maxLength p
Atom a -> case a of
LongOption o -> length o
ShortOption _ -> 1
Command c -> length c
Argument a -> 100
AnyOption -> 0