cabal-dev-0.8: src/Distribution/Dev/InterrogateCabalInstall.hs
{-|
Support for determining the interface of cabal-install via its --help
output.
-}
module Distribution.Dev.InterrogateCabalInstall
( CabalCommandStr
, ccStr
, parseCabalHelp
, Option(..)
, OptionName(..)
, ArgType(..)
, optParseFlags
, getCabalCommandHelp
, getCabalHelp
, getCabalCommands
, Program
, progStr
, getCabalProgs
)
where
import Control.Applicative ( (<$>) )
import Data.Char ( isSpace, isAsciiUpper, isAsciiLower, ord, isLetter )
import Data.List ( isPrefixOf, sort )
import Control.Monad ( guard )
import Data.Maybe ( mapMaybe )
import Distribution.Simple.Utils ( rawSystemStdout )
import Distribution.Verbosity ( verbose )
-- |A cabal-install command name
newtype CabalCommandStr = CabalCommandStr { ccStr :: String }
newtype Program = Program { progStr :: String } deriving (Show, Eq)
-- |Get the command names from a String containing the output of
-- cabal-install --help
parseCabalHelp :: String -> [CabalCommandStr]
parseCabalHelp = map (CabalCommandStr . extractName) .
takeCommands .
dropTillCommands .
lines
where
extractName = takeWhile (not . isSpace) . dropWhile isSpace
takeCommands = takeWhile (not . all isSpace)
dropTillCommands = drop 1 .
dropWhile (not . ("Commands:" `isPrefixOf`))
-- |The kinds of options supported by cabal-install
--
-- XXX: this should also record whether an argument is required.
--
-- XXX: this should also parse short options
data OptionName
= LongOption String
| Short Char
deriving (Eq, Show)
progBeforeOpt :: [Program] -> String -> [OptionName]
progBeforeOpt progs s = map (\p -> LongOption $ progStr p ++ '-':s) progs
progAfterOpt :: [Program] -> String -> [OptionName]
progAfterOpt progs s = map (LongOption . (s ++) . ('-':) . progStr) progs
data Option = Option OptionName ArgType deriving (Show, Eq)
data ArgType = Req | Opt | NoArg deriving (Eq, Show)
parseProgs :: String -> [Program]
parseProgs = map Program . words . concat . takeWhile (not . all isSpace) .
drop 1 . dropWhile (not . isProgIntro) . lines
where
isProgIntro s = (reverse "can be used with the following programs:")
`isPrefixOf` (dropWhile isSpace $ reverse s)
-- |Parse the output of 'cabal foo --help' to determine the valid
-- options for 'cabal foo'
--
-- Note that the --config-file flag is never documented.
optParseFlags :: [Program] -> String -> [Option]
optParseFlags progs = extractLongOptions . findOptionLines . lines
where
findOptionLines = takeWhile (not . all isSpace) .
drop 1 .
dropWhile (not . ("Flags for " `isPrefixOf`))
leftmostDoubleDash = take 1 . sort . mapMaybe (findDoubleDash 0)
extractLongOptions ls = do
i <- leftmostDoubleDash ls
guard $ checkLoc i ls
(soptStr, (' ':l@('-':'-':_))) <- splitAt i <$> ls
let (doubleOpts, tys) = unzip $ parseDoubleOpts l
ty = case tys of
(t:_) -> t
[] -> NoArg
map (\n -> Option n ty) $ parseSingleOpts soptStr ++ doubleOpts
-- Check that the spot that we picked to split is either the
-- start of a long option description or whitespace
checkLoc i = all (`elem` [" --", " "]) . map (take 3 . drop i)
findDoubleDash n (' ':'-':'-':_) = Just n
findDoubleDash _ [] = Nothing
findDoubleDash n (_:xs) = let n' = n + 1
in n' `seq` findDoubleDash n' xs
parseDoubleOpts ('-':'-':xs) = do
(optName, rest) <- plainOpt xs ++ progBefore xs ++ progAfter xs
let (eoc, ty) =
case take 2 rest of
['=',_] -> (dropWhile isLetter $ drop 1 rest, Req)
"[=" -> (drop 1 $ dropWhile isLetter $ drop 2 rest, Opt)
_ -> (rest, NoArg)
opt = (optName, ty)
case eoc of
(',':' ':rest') -> opt:parseDoubleOpts rest'
(' ':_) -> [opt]
[] -> [opt]
_ -> []
parseDoubleOpts _ = []
parseSingleOpts s =
case dropWhile isSpace s of
('-':c:rest)
| isAsciiLower c || isAsciiUpper c ->
case rest of
(',':' ':s') -> Short c:parseSingleOpts s'
[] -> [Short c]
(' ':_) -> [Short c]
_ -> []
_ -> []
optChar c = ord c < 128 && (isAsciiLower c || c == '-')
plainOpt s = let (c, rest) = break (not . optChar) s
in do guard $ not $ null c
return (LongOption c, rest)
progBefore s = case break (== '-') s of
("PROG", '-':rest) ->
do (LongOption n, rest') <- plainOpt rest
o <- progBeforeOpt progs n
return (o, rest')
_ -> []
progAfter s = do (LongOption n, rest) <- plainOpt s
guard $ take 1 (reverse n) == "-"
case break (not . isAsciiUpper) rest of
("PROG", rest') -> do o <- progAfterOpt progs $ init n
return (o, rest')
_ -> []
-- |Obtain the --help output for a particular cabal-install command
getCabalCommandHelp :: CabalCommandStr -> IO String
getCabalCommandHelp c = rawSystemStdout verbose "cabal" [ccStr c, "--help"]
-- |Obtain the top-level --help output for cabal-install
getCabalHelp :: IO String
getCabalHelp = rawSystemStdout verbose "cabal" ["--help"]
-- |Invoke cabal-install in order to determine what commands it
-- supports.
getCabalCommands :: IO [CabalCommandStr]
getCabalCommands = parseCabalHelp <$> getCabalHelp
getCabalProgs :: IO [Program]
getCabalProgs = parseProgs <$> getCabalCommandHelp (CabalCommandStr "install")