hsdev-0.1.0.0: tools/System/Command.hs
{-# LANGUAGE GeneralizedNewtypeDeriving, OverloadedStrings, DefaultSignatures, FlexibleInstances #-}
module System.Command (
Command(..),
DefaultConfig(..),
cmd, cmd_,
Help(..), addHelpCommand, addHelp,
brief, help,
run, runCmd,
Opts(..),
mapOpts, traverseOpts,
option, option_, req, noreq, flag,
opt, optMaybe, hasOpt, askOpt, askOptDef, askOpts,
optsToArgs,
splitArgs, unsplitArgs
) where
import Control.Arrow
import Control.Applicative
import Control.Monad (join)
import Data.Char
import Data.List (stripPrefix, unfoldr, isPrefixOf)
import Data.Maybe (fromMaybe, mapMaybe, listToMaybe, maybeToList)
import Data.Map (Map)
import qualified Data.Map as M
import Data.Monoid
import Data.Traversable (traverse)
import System.Console.GetOpt
-- | Command
data Command a = Command {
commandName :: [String],
commandPosArgs :: [String],
commandDesc :: Maybe String,
commandUsage :: [String],
commandRun :: [String] -> Maybe (Either [String] a) }
instance Functor Command where
fmap f cmd' = cmd' {
commandRun = fmap (fmap f) . commandRun cmd' }
-- | Default value for options
class DefaultConfig a where
defaultConfig :: a
default defaultConfig :: Monoid a => a
defaultConfig = mempty
instance DefaultConfig ()
instance DefaultConfig [String]
-- | Make command
-- >cmd name args desc args' onCmd
cmd :: (Monoid c, DefaultConfig c) => [String] -> [String] -> String -> [OptDescr c] -> (c -> [String] -> a) -> Command a
cmd name posArgs descr as act = Command {
commandName = name,
commandPosArgs = posArgs,
commandDesc = descr',
commandUsage = lines $ usageInfo (unwords (name ++ map (\a -> "[" ++ a ++ "]") posArgs) ++ maybe "" (" -- " ++) descr') as,
commandRun = \opts -> case getOpt Permute as opts of
(opts', cs, errs) -> fmap (\cs' -> if null errs then return (act (mconcat opts' `mappend` defaultConfig) cs') else Left errs) (stripPrefix name cs) }
where
descr' = if null descr then Nothing else Just descr
-- | Make command without params
cmd_ :: [String] -> [String] -> String -> ([String] -> a) -> Command a
cmd_ name posArgs descr act = cmd name posArgs descr [] (act' act) where
act' :: a -> () -> a
act' = const
data Help =
HelpUsage [String] |
HelpCommands [([String], [String])]
deriving (Eq, Ord, Read, Show)
-- | Add help command
addHelpCommand :: String -> (Either String Help -> a) -> [Command a] -> [Command a]
addHelpCommand tool toCmd cmds = cmds' where
cmds' = helpcmd' : cmds
helpcmd = fmap toCmd $ cmd_ ["help"] ["command"] ("help command, also can be called in form '" ++ tool ++ " [command] -?'") onHelp
-- allow help by last argument '-?'
helpcmd' = helpcmd { commandRun = commandRun helpcmd . rewrite } where
rewrite as
| last as == "-?" = "help" : init as
| otherwise = as
onHelp [] = Right $ HelpUsage [tool ++ " " ++ brief c | c <- cmds']
onHelp cmdname = case filter ((cmdname `isPrefixOf`) . commandName) cmds' of
[] -> Left $ "Unknown command: " ++ unwords cmdname
helps -> Right $ HelpCommands $ map (commandName &&& (addHeader . help)) helps
addHeader [] = []
addHeader (h:hs) = (tool ++ " " ++ h) : hs
-- | Add help commands, which outputs help to stdout
addHelp :: String -> (IO () -> a) -> [Command a] -> [Command a]
addHelp tool liftPrint cmds = addHelpCommand tool toCmd cmds where
toCmd = liftPrint . either putStrLn printHelp
printHelp :: Help -> IO ()
printHelp (HelpUsage u) = mapM_ putStrLn $ map ('\t':) u
printHelp (HelpCommands cs) = mapM_ putStrLn $ map ('\t':) $ concatMap snd cs
-- | Show brief help for command
brief :: Command a -> String
brief = head . commandUsage
-- | Show detailed help for command
help :: Command a -> [String]
help = commandUsage
-- | Run commands
run :: [Command a] -> a -> ([String] -> a) -> [String] -> a
run cmds onDef onError as = maybe onDef (either onError id) found where
found = listToMaybe $ mapMaybe (`commandRun` as) cmds
-- | Try run command, wrapping any negative result to 'Maybe'
runCmd :: Command a -> [String] -> Maybe a
runCmd cmd = join . fmap toMaybe . commandRun cmd where
toMaybe :: Either b c -> Maybe c
toMaybe = either (const Nothing) Just
-- | Options holder
newtype Opts = Opts { getOpts :: Map String [String] }
instance Monoid Opts where
mempty = Opts mempty
(Opts l) `mappend` (Opts r) = Opts $ M.unionWith (++) l r
instance DefaultConfig Opts
-- | Map 'Opts'
mapOpts :: (String -> String) -> Opts -> Opts
mapOpts f = Opts . M.mapKeys f . getOpts
-- | Traverse 'Opts'
traverseOpts :: Applicative f => (String -> String -> f String) -> Opts -> f Opts
traverseOpts f = fmap Opts . M.traverseWithKey (traverse . f) . getOpts
option :: [Char] -> String -> [String] -> (String -> ArgDescr Opts) -> String -> OptDescr Opts
option fs name names onOption d = Option fs (name:names) (onOption name) d
option_ :: [Char] -> String -> (String -> ArgDescr Opts) -> String -> OptDescr Opts
option_ fs name = option fs name []
-- | Required option
req :: String -> String -> ArgDescr Opts
req nm n = ReqArg (opt n) nm
-- | Optional option
noreq :: String -> String -> ArgDescr Opts
noreq nm n = OptArg (optMaybe n) nm
-- | Flag option
flag :: String -> ArgDescr Opts
flag n = NoArg flag' where
flag' :: Opts
flag' = Opts $ M.singleton n []
opt :: String -> String -> Opts
opt n v = Opts $ M.singleton n [v]
optMaybe :: String -> Maybe String -> Opts
optMaybe n = Opts . M.singleton n . maybeToList
hasOpt :: String -> Opts -> Bool
hasOpt n = M.member n . getOpts
askOpt :: String -> Opts -> Maybe String
askOpt n = listToMaybe . askOpts n
askOptDef :: String -> Opts -> Maybe (Maybe String)
askOptDef n = fmap listToMaybe . M.lookup n . getOpts
askOpts :: String -> Opts -> [String]
askOpts n = fromMaybe [] . M.lookup n . getOpts
-- | Print 'Opts' as args
optsToArgs :: Opts -> [String]
optsToArgs = concatMap optToArgs . M.toList . getOpts where
optToArgs :: (String, [String]) -> [String]
optToArgs (n, []) = ["--" ++ n]
optToArgs (n, vs) = ["--"++ n ++ "=" ++ v | v <- vs]
-- | Split string to words
splitArgs :: String -> [String]
splitArgs "" = []
splitArgs (c:cs)
| isSpace c = splitArgs cs
| c == '"' = let (w, cs') = readQuote cs in w : splitArgs cs'
| otherwise = let (ws, tl) = break isSpace cs in (c:ws) : splitArgs tl
where
readQuote :: String -> (String, String)
readQuote "" = ("", "")
readQuote ('\\':ss)
| null ss = ("\\", "")
| otherwise = first (head ss :) $ readQuote (tail ss)
readQuote ('"':ss) = ("", ss)
readQuote (s:ss) = first (s:) $ readQuote ss
unsplitArgs :: [String] -> String
unsplitArgs = unwords . map escape where
escape :: String -> String
escape str
| any isSpace str || '"' `elem` str = "\"" ++ concat (unfoldr escape' str) ++ "\""
| otherwise = str
escape' :: String -> Maybe (String, String)
escape' [] = Nothing
escape' (ch:tl) = Just (escaped, tl) where
escaped = case ch of
'"' -> "\\\""
'\\' -> "\\\\"
_ -> [ch]