{-# LANGUAGE FlexibleContexts, CPP #-}
module Jukebox.Options where
import Data.Char
import Data.List
import System.Environment
import System.Exit
import System.IO
#if __GLASGOW_HASKELL__ < 710
import Control.Applicative
import Data.Monoid
#endif
import Data.Semigroup(Semigroup(..))
data Annotated d p a = Annotated
{ descr :: d,
parser :: p a }
instance Functor p => Functor (Annotated d p) where
fmap f (Annotated d x) = Annotated d (fmap f x)
instance (Monoid d, Applicative p) => Applicative (Annotated d p) where
pure = Annotated mempty . pure
Annotated d f <*> Annotated d' x =
Annotated (d `mappend` d') (f <*> x)
instance (Semigroup d, Monoid d, Semigroup (p a), Monoid (p a)) => Monoid (Annotated d p a) where
mempty = Annotated mempty mempty
mappend = (<>)
instance (Semigroup d, Semigroup (p a)) => Semigroup (Annotated d p a) where
Annotated d p <> Annotated d' p' =
Annotated (d <> d') (p <> p')
type ArgParser = Annotated [String] SeqParser
data SeqParser a = SeqParser
{ args :: Int,
consume :: [String] -> Either Error a }
instance Functor SeqParser where
fmap f (SeqParser a c) = SeqParser a (fmap f . c)
instance Applicative SeqParser where
pure = SeqParser 0 . const . pure
SeqParser a c <*> SeqParser a' c' = SeqParser (a + a') f
where f xs = c xs <*> c' (drop a xs)
arg :: String -> String -> (String -> Maybe a) -> ArgParser a
arg desc err f = Annotated [desc] (SeqParser 1 c)
where c [] = Left (Mistake err)
c (x:_) | "-" `isPrefixOf` x = Left (Mistake err)
c (x:_) =
case f x of
Nothing -> Left (Mistake err)
Just ok -> Right ok
argNum :: (Read a, Num a) => ArgParser a
argNum = arg "<num>" "expected a number" f
where f x =
case reads x of
[(y, "")] -> Just y
_ -> Nothing
argFile :: ArgParser FilePath
argFile = arg "<file>" "expected a file" Just
argFiles :: ArgParser [FilePath]
argFiles = arg "<files>" "expected a list of files" $ \x ->
Just $ elts $ x ++ ","
where
elts [] = []
elts s = w:elts r
where
w = takeWhile (/= ',') s
r = tail (dropWhile (/= ',') s)
argName :: ArgParser String
argName = arg "<name>" "expected a name" Just
argNums :: ArgParser [Int]
argNums = arg "<nums>" "expected a number list" $ \x ->
nums . groupBy (\x y -> isDigit x == isDigit y) $ x ++ ","
where
nums [] = Just []
nums (n:",":ns) = (read n :) `fmap` nums ns
nums (n:"..":m:",":ns) = ([read n .. read m] ++) `fmap` nums ns
nums _ = Nothing
argOption :: [(String, a)] -> ArgParser a
argOption as =
argOptionWith "one" "or" "" (map fst as) (`lookup` as)
argList :: [String] -> ArgParser [String]
argList as =
argOptionWith "several" "and" "*" as $ \x -> elts (x ++ ",")
where
elts [] = Just []
elts s | w `elem` as = (w:) `fmap` elts r
where
w = takeWhile (/= ',') s
r = tail (dropWhile (/= ',') s)
elts _ = Nothing
argOptionWith :: String -> String -> String -> [String] -> (String -> Maybe a) -> ArgParser a
argOptionWith one or suff opts p =
arg ("<" ++ intercalate " | " opts ++ ">" ++ suff)
("expected " ++ one ++ " of " ++ list) p
where
list =
case opts of
[] -> "<empty list>"
_ ->
intercalate ", " (init opts) ++ " " ++ or ++ " " ++ last opts
argUsage :: ExitCode -> [String] -> ArgParser a
argUsage code err = Annotated [] (SeqParser 0 (const (Left (Usage code err))))
type OptionParser = Annotated [Flag] ParParser
data Flag = Flag
{ flagName :: String,
flagGroup :: String,
flagMode :: FlagMode,
flagHelp :: [String],
flagArgs :: String } deriving (Eq, Show)
data FlagMode = NormalMode | ExpertMode | HiddenMode deriving (Eq, Show)
flagExpert :: Flag -> Bool
flagExpert f = flagMode f == ExpertMode
data ParParser a = ParParser
{ val :: IO a,
peek :: [String] -> ParseResult a }
data ParseResult a
= Yes Int (ParParser a)
| No (ParParser a)
| Error Error
data Error =
Mistake String
| Usage ExitCode [String]
instance Functor ParParser where
fmap f x = pure f <*> x
instance Applicative ParParser where
pure x = ParParser (return x) (const (pure x))
ParParser v p <*> ParParser v' p' =
ParParser (v <*> v') (\xs -> p xs <*> p' xs)
instance Functor ParseResult where
fmap f x = pure f <*> x
instance Applicative ParseResult where
pure = No . pure
Yes n r <*> Yes n' r'
| n == n' = Yes n (r <*> r')
| otherwise = error "Options.ParseResult: inconsistent number of arguments"
Error s <*> _ = Error s
_ <*> Error s = Error s
Yes n r <*> No x = Yes n (r <*> x)
No x <*> Yes n r = Yes n (x <*> r)
No f <*> No x = No (f <*> x)
runPar :: ParParser a -> [String] -> Either Error (IO a)
runPar p [] = Right (val p)
runPar p xs@(x:_) =
case peek p xs of
Yes n p' -> runPar p' (drop n xs)
No _ -> Left (Mistake ("Didn't recognise option " ++ x))
Error err -> Left err
await :: (String -> Bool) -> a -> (String -> [String] -> ParseResult a) -> ParParser a
await p def par = ParParser (return def) f
where f (x:xs) | p x =
case par x xs of
Yes n r -> Yes (n+1) r
No _ ->
error "Options.await: got No"
Error err -> Error err
f _ = No (await p def par)
primFlag ::
String -> [String] ->
(String -> Bool) ->
(a -> a -> Either Error a) ->
a -> ArgParser (String -> a) -> OptionParser a
primFlag name help p combine def (Annotated desc (SeqParser args f)) =
Annotated [desc'] (await p def (g Right))
where desc' = Flag name "General options" NormalMode help (unwords desc)
g comb x xs =
case f xs >>= comb . ($ x) of
Left (Mistake err) -> Error (Mistake ("Error in option --" ++ name ++ ": " ++ err))
Left (Usage code err) -> Error (Usage code err)
Right y ->
Yes args (await p y (g (combine y)))
flag :: String -> [String] -> a -> ArgParser a -> OptionParser a
flag name help def p =
primFlag name help
(\x -> x == "--" ++ name)
(\_ y -> return y)
def (const <$> p)
manyFlags :: String -> [String] -> ArgParser a -> OptionParser [a]
manyFlags name help p =
primFlag name help
(\x -> x == "--" ++ name)
(\x y -> return (x ++ y))
[] (const <$> return <$> p)
bool :: String -> [String] -> Bool -> OptionParser Bool
bool name help def =
primFlag ("(no-)" ++ name) help
(\x -> x `elem` ["--" ++ name, "--no-" ++ name])
(\_ y -> return y)
def
(pure (\name' -> if "--" ++ name == name' then True else False))
filenames :: OptionParser [String]
filenames = Annotated [] (from [])
where from xs = await p xs (f xs)
p x = not ("-" `isPrefixOf` x) || x == "-"
f xs y _ = Yes 0 (from (xs ++ [y]))
io :: IO a -> OptionParser a
io m = Annotated [] p
where p = ParParser m (const (No p))
inGroup :: String -> OptionParser a -> OptionParser a
inGroup x (Annotated fls f) = Annotated [fl{ flagGroup = x } | fl <- fls] f
expert :: OptionParser a -> OptionParser a
expert (Annotated fls f) = Annotated [fl{ flagMode = ExpertMode } | fl <- fls] f
hidden :: OptionParser a -> OptionParser a
hidden (Annotated fls f) = Annotated [fl{ flagMode = HiddenMode } | fl <- fls] f
version :: String -> OptionParser a -> OptionParser a
version x p =
p <*
inGroup "Miscellaneous options"
(flag "version" ["Show the version number."] ()
(argUsage ExitSuccess [x]))
printHelp :: ExitCode -> [String] -> IO a
printHelp code xs = do
mapM_ (hPutStrLn stderr) xs
exitWith code
printError :: String -> String -> IO a
printError name err =
printHelp (ExitFailure 1) $
[err ++ ".", "Try " ++ name ++ " --help."]
help :: String -> String -> OptionParser a -> OptionParser a
help name description p = p'
where
p' =
p <*
(inGroup "Miscellaneous options" $
flag "help" ["Show help text."] ()
(argUsage ExitSuccess (helpText False name description p')))
<*
(if any flagExpert (descr p) then
(inGroup "Miscellaneous options" $
flag "expert-help" ["Show help text for hidden options."] ()
(argUsage ExitSuccess (helpText True name description p')))
else pure ())
usageText :: String -> String -> [String]
usageText name descr =
[descr ++ ".",
"Usage: " ++ name ++ " <option>* <file>*, where <file> is in TPTP format."]
helpText :: Bool -> String -> String -> OptionParser a -> [String]
helpText expert name description p =
intercalate [""] $
[usageText name description] ++
[[flagGroup f0 ++ ":"] ++
concat [justify ("--" ++ flagName f ++ " " ++ flagArgs f) (flagHelp f) | f <- fs]
| fs@(f0:_) <- groups (filter ok (nub (descr p))) ] ++
[ ["To see hidden options too, try --expert-help."]
| any flagExpert (descr p), not expert ]
where
groups [] = []
groups (f:fs) =
(f:[f' | f' <- fs, flagGroup f == flagGroup f']):
groups [f' | f' <- fs, flagGroup f /= flagGroup f']
ok flag =
case flagMode flag of
NormalMode -> True
ExpertMode -> expert
HiddenMode -> False
justify :: String -> [String] -> [String]
justify name help = [" " ++ name] ++ map (" " ++) help
parseCommandLine :: String -> OptionParser a -> IO a
parseCommandLine description p =
parseCommandLineWithExtraArgs [] description p
parseCommandLineWithExtraArgs :: [String] -> String -> OptionParser a -> IO a
parseCommandLineWithExtraArgs args0 description p = do
name <- getProgName
args <- getArgs
parseCommandLineWithArgs name (args0 ++ args) description p
parseCommandLineWithArgs :: String -> [String] -> String -> OptionParser a -> IO a
parseCommandLineWithArgs name args description p = do
case runPar (parser (help name description p)) args of
Left (Mistake err) -> printError name err
Left (Usage code err) -> printHelp code err
Right x -> x