{-# LANGUAGE ConstraintKinds #-} {-# LANGUAGE DataKinds #-} {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE DeriveFunctor #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE InstanceSigs #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE OverlappingInstances #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE TypeOperators #-} {-# LANGUAGE TypeSynonymInstances #-} {-# LANGUAGE ViewPatterns #-} {-# OPTIONS_GHC -fno-warn-deprecated-flags #-} {-# OPTIONS_GHC -fno-warn-unrecognised-pragmas #-} -- | @getopt-generics@ tries to make it very simple to create command line -- argument parsers. module System.Console.GetOpt.Generics.GetArguments where import Data.Orphans () import Prelude () import Prelude.Compat import Data.Char import Data.List.Compat import Data.Maybe import Data.Proxy import Data.Typeable import Generics.SOP import System.Console.GetOpt import System.Environment import Text.Read.Compat import System.Console.GetOpt.Generics.FieldString import System.Console.GetOpt.Generics.Modifier import System.Console.GetOpt.Generics.Result -- | Parses command line arguments (gotten from 'withArgs') and returns the -- parsed value. This function should be enough for simple use-cases. -- -- Throws the same exceptions as 'simpleCLI'. -- -- Here's an example: -- ### Start "docs/RecordTypeExample.hs" Haddock ### -- | -- > {-# LANGUAGE DeriveGeneric #-} -- > -- > import GHC.Generics -- > import System.Console.GetOpt.Generics -- > -- > -- All you have to do is to define a type and derive some instances: -- > -- > data Options -- > = Options { -- > port :: Int, -- > daemonize :: Bool, -- > config :: Maybe FilePath -- > } -- > deriving (Show, GHC.Generics.Generic) -- > -- > instance System.Console.GetOpt.Generics.Generic Options -- > instance HasDatatypeInfo Options -- > -- > -- Then you can use `getArguments` to create a command-line argument parser: -- > -- > main :: IO () -- > main = do -- > options <- getArguments -- > print (options :: Options) -- ### End ### -- | And this is how the above program behaves: -- ### Start "docs/RecordTypeExample.bash-protocol" Haddock ### -- | -- > $ program --port 8080 --config some/path -- > Options {port = 8080, daemonize = False, config = Just "some/path"} -- > $ program --port 8080 --daemonize -- > Options {port = 8080, daemonize = True, config = Nothing} -- > $ program --port foo -- > cannot parse as INTEGER: foo -- > $ program -- > missing option: --port=INTEGER -- > $ program --help -- > program [OPTIONS] -- > --port=INTEGER -- > --daemonize -- > --config=STRING (optional) -- > -h --help show help and exit -- ### End ### getArguments :: forall a . (Generic a, HasDatatypeInfo a, All2 Option (Code a)) => IO a getArguments = modifiedGetArguments [] -- | Like 'getArguments` but allows you to pass in 'Modifier's. modifiedGetArguments :: forall a . (Generic a, HasDatatypeInfo a, All2 Option (Code a)) => [Modifier] -> IO a modifiedGetArguments modifiers = do args <- getArgs progName <- getProgName handleResult $ parseArguments progName modifiers args -- | Pure variant of 'modifiedGetArguments'. -- -- Does not throw any exceptions. parseArguments :: forall a . (Generic a, HasDatatypeInfo a, All2 Option (Code a)) => String -- ^ Name of the program (e.g. from 'getProgName'). -> [Modifier] -- ^ List of 'Modifier's to manually tweak the command line interface. -> [String] -- ^ List of command line arguments to parse (e.g. from 'getArgs'). -> Result a parseArguments progName modifiersList args = do let modifiers = mkModifiers modifiersList case datatypeInfo (Proxy :: Proxy a) of ADT typeName _ (constructorInfo :* Nil) -> case constructorInfo of (Record _ fields) -> processFields progName modifiers args (hliftA (Comp . Selector) fields) Constructor{} -> processFields progName modifiers args (hpure (Comp NoSelector)) Infix{} -> err typeName "infix constructors" ADT typeName _ Nil -> err typeName "empty data types" ADT typeName _ (_ :* _ :* _) -> err typeName "sum types" Newtype _ _ (Record _ fields) -> processFields progName modifiers args (hliftA (Comp . Selector) fields) Newtype typeName _ (Constructor _) -> err typeName "constructors without field labels" where err typeName message = errors ["getopt-generics doesn't support " ++ message ++ " (" ++ typeName ++ ")."] data Field a = NoSelector | Selector a processFields :: forall a xs . (Generic a, Code a ~ '[xs], SingI xs, All Option xs) => String -> Modifiers -> [String] -> NP (Field :.: FieldInfo) xs -> Result a processFields progName modifiers args fields = do initialFieldStates <- mkInitialFieldStates modifiers fields showOutputInfo let (options, arguments, parseErrors) = getOpt Permute (mkOptDescrs modifiers fields) args reportGetOptErrors parseErrors withPositionalArguments <- fillInPositionalArguments arguments $ project options initialFieldStates to . SOP . Z <$> collectResult withPositionalArguments where showOutputInfo :: Result () showOutputInfo = outputInfo progName modifiers args fields reportGetOptErrors :: [String] -> Result () reportGetOptErrors parseErrors = case parseErrors of [] -> pure () errs -> errors errs -- Creates a list of NS where every element corresponds to one field. To be -- used by 'getOpt'. mkOptDescrs :: forall xs . (SingI xs, All Option xs) => Modifiers -> NP (Field :.: FieldInfo) xs -> [OptDescr (NS FieldState xs)] mkOptDescrs modifiers = mapMaybe toOptDescr . apInjs_NP . hcliftA (Proxy :: Proxy Option) (mkOptDescr modifiers) newtype OptDescrE a = OptDescrE (Maybe (OptDescr (FieldState a))) mkOptDescr :: forall a . Option a => Modifiers -> (Field :.: FieldInfo) a -> OptDescrE a mkOptDescr _modifiers (Comp NoSelector) = OptDescrE Nothing mkOptDescr modifiers (Comp (Selector (FieldInfo (mkFieldString -> name)))) = OptDescrE $ if isPositionalArgumentsField modifiers name then Nothing else Just $ Option (mkShortOptions modifiers name) [mkLongOption modifiers name] _toOption (getHelpText modifiers name) toOptDescr :: NS OptDescrE xs -> Maybe (OptDescr (NS FieldState xs)) toOptDescr (Z (OptDescrE (Just a))) = Just $ fmap Z a toOptDescr (Z (OptDescrE Nothing)) = Nothing toOptDescr (S a) = fmap (fmap S) (toOptDescr a) -- Initializes an NP of empty fields to be filled in later. -- Contains only default values. mkInitialFieldStates :: forall xs . (SingI xs, All Option xs) => Modifiers -> NP (Field :.: FieldInfo) xs -> Result (NP FieldState xs) mkInitialFieldStates modifiers fields = case (sing :: Sing xs, fields) of (SNil, Nil) -> return Nil (SCons, Comp (Selector (FieldInfo (mkFieldString -> name))) :* r) -> (:*) <$> inner name <*> mkInitialFieldStates modifiers r (SCons, Comp NoSelector :* r) -> (:*) <$> Success PositionalArgument <*> mkInitialFieldStates modifiers r _ -> uninhabited "mkInitialFieldStates" where inner :: forall x . Option x => FieldString -> Result (FieldState x) inner name = if isPositionalArgumentsField modifiers name then case cast (id :: FieldState x -> FieldState x) of (Just id' :: Maybe (FieldState [String] -> FieldState x)) -> Success $ id' PositionalArguments Nothing -> errors ["UseForPositionalArguments can only be used " ++ "for fields of type [String] not " ++ show (typeOf (impossible "mkInitialFieldStates" :: x))] else return $ _emptyOption modifiers name -- * showing output information data OutputInfoFlag = HelpFlag | VersionFlag String deriving (Eq, Ord) -- Outputs the help or version information if the corresponding flags are given. outputInfo :: (SingI xs, All Option xs) => String -> Modifiers -> [String] -> NP (Field :.: FieldInfo) xs -> Result () outputInfo progName modifiers args fields = case (\ (a, b, c) -> (sort a, b, c)) (getOpt Permute options args) of ([], _, _) -> return () -- no help or version flag given (HelpFlag : _, _, _) -> outputAndExit $ usageInfo header $ toOptDescrUnit (mkOptDescrs modifiers fields) ++ toOptDescrUnit options (VersionFlag version : _, _, _) -> outputAndExit $ progName ++ " version " ++ version ++ "\n" where options :: [OptDescr OutputInfoFlag] options = helpOption : maybeToList versionOption helpOption :: OptDescr OutputInfoFlag helpOption = Option ['h'] ["help"] (NoArg HelpFlag) "show help and exit" versionOption :: Maybe (OptDescr OutputInfoFlag) versionOption = case getVersion modifiers of Just version -> Just $ Option [] ["version"] (NoArg (VersionFlag version)) "show version and exit" Nothing -> Nothing toOptDescrUnit :: [OptDescr a] -> [OptDescr ()] toOptDescrUnit = map (fmap (const ())) header :: String header = unwords $ progName : "[OPTIONS]" : positionalArgumentHelp fields ++ maybe [] (\ t -> ["[" ++ t ++ "]"]) (getPositionalArgumentType modifiers) ++ [] positionalArgumentHelp :: (All Option xs) => NP (Field :.: FieldInfo) xs -> [String] positionalArgumentHelp (p@(Comp NoSelector) :* r) = argumentType (toProxy p) : positionalArgumentHelp r positionalArgumentHelp (_ :* r) = positionalArgumentHelp r positionalArgumentHelp Nil = [] -- Fills in the positional arguments in the NP that already contains the flag -- values. Fills in FieldErrors in case of -- - parse errors and -- - missing positional arguments. -- The returned Either contains errors in case of too many positional arguments. fillInPositionalArguments :: (All Option xs) => [String] -> NP FieldState xs -> Result (NP FieldState xs) fillInPositionalArguments args inputFieldStates = do let (result, errs) = inner (Just args) inputFieldStates either errors return errs Success result where inner :: All Option xs => Maybe [String] -> NP FieldState xs -> (NP FieldState xs, Either [String] ()) inner arguments fields = case (arguments, fields) of (Just arguments, PositionalArguments :* r) -> FieldSuccess arguments `cons` inner Nothing r (Nothing, PositionalArguments :* r) -> FieldErrors ["UseForPositionalArguments can only be used once"] `cons` inner Nothing r (Just (argument : arguments), PositionalArgument :* r) -> case parseArgumentEither argument of Right a -> FieldSuccess a `cons` inner (Just arguments) r Left err -> FieldErrors [err] `cons` inner (Just arguments) r (Just [], p@PositionalArgument :* r) -> FieldErrors ["missing argument of type " ++ argumentType (toProxy p)] `cons` inner (Just []) r (Nothing, PositionalArgument :* _) -> impossible "fillInPositionalArguments" (arguments, a :* r) -> a `cons` inner arguments r (Just [], Nil) -> (Nil, Right ()) (Nothing, Nil) -> (Nil, Right ()) (Just arguments@(_ : _), Nil) -> (Nil, Left (map (\ arg -> "unknown argument: " ++ arg) arguments)) cons :: FieldState x -> (NP FieldState xs, r) -> (NP FieldState (x ': xs), r) cons fieldState (arguments, r) = (fieldState :* arguments, r) -- Collects all FieldStates into a Result NP. If any errors are contained they -- will be accumulated. collectResult :: (SingI xs) => NP FieldState xs -> Result (NP I xs) collectResult input = hsequence $ hliftA inner input where inner :: FieldState x -> Result x inner s = case s of FieldSuccess v -> Success v FieldErrors errs -> errors errs Unset err -> errors [err] PositionalArguments -> impossible "collectResult" PositionalArgument -> impossible "collectResult" -- * helper functions for NS and NP project :: (SingI xs, All Option xs) => [NS FieldState xs] -> NP FieldState xs -> NP FieldState xs project sums start = foldl' inner start sums where inner :: (All Option xs) => NP FieldState xs -> NS FieldState xs -> NP FieldState xs inner (a :* r) (Z b) = combine a b :* r inner (a :* r) (S rSum) = a :* inner r rSum inner Nil _ = uninhabited "project" impossible :: String -> a impossible name = error ("System.Console.GetOpt.Generics." ++ name ++ ": This should never happen!") uninhabited :: String -> a uninhabited = impossible toProxy :: f a -> Proxy a toProxy = const Proxy -- * possible field types data FieldState a where Unset :: String -> FieldState a FieldErrors :: [String] -> FieldState a FieldSuccess :: a -> FieldState a PositionalArguments :: FieldState [String] PositionalArgument :: FieldState a deriving (Typeable) -- | Type class for all allowed field types. -- -- If you want to use custom field types you should implement an -- @instance Option YourCustomType@ containing implementations of -- 'argumentType' and 'parseArgument' (the minimal complete definition). -- -- Here's an example: -- ### Start "docs/CustomOptionsExample.hs" Haddock ### -- | -- > {-# LANGUAGE DeriveDataTypeable #-} -- > -- > import Data.Typeable -- > import System.Console.GetOpt.Generics -- > -- > data File = File FilePath -- > deriving (Show, Typeable) -- > -- > instance Option File where -- > argumentType Proxy = "custom-file-type" -- > parseArgument f = Just (File f) -- > -- > main :: IO () -- > main = simpleCLI $ \ file -> do -- > print (file :: File) -- ### End ### -- | This would give you: -- ### Start "docs/CustomOptionsExample.bash-protocol" Haddock ### -- | -- > $ program some/file -- > File "some/file" -- > $ program --help -- > program [OPTIONS] custom-file-type -- > -h --help show help and exit -- ### End ### class Typeable a => Option a where {-# MINIMAL argumentType, parseArgument #-} -- | Name of the argument type, e.g. "bool" or "integer". argumentType :: Proxy a -> String -- | Parses a 'String' into an argument. Returns 'Nothing' on parse errors. parseArgument :: String -> Maybe a -- | This is meant to be an internal function. _toOption :: ArgDescr (FieldState a) _toOption = ReqArg parseAsFieldState (argumentType (Proxy :: Proxy a)) -- | This is meant to be an internal function. _emptyOption :: Modifiers -> FieldString -> FieldState a _emptyOption modifiers flagName = Unset ("missing option: --" ++ mkLongOption modifiers flagName ++ "=" ++ argumentType (Proxy :: Proxy a)) -- | This is meant to be an internal function. _accumulate :: a -> a -> a _accumulate _ x = x parseArgumentEither :: forall a . Option a => String -> Either String a parseArgumentEither s = maybe (Left ("cannot parse as " ++ argumentType (Proxy :: Proxy a) ++ ": " ++ s)) Right (parseArgument s) parseAsFieldState :: forall a . Option a => String -> FieldState a parseAsFieldState s = either (\ err -> FieldErrors [err]) FieldSuccess (parseArgumentEither s) combine :: Option a => FieldState a -> FieldState a -> FieldState a combine _ (Unset _) = impossible "combine" combine _ PositionalArguments = impossible "combine" combine _ PositionalArgument = impossible "combine" combine (FieldErrors e) (FieldErrors f) = FieldErrors (e ++ f) combine (FieldErrors e) _ = FieldErrors e combine (Unset _) x = x combine (FieldSuccess _) (FieldErrors e) = FieldErrors e combine (FieldSuccess a) (FieldSuccess b) = FieldSuccess (_accumulate a b) combine PositionalArguments _ = PositionalArguments combine PositionalArgument _ = PositionalArgument instance Option a => Option [a] where argumentType Proxy = argumentType (Proxy :: Proxy a) ++ " (multiple possible)" parseArgument x = case parseArgument x of Just (x :: a) -> Just [x] Nothing -> Nothing _emptyOption _ _ = FieldSuccess [] _accumulate = (++) instance Option a => Option (Maybe a) where argumentType Proxy = argumentType (Proxy :: Proxy a) ++ " (optional)" parseArgument x = case parseArgument x of Just (x :: a) -> Just (Just x) Nothing -> Nothing _emptyOption _ _ = FieldSuccess Nothing instance Option Bool where argumentType _ = "BOOL" parseArgument :: String -> Maybe Bool parseArgument s | map toLower s `elem` ["true", "yes", "on"] = Just True | map toLower s `elem` ["false", "no", "off"] = Just False | otherwise = case readMaybe s of Just (n :: Integer) -> Just (n > 0) Nothing -> Nothing _toOption = NoArg (FieldSuccess True) _emptyOption _ _ = FieldSuccess False instance Option String where argumentType Proxy = "STRING" parseArgument = Just instance Option Int where argumentType _ = "INTEGER" parseArgument = readMaybe instance Option Integer where argumentType _ = "INTEGER" parseArgument = readMaybe readNumber :: (RealFloat n, Read n) => String -> Maybe n readNumber s = case readMaybe s of Just n -> Just n Nothing | "." `isPrefixOf` s -> readMaybe ("0" ++ s) | otherwise -> Nothing instance Option Float where argumentType _ = "NUMBER" parseArgument = readNumber instance Option Double where argumentType _ = "NUMBER" parseArgument = readNumber