{-# LANGUAGE ExistentialQuantification #-}
module Distribution.Simple.Command (
CommandUI(..),
commandShowOptions,
CommandParse(..),
commandParseArgs,
getNormalCommandDescriptions,
helpCommandUI,
ShowOrParseArgs(..),
usageDefault,
usageAlternatives,
mkCommandUI,
hiddenCommand,
Command,
commandAddAction,
noExtraFlags,
CommandType(..),
CommandSpec(..),
commandFromSpec,
commandsRun,
OptionField(..), Name,
option, multiOption,
liftOption, viewAsFieldDescr,
OptDescr(..), Description, SFlags, LFlags, OptFlags, ArgPlaceHolder,
MkOptDescr,
reqArg, reqArg', optArg, optArg', noArg,
boolOpt, boolOpt', choiceOpt, choiceOptFromEnum
) where
import qualified Distribution.GetOpt as GetOpt
import Distribution.Text
import Distribution.ParseUtils
import Distribution.ReadE
import Distribution.Simple.Utils
import Control.Monad
import Data.Char (isAlpha, toLower)
import Data.List (sortBy)
import Data.Maybe
import Data.Monoid as Mon
import Text.PrettyPrint ( punctuate, cat, comma, text )
import Text.PrettyPrint as PP ( empty )
data CommandUI flags = CommandUI {
commandName :: String,
commandSynopsis :: String,
commandUsage :: String -> String,
commandDescription :: Maybe (String -> String),
commandNotes :: Maybe (String -> String),
commandDefaultFlags :: flags,
commandOptions :: ShowOrParseArgs -> [OptionField flags]
}
data ShowOrParseArgs = ShowArgs | ParseArgs
type Name = String
type Description = String
data OptionField a = OptionField {
optionName :: Name,
optionDescr :: [OptDescr a] }
data OptDescr a = ReqArg Description OptFlags ArgPlaceHolder
(ReadE (a->a)) (a -> [String])
| OptArg Description OptFlags ArgPlaceHolder
(ReadE (a->a)) (a->a) (a -> [Maybe String])
| ChoiceOpt [(Description, OptFlags, a->a, a -> Bool)]
| BoolOpt Description OptFlags OptFlags
(Bool -> a -> a) (a-> Maybe Bool)
type SFlags = [Char]
type LFlags = [String]
type OptFlags = (SFlags,LFlags)
type ArgPlaceHolder = String
option :: SFlags -> LFlags -> Description -> get -> set -> MkOptDescr get set a
-> OptionField a
option sf lf@(n:_) d get set arg = OptionField n [arg sf lf d get set]
option _ _ _ _ _ _ = error $ "Distribution.command.option: "
++ "An OptionField must have at least one LFlag"
multiOption :: Name -> get -> set
-> [get -> set -> OptDescr a]
-> OptionField a
multiOption n get set args = OptionField n [arg get set | arg <- args]
type MkOptDescr get set a = SFlags -> LFlags -> Description -> get -> set
-> OptDescr a
reqArg :: Monoid b => ArgPlaceHolder -> ReadE b -> (b -> [String])
-> MkOptDescr (a -> b) (b -> a -> a) a
reqArg ad mkflag showflag sf lf d get set =
ReqArg d (sf,lf) ad (fmap (\a b -> set (get b `mappend` a) b) mkflag)
(showflag . get)
optArg :: Monoid b => ArgPlaceHolder -> ReadE b -> b -> (b -> [Maybe String])
-> MkOptDescr (a -> b) (b -> a -> a) a
optArg ad mkflag def showflag sf lf d get set =
OptArg d (sf,lf) ad (fmap (\a b -> set (get b `mappend` a) b) mkflag)
(\b -> set (get b `mappend` def) b)
(showflag . get)
reqArg' :: Monoid b => ArgPlaceHolder -> (String -> b) -> (b -> [String])
-> MkOptDescr (a -> b) (b -> a -> a) a
reqArg' ad mkflag showflag =
reqArg ad (succeedReadE mkflag) showflag
optArg' :: Mon.Monoid b => ArgPlaceHolder -> (Maybe String -> b)
-> (b -> [Maybe String])
-> MkOptDescr (a -> b) (b -> a -> a) a
optArg' ad mkflag showflag =
optArg ad (succeedReadE (mkflag . Just)) def showflag
where def = mkflag Nothing
noArg :: (Eq b) => b -> MkOptDescr (a -> b) (b -> a -> a) a
noArg flag sf lf d = choiceOpt [(flag, (sf,lf), d)] sf lf d
boolOpt :: (b -> Maybe Bool) -> (Bool -> b) -> SFlags -> SFlags
-> MkOptDescr (a -> b) (b -> a -> a) a
boolOpt g s sfT sfF _sf _lf@(n:_) d get set =
BoolOpt d (sfT, ["enable-"++n]) (sfF, ["disable-"++n]) (set.s) (g.get)
boolOpt _ _ _ _ _ _ _ _ _ = error
"Distribution.Simple.Setup.boolOpt: unreachable"
boolOpt' :: (b -> Maybe Bool) -> (Bool -> b) -> OptFlags -> OptFlags
-> MkOptDescr (a -> b) (b -> a -> a) a
boolOpt' g s ffT ffF _sf _lf d get set = BoolOpt d ffT ffF (set.s) (g . get)
choiceOpt :: Eq b => [(b,OptFlags,Description)]
-> MkOptDescr (a -> b) (b -> a -> a) a
choiceOpt aa_ff _sf _lf _d get set = ChoiceOpt alts
where alts = [(d,flags, set alt, (==alt) . get) | (alt,flags,d) <- aa_ff]
choiceOptFromEnum :: (Bounded b, Enum b, Show b, Eq b) =>
MkOptDescr (a -> b) (b -> a -> a) a
choiceOptFromEnum _sf _lf d get =
choiceOpt [ (x, (sf, [map toLower $ show x]), d')
| (x, sf) <- sflags'
, let d' = d ++ show x]
_sf _lf d get
where sflags' = foldl f [] [firstOne..]
f prev x = let prevflags = concatMap snd prev in
prev ++ take 1 [(x, [toLower sf])
| sf <- show x, isAlpha sf
, toLower sf `notElem` prevflags]
firstOne = minBound `asTypeOf` get undefined
commandGetOpts :: ShowOrParseArgs -> CommandUI flags
-> [GetOpt.OptDescr (flags -> flags)]
commandGetOpts showOrParse command =
concatMap viewAsGetOpt (commandOptions command showOrParse)
viewAsGetOpt :: OptionField a -> [GetOpt.OptDescr (a->a)]
viewAsGetOpt (OptionField _n aa) = concatMap optDescrToGetOpt aa
where
optDescrToGetOpt (ReqArg d (cs,ss) arg_desc set _) =
[GetOpt.Option cs ss (GetOpt.ReqArg set' arg_desc) d]
where set' = readEOrFail set
optDescrToGetOpt (OptArg d (cs,ss) arg_desc set def _) =
[GetOpt.Option cs ss (GetOpt.OptArg set' arg_desc) d]
where set' Nothing = def
set' (Just txt) = readEOrFail set txt
optDescrToGetOpt (ChoiceOpt alts) =
[GetOpt.Option sf lf (GetOpt.NoArg set) d | (d,(sf,lf),set,_) <- alts ]
optDescrToGetOpt (BoolOpt d (sfT, lfT) ([], []) set _) =
[ GetOpt.Option sfT lfT (GetOpt.NoArg (set True)) d ]
optDescrToGetOpt (BoolOpt d ([], []) (sfF, lfF) set _) =
[ GetOpt.Option sfF lfF (GetOpt.NoArg (set False)) d ]
optDescrToGetOpt (BoolOpt d (sfT,lfT) (sfF, lfF) set _) =
[ GetOpt.Option sfT lfT (GetOpt.NoArg (set True)) ("Enable " ++ d)
, GetOpt.Option sfF lfF (GetOpt.NoArg (set False)) ("Disable " ++ d) ]
viewAsFieldDescr :: OptionField a -> FieldDescr a
viewAsFieldDescr (OptionField _n []) =
error "Distribution.command.viewAsFieldDescr: unexpected"
viewAsFieldDescr (OptionField n dd) = FieldDescr n get set
where
optDescr = head $ sortBy cmp dd
cmp :: OptDescr a -> OptDescr a -> Ordering
ReqArg{} `cmp` ReqArg{} = EQ
ReqArg{} `cmp` _ = GT
BoolOpt{} `cmp` ReqArg{} = LT
BoolOpt{} `cmp` BoolOpt{} = EQ
BoolOpt{} `cmp` _ = GT
ChoiceOpt{} `cmp` ReqArg{} = LT
ChoiceOpt{} `cmp` BoolOpt{} = LT
ChoiceOpt{} `cmp` ChoiceOpt{} = EQ
ChoiceOpt{} `cmp` _ = GT
OptArg{} `cmp` OptArg{} = EQ
OptArg{} `cmp` _ = LT
get t = case optDescr of
ReqArg _ _ _ _ ppr ->
(cat . punctuate comma . map text . ppr) t
OptArg _ _ _ _ _ ppr ->
case ppr t of [] -> PP.empty
(Nothing : _) -> text "True"
(Just a : _) -> text a
ChoiceOpt alts ->
fromMaybe PP.empty $ listToMaybe
[ text lf | (_,(_,lf:_), _,enabled) <- alts, enabled t]
BoolOpt _ _ _ _ enabled -> (maybe PP.empty disp . enabled) t
set line val a =
case optDescr of
ReqArg _ _ _ readE _ -> ($ a) `liftM` runE line n readE val
ChoiceOpt{} ->
case getChoiceByLongFlag optDescr val of
Just f -> return (f a)
_ -> syntaxError line val
BoolOpt _ _ _ setV _ -> (`setV` a) `liftM` runP line n parse val
OptArg _ _ _ readE _ _ -> ($ a) `liftM` runE line n readE val
getChoiceByLongFlag :: OptDescr b -> String -> Maybe (b->b)
getChoiceByLongFlag (ChoiceOpt alts) val = listToMaybe
[ set | (_,(_sf,lf:_), set, _) <- alts
, lf == val]
getChoiceByLongFlag _ _ =
error "Distribution.command.getChoiceByLongFlag: expected a choice option"
getCurrentChoice :: OptDescr a -> a -> [String]
getCurrentChoice (ChoiceOpt alts) a =
[ lf | (_,(_sf,lf:_), _, currentChoice) <- alts, currentChoice a]
getCurrentChoice _ _ = error "Command.getChoice: expected a Choice OptDescr"
liftOption :: (b -> a) -> (a -> (b -> b)) -> OptionField a -> OptionField b
liftOption get' set' opt =
opt { optionDescr = liftOptDescr get' set' `map` optionDescr opt}
liftOptDescr :: (b -> a) -> (a -> (b -> b)) -> OptDescr a -> OptDescr b
liftOptDescr get' set' (ChoiceOpt opts) =
ChoiceOpt [ (d, ff, liftSet get' set' set , (get . get'))
| (d, ff, set, get) <- opts]
liftOptDescr get' set' (OptArg d ff ad set def get) =
OptArg d ff ad (liftSet get' set' `fmap` set)
(liftSet get' set' def) (get . get')
liftOptDescr get' set' (ReqArg d ff ad set get) =
ReqArg d ff ad (liftSet get' set' `fmap` set) (get . get')
liftOptDescr get' set' (BoolOpt d ffT ffF set get) =
BoolOpt d ffT ffF (liftSet get' set' . set) (get . get')
liftSet :: (b -> a) -> (a -> (b -> b)) -> (a -> a) -> b -> b
liftSet get' set' set x = set' (set $ get' x) x
commandShowOptions :: CommandUI flags -> flags -> [String]
commandShowOptions command v = concat
[ showOptDescr v od | o <- commandOptions command ParseArgs
, od <- optionDescr o]
where
maybePrefix [] = []
maybePrefix (lOpt:_) = ["--" ++ lOpt]
showOptDescr :: a -> OptDescr a -> [String]
showOptDescr x (BoolOpt _ (_,lfTs) (_,lfFs) _ enabled)
= case enabled x of
Nothing -> []
Just True -> maybePrefix lfTs
Just False -> maybePrefix lfFs
showOptDescr x c@ChoiceOpt{}
= ["--" ++ val | val <- getCurrentChoice c x]
showOptDescr x (ReqArg _ (_ssff,lf:_) _ _ showflag)
= [ "--"++lf++"="++flag
| flag <- showflag x ]
showOptDescr x (OptArg _ (_ssff,lf:_) _ _ _ showflag)
= [ case flag of
Just s -> "--"++lf++"="++s
Nothing -> "--"++lf
| flag <- showflag x ]
showOptDescr _ _
= error "Distribution.Simple.Command.showOptDescr: unreachable"
commandListOptions :: CommandUI flags -> [String]
commandListOptions command =
concatMap listOption $
addCommonFlags ShowArgs $
commandGetOpts ShowArgs command
where
listOption (GetOpt.Option shortNames longNames _ _) =
[ "-" ++ [name] | name <- shortNames ]
++ [ "--" ++ name | name <- longNames ]
commandHelp :: CommandUI flags -> String -> String
commandHelp command pname =
commandSynopsis command
++ "\n\n"
++ commandUsage command pname
++ ( case commandDescription command of
Nothing -> ""
Just desc -> '\n': desc pname)
++ "\n"
++ ( if cname == ""
then "Global flags:"
else "Flags for " ++ cname ++ ":" )
++ ( GetOpt.usageInfo ""
. addCommonFlags ShowArgs
$ commandGetOpts ShowArgs command )
++ ( case commandNotes command of
Nothing -> ""
Just notes -> '\n': notes pname)
where cname = commandName command
usageDefault :: String -> String -> String
usageDefault name pname =
"Usage: " ++ pname ++ " " ++ name ++ " [FLAGS]\n\n"
++ "Flags for " ++ name ++ ":"
usageAlternatives :: String -> [String] -> String -> String
usageAlternatives name strs pname = unlines
[ start ++ pname ++ " " ++ name ++ " " ++ s
| let starts = "Usage: " : repeat " or: "
, (start, s) <- zip starts strs
]
mkCommandUI :: String
-> String
-> [String]
-> flags
-> (ShowOrParseArgs -> [OptionField flags])
-> CommandUI flags
mkCommandUI name synopsis usages flags options = CommandUI
{ commandName = name
, commandSynopsis = synopsis
, commandDescription = Nothing
, commandNotes = Nothing
, commandUsage = usageAlternatives name usages
, commandDefaultFlags = flags
, commandOptions = options
}
data CommonFlag = HelpFlag | ListOptionsFlag
commonFlags :: ShowOrParseArgs -> [GetOpt.OptDescr CommonFlag]
commonFlags showOrParseArgs = case showOrParseArgs of
ShowArgs -> [help]
ParseArgs -> [help, list]
where
help = GetOpt.Option helpShortFlags ["help"] (GetOpt.NoArg HelpFlag)
"Show this help text"
helpShortFlags = case showOrParseArgs of
ShowArgs -> ['h']
ParseArgs -> ['h', '?']
list = GetOpt.Option [] ["list-options"] (GetOpt.NoArg ListOptionsFlag)
"Print a list of command line flags"
addCommonFlags :: ShowOrParseArgs
-> [GetOpt.OptDescr a]
-> [GetOpt.OptDescr (Either CommonFlag a)]
addCommonFlags showOrParseArgs options =
map (fmapOptDesc Left) (commonFlags showOrParseArgs)
++ map (fmapOptDesc Right) options
where fmapOptDesc f (GetOpt.Option s l d m) =
GetOpt.Option s l (fmapArgDesc f d) m
fmapArgDesc f (GetOpt.NoArg a) = GetOpt.NoArg (f a)
fmapArgDesc f (GetOpt.ReqArg s d) = GetOpt.ReqArg (f . s) d
fmapArgDesc f (GetOpt.OptArg s d) = GetOpt.OptArg (f . s) d
commandParseArgs :: CommandUI flags
-> Bool
-> [String]
-> CommandParse (flags -> flags, [String])
commandParseArgs command global args =
let options = addCommonFlags ParseArgs
$ commandGetOpts ParseArgs command
order | global = GetOpt.RequireOrder
| otherwise = GetOpt.Permute
in case GetOpt.getOpt' order options args of
(flags, _, _, _)
| any listFlag flags -> CommandList (commandListOptions command)
| any helpFlag flags -> CommandHelp (commandHelp command)
where listFlag (Left ListOptionsFlag) = True; listFlag _ = False
helpFlag (Left HelpFlag) = True; helpFlag _ = False
(flags, opts, opts', [])
| global || null opts' -> CommandReadyToGo (accum flags, mix opts opts')
| otherwise -> CommandErrors (unrecognised opts')
(_, _, _, errs) -> CommandErrors errs
where
accum flags = foldr (flip (.)) id [ f | Right f <- flags ]
unrecognised opts = [ "unrecognized "
++ "'" ++ (commandName command) ++ "'"
++ " option `" ++ opt ++ "'\n"
| opt <- opts ]
mix [] ys = ys
mix (x:xs) ys = x:ys++xs
data CommandParse flags = CommandHelp (String -> String)
| CommandList [String]
| CommandErrors [String]
| CommandReadyToGo flags
instance Functor CommandParse where
fmap _ (CommandHelp help) = CommandHelp help
fmap _ (CommandList opts) = CommandList opts
fmap _ (CommandErrors errs) = CommandErrors errs
fmap f (CommandReadyToGo flags) = CommandReadyToGo (f flags)
data CommandType = NormalCommand | HiddenCommand
data Command action =
Command String String ([String] -> CommandParse action) CommandType
hiddenCommand :: Command action -> Command action
hiddenCommand (Command name synopsys f _cmdType) =
Command name synopsys f HiddenCommand
commandAddAction :: CommandUI flags
-> (flags -> [String] -> action)
-> Command action
commandAddAction command action =
Command (commandName command)
(commandSynopsis command)
(fmap (uncurry applyDefaultArgs) . commandParseArgs command False)
NormalCommand
where applyDefaultArgs mkflags args =
let flags = mkflags (commandDefaultFlags command)
in action flags args
commandsRun :: CommandUI a
-> [Command action]
-> [String]
-> CommandParse (a, CommandParse action)
commandsRun globalCommand commands args =
case commandParseArgs globalCommand True args of
CommandHelp help -> CommandHelp help
CommandList opts -> CommandList (opts ++ commandNames)
CommandErrors errs -> CommandErrors errs
CommandReadyToGo (mkflags, args') -> case args' of
("help":cmdArgs) -> handleHelpCommand cmdArgs
(name:cmdArgs) -> case lookupCommand name of
[Command _ _ action _]
-> CommandReadyToGo (flags, action cmdArgs)
_ -> CommandReadyToGo (flags, badCommand name)
[] -> CommandReadyToGo (flags, noCommand)
where flags = mkflags (commandDefaultFlags globalCommand)
where
lookupCommand cname = [ cmd | cmd@(Command cname' _ _ _) <- commands'
, cname' == cname ]
noCommand = CommandErrors ["no command given (try --help)\n"]
badCommand cname = CommandErrors ["unrecognised command: " ++ cname
++ " (try --help)\n"]
commands' = commands ++ [commandAddAction helpCommandUI undefined]
commandNames = [ name | (Command name _ _ NormalCommand) <- commands' ]
handleHelpCommand cmdArgs =
case commandParseArgs helpCommandUI True cmdArgs of
CommandHelp help -> CommandHelp help
CommandList list -> CommandList (list ++ commandNames)
CommandErrors _ -> CommandHelp globalHelp
CommandReadyToGo (_,[]) -> CommandHelp globalHelp
CommandReadyToGo (_,(name:cmdArgs')) ->
case lookupCommand name of
[Command _ _ action _] ->
case action ("--help":cmdArgs') of
CommandHelp help -> CommandHelp help
CommandList _ -> CommandList []
_ -> CommandHelp globalHelp
_ -> badCommand name
where globalHelp = commandHelp globalCommand
noExtraFlags :: [String] -> IO ()
noExtraFlags [] = return ()
noExtraFlags extraFlags =
die $ "Unrecognised flags: " ++ intercalate ", " extraFlags
getNormalCommandDescriptions :: [Command action] -> [(String, String)]
getNormalCommandDescriptions cmds =
[ (name, description)
| Command name description _ NormalCommand <- cmds ]
helpCommandUI :: CommandUI ()
helpCommandUI =
(mkCommandUI
"help"
"Help about commands."
["[FLAGS]", "COMMAND [FLAGS]"]
()
(const []))
{
commandNotes = Just $ \pname ->
"Examples:\n"
++ " " ++ pname ++ " help help\n"
++ " Oh, appararently you already know this.\n"
}
data CommandSpec action
= forall flags. CommandSpec (CommandUI flags) (CommandUI flags -> Command action) CommandType
commandFromSpec :: CommandSpec a -> Command a
commandFromSpec (CommandSpec ui action _) = action ui