{-# LANGUAGE FlexibleInstances, DeriveDataTypeable, CPP #-}
#if __GLASGOW_HASKELL__ > 720
{-# LANGUAGE Safe #-}
#endif
module System.Console.ParseArgs (
Arg(..),
Argtype(..),
ArgsComplete(..),
ArgsDash(..),
APCData(..),
ArgsParseControl(..),
DataArg,
argDataRequired, argDataOptional, argDataDefaulted,
Args(..),
parseArgs, parseArgsIO,
gotArg, ArgType(..),
getArgString, getArgFile, getArgStdio,
getArgInteger, getArgInt,
getArgDouble, getArgFloat,
ArgFileOpener(..),
ParseArgsException(..),
baseName, parseError, usageError,
System.IO.IOMode(ReadMode, WriteMode, AppendMode))
where
import Control.Exception
import Control.Monad
import Control.Monad.ST
import Data.List
import qualified Data.Map as Map
import Data.Maybe
import Data.Typeable
import System.Environment
import System.IO
data (Ord a) => Arg a =
Arg { argIndex :: a
, argAbbr :: Maybe Char
, argName :: Maybe String
, argData :: Maybe DataArg
, argDesc :: String
}
data Argtype = ArgtypeString (Maybe String)
| ArgtypeInteger (Maybe Integer)
| ArgtypeInt (Maybe Int)
| ArgtypeDouble (Maybe Double)
| ArgtypeFloat (Maybe Float)
data DataArg = DataArg { dataArgName :: String
, dataArgArgtype :: Argtype
, dataArgOptional :: Bool
}
argDataRequired :: String
-> (Maybe a -> Argtype)
-> Maybe DataArg
argDataRequired s c = Just (DataArg { dataArgName = s,
dataArgArgtype = c Nothing,
dataArgOptional = False })
argDataOptional :: String
-> (Maybe a -> Argtype)
-> Maybe DataArg
argDataOptional s c = Just (DataArg { dataArgName = s,
dataArgArgtype = c Nothing,
dataArgOptional = True })
argDataDefaulted :: String
-> (Maybe a -> Argtype)
-> a
-> Maybe DataArg
argDataDefaulted s c d = Just (DataArg { dataArgName = s,
dataArgArgtype = c (Just d),
dataArgOptional = True })
data Argval = ArgvalFlag
| ArgvalString String
| ArgvalInteger Integer
| ArgvalInt Int
| ArgvalDouble Double
| ArgvalFloat Float
newtype ArgRecord a = ArgRecord (Map.Map a Argval)
data (Ord a) => Args a =
Args { __args :: ArgRecord a
, argsProgName :: String
, argsUsage :: String
, argsRest :: [ String ]
}
data ParseArgsException = ParseArgsException String String
deriving (Eq, Typeable)
instance Exception ParseArgsException
instance Show ParseArgsException where
show (ParseArgsException usage msg) = msg ++ "\n" ++ usage
arg_posn :: (Ord a) =>
Arg a
-> Bool
arg_posn (Arg { argAbbr = Nothing,
argName = Nothing }) = True
arg_posn _ = False
arg_flag :: (Ord a) =>
Arg a
-> Bool
arg_flag a = not (arg_posn a)
arg_optional :: (Ord a) =>
Arg a
-> Bool
arg_optional (Arg { argData = Just (DataArg { dataArgOptional = b }) }) = b
arg_optional _ = True
arg_required :: (Ord a) =>
Arg a
-> Bool
arg_required a = not (arg_optional a)
arg_default_value :: (Ord a)
=> Arg a
-> Maybe Argval
arg_default_value arg@(Arg { argData = Just
(DataArg { dataArgArgtype = da }) }) |
arg_optional arg =
defval da
where
defval (ArgtypeString (Just v)) = Just (ArgvalString v)
defval (ArgtypeInteger (Just v)) = Just (ArgvalInteger v)
defval (ArgtypeInt (Just v)) = Just (ArgvalInt v)
defval (ArgtypeDouble (Just v)) = Just (ArgvalDouble v)
defval (ArgtypeFloat (Just v)) = Just (ArgvalFloat v)
defval _ = Nothing
arg_default_value _ = Nothing
perhaps :: Bool -> String -> String
perhaps b s = if b then s else ""
arg_string :: (Ord a) =>
Arg a
-> String
arg_string a@(Arg { argAbbr = abbr,
argName = name,
argData = arg }) =
(optionally "[") ++
(sometimes flag_abbr abbr) ++
(perhaps ((isJust abbr) && (isJust name)) ",") ++
(sometimes flag_name name) ++
(perhaps ((arg_flag a) && (isJust arg)) " ") ++
(sometimes data_arg arg) ++
(optionally "]")
where
sometimes = maybe ""
optionally s = perhaps (arg_optional a) s
flag_name s = "--" ++ s
flag_abbr c = [ '-', c ]
data_arg (DataArg {dataArgName = s}) = "<" ++ s ++ ">"
filter_keys :: [ (Maybe a, b) ]
-> [ (a, b) ]
filter_keys l =
foldr check_key [] l
where
check_key (Nothing, _) rest = rest
check_key (Just k, v) rest = (k, v) : rest
argdesc_error :: String
-> a
argdesc_error msg =
error ("internal error: argument description: " ++ msg)
keymap_from_list :: (Ord k, Show k) =>
[ (k, a) ]
-> Map.Map k a
keymap_from_list l =
foldl add_entry Map.empty l
where
add_entry m (k, a) =
case Map.member k m of
False -> Map.insert k a m
True -> argdesc_error ("duplicate argument description name " ++
(show k))
make_keymap :: (Ord k, Show k) =>
(Arg a -> Maybe k)
-> [Arg a]
-> Map.Map k (Arg a)
make_keymap f_field ads =
(keymap_from_list .
filter_keys .
map (\arg -> (f_field arg, arg))) ads
data ArgsComplete = ArgsComplete
| ArgsTrailing String
| ArgsInterspersed
data ArgsDash = ArgsHardDash
| ArgsSoftDash
deriving Eq
data ArgsParseControl = ArgsParseControl {
apcComplete :: ArgsComplete,
apcDash :: ArgsDash }
class APCData a where
getAPCData :: a -> ArgsParseControl
instance APCData ArgsParseControl where
getAPCData a = a
instance APCData ArgsComplete where
getAPCData a = ArgsParseControl a ArgsHardDash
exhaust :: (s -> [e] -> ([e], s))
-> s
-> [e]
-> s
exhaust _ s [] = s
exhaust f s l =
let (l', s') = f s l
in exhaust f s' l'
parseError :: String
-> String
-> a
parseError usage msg =
throw (ParseArgsException usage msg)
parseArgs :: (Show a, Ord a, APCData b) =>
b
-> [ Arg a ]
-> String
-> [ String ]
-> Args a
parseArgs apcData argd pathname argv =
runST (do
check_argd
let (flag_args, posn_args) = span arg_flag argd
let name_hash = make_keymap argName flag_args
let abbr_hash = make_keymap argAbbr flag_args
let prog_name = baseName pathname
let usage = make_usage_string prog_name
let (am, _, rest) = exhaust (parse usage name_hash abbr_hash)
(Map.empty, posn_args, [])
argv
let required_args = filter (not . arg_optional) argd
unless (and (map (check_present usage am) required_args))
(error "internal error")
let am' = foldl supply_defaults am argd
return (Args { __args = ArgRecord am',
argsProgName = prog_name,
argsUsage = usage,
argsRest = rest }))
where
supply_defaults am ad@(Arg { argIndex = k }) =
case Map.lookup k am of
Just _ -> am
Nothing -> case arg_default_value ad of
Just v -> Map.insert k v am
Nothing -> am
check_present usage am ad@(Arg { argIndex = k }) =
case Map.lookup k am of
Just _ -> True
Nothing -> parseError usage ("missing required argument " ++
(arg_string ad))
check_argd :: ST s ()
check_argd = do
let (_, posns) = span arg_flag argd
unless (all arg_posn posns)
(argdesc_error "argument description mixes flags and positionals")
when (or (map arg_nullary argd))
(argdesc_error "bogus 'nothing' argument")
return ()
where
arg_nullary (Arg { argName = Nothing,
argAbbr = Nothing,
argData = Nothing }) = True
arg_nullary _ = False
make_usage_string prog_name =
summary_line ++ arg_lines
where
flag_args = filter arg_flag argd
posn_args = filter arg_posn argd
n = maximum (map (length . arg_string) argd)
summary_line =
"usage: " ++ prog_name ++
perhaps
(not (null flag_args))
" [options]" ++
perhaps
(not (null posn_args))
(" " ++ unwords (map arg_string posn_args)) ++
(case apcComplete $ getAPCData apcData of
ArgsComplete -> ""
ArgsTrailing s -> " [--] [" ++ s ++ " ...]"
ArgsInterspersed -> " ... [--] ...") ++ "\n"
arg_lines = concatMap (arg_line n) argd where
arg_line na a =
let s = arg_string a in
" " ++ s ++
replicate (na - (length s)) ' ' ++
" " ++ argDesc a ++ "\n"
parse _ _ _ av@(_, _, []) [] = ([], av)
parse usage _ _ av [] =
case apcComplete $ getAPCData apcData of
ArgsComplete -> parseError usage "unexpected extra arguments"
_ -> ([], av)
parse usage name_hash abbr_hash (am, posn, rest) av@(aa : aas) =
case aa of
"--" -> case getAPCData apcData of
ArgsParseControl ArgsComplete ArgsHardDash ->
parseError usage ("unexpected -- " ++
"(extra arguments not allowed)")
_ -> ([], (am, posn, (rest ++ aas)))
s@('-' : '-' : name)
| isJust (Map.lookup name name_hash) ||
apcDash (getAPCData apcData) == ArgsHardDash ->
case Map.lookup name name_hash of
Just ad ->
let (args', am') = peel s ad aas in
(args', (am', posn, rest))
Nothing ->
case getAPCData apcData of
ArgsParseControl ArgsInterspersed _ ->
(aas, (am, posn, rest ++ ["--" ++ name]))
_ ->
parseError usage
("unknown argument --" ++ name)
('-' : abbr : abbrs)
| isJust (Map.lookup abbr abbr_hash) ||
apcDash (getAPCData apcData) == ArgsHardDash ->
case Map.lookup abbr abbr_hash of
Just ad ->
let (args', am') = peel ['-', abbr] ad aas
state' = (am', posn, rest)
in case abbrs of
[] -> (args', state')
('-' : _) -> parseError usage
("bad internal '-' in argument " ++ aa)
_ -> (['-' : abbrs] ++ args', state')
Nothing ->
case apcComplete $ getAPCData apcData of
ArgsInterspersed ->
(aas,
(am, posn, rest ++ ['-' : abbr : abbrs]))
_ -> parseError usage
("unknown argument -" ++ [abbr])
_ ->
case posn of
(p : ps) ->
let (_, req_posn) = partition arg_optional posn in
case length av - length req_posn of
n_extra | n_extra > 0 || (n_extra == 0 && arg_required p) ->
let (args', am') = peel (dataArgName $ fromJust $
argData p) p av in
(args', (am', ps, rest))
0 -> (av, (am, ps, rest))
_ -> parseError usage
"missing required positional argument(s)"
[] -> ([], (am, [], rest ++ av))
where
add_entry s m (k, a) =
case Map.member k m of
False -> Map.insert k a m
True -> parseError usage ("duplicate argument " ++ s)
peel name (Arg { argData = Nothing, argIndex = index }) argl =
let am' = add_entry name am (index, ArgvalFlag)
in (argl, am')
peel name (Arg { argData = Just (DataArg {}) }) [] =
parseError usage (name ++ " is missing its argument")
peel name (Arg { argData =
Just (DataArg { dataArgArgtype = atype }),
argIndex = index })
(a : argl) =
let v = case atype of
ArgtypeString _ -> ArgvalString a
ArgtypeInteger _ -> read_arg ArgvalInteger
"an integer"
ArgtypeInt _ -> read_arg ArgvalInt "an int"
ArgtypeDouble _ -> read_arg ArgvalDouble "a double"
ArgtypeFloat _ -> read_arg ArgvalFloat "a float"
where
read_arg constructor kind =
case reads a of
[(val, "")] -> constructor val
_ -> parseError usage ("argument " ++
a ++ " to " ++ name ++
" is not " ++ kind)
am' = add_entry name am (index, v)
in (argl, am')
parseArgsIO :: (Show a, Ord a, APCData b) =>
b
-> [ Arg a ]
-> IO (Args a)
parseArgsIO apcData argd = do
argv <- getArgs
pathname <- getProgName
return (parseArgs apcData argd pathname argv)
gotArg :: (Ord a) =>
Args a
-> a
-> Bool
gotArg (Args { __args = ArgRecord am }) k =
case Map.lookup k am of
Just _ -> True
Nothing -> False
class ArgType b where
getArg :: (Show a, Ord a)
=> Args a
-> a
-> Maybe b
getRequiredArg :: (Show a, Ord a)
=> Args a
-> a
-> b
getRequiredArg ads index =
case getArg ads index of
Just v -> v
Nothing -> error ("internal error: required argument "
++ show index ++ "not supplied")
getArgPrimitive :: Ord a => (Argval -> Maybe b) -> Args a -> a -> Maybe b
getArgPrimitive decons (Args { __args = ArgRecord am }) k =
Map.lookup k am >>= decons
instance ArgType () where
getArg =
getArgPrimitive flagArg
where
flagArg ArgvalFlag = return ()
flagArg _ = error "internal error: flag arg at wrong type"
instance ArgType ([] Char) where
getArg =
getArgPrimitive stringArg
where
stringArg (ArgvalString s) = return s
stringArg _ = error "internal error: string arg at wrong type"
getArgString :: (Show a, Ord a) =>
Args a
-> a
-> Maybe String
getArgString = getArg
instance ArgType Integer where
getArg =
getArgPrimitive integerArg
where
integerArg (ArgvalInteger i) = return i
integerArg _ = error "internal error: integer arg at wrong type"
getArgInteger :: (Show a, Ord a) =>
Args a
-> a
-> Maybe Integer
getArgInteger = getArg
instance ArgType Int where
getArg =
getArgPrimitive intArg
where
intArg (ArgvalInt i) = return i
intArg _ = error "internal error: int arg at wrong type"
getArgInt :: (Show a, Ord a) =>
Args a
-> a
-> Maybe Int
getArgInt = getArg
instance ArgType Double where
getArg =
getArgPrimitive doubleArg
where
doubleArg (ArgvalDouble d) = return d
doubleArg _ = error "internal error: double arg at wrong type"
getArgDouble :: (Show a, Ord a) =>
Args a
-> a
-> Maybe Double
getArgDouble = getArg
instance ArgType Float where
getArg =
getArgPrimitive floatArg
where
floatArg (ArgvalFloat f) = return f
floatArg _ = error "internal error: float arg at wrong type"
getArgFloat :: (Show a, Ord a) =>
Args a
-> a
-> Maybe Float
getArgFloat = getArg
newtype ArgFileOpener = ArgFileOpener {
argFileOpener :: IOMode -> IO Handle
}
instance ArgType ArgFileOpener where
getArg ads index =
getArg ads index >>=
(\s -> return $ ArgFileOpener { argFileOpener = openFile s })
getArgFile :: (Show a, Ord a) =>
Args a
-> a
-> IOMode
-> IO (Maybe Handle)
getArgFile ads k m =
case getArg ads k of
Just fo -> (do h <- argFileOpener fo m; return (Just h))
Nothing -> return Nothing
getArgStdio :: (Show a, Ord a) =>
Args a
-> a
-> IOMode
-> IO Handle
getArgStdio ads k m =
case getArg ads k of
Just s -> openFile s m
Nothing ->
case m of
ReadMode -> return stdin
WriteMode -> return stdout
AppendMode -> return stdout
ReadWriteMode ->
error ("internal error: tried to open stdio "
++ "in ReadWriteMode")
baseName :: String
-> String
baseName s =
let s' = dropWhile (/= '/') s in
if null s' then s else baseName (tail s')
usageError :: (Ord a) => Args a -> String -> b
usageError ads msg = error (argsUsage ads ++ "\n" ++ msg)