{-# LANGUAGE ConstraintKinds #-} {-# LANGUAGE DataKinds #-} {-# LANGUAGE DeriveFunctor #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE TypeSynonymInstances #-} {-# LANGUAGE ViewPatterns #-} {-# OPTIONS_GHC -fno-warn-unrecognised-pragmas #-} -- | @getopt-generics@ tries to make it very simple to create command line -- argument parsers. An introductory example can be found in the -- . module System.Console.GetOpt.Generics ( -- * IO API getArguments, modifiedGetArguments, -- * Pure API parseArguments, Result(..), -- * Customizing the CLI Modifier(..), deriveShortOptions, -- * Available Field Types Option(..), ) where import Prelude () import Prelude.Compat import Data.Char import Data.List import Generics.SOP import System.Console.GetOpt.Compat import System.Environment import System.Exit import System.IO import Text.Read.Compat import System.Console.GetOpt.Generics.Modifier import System.Console.GetOpt.Generics.Internal -- | Parses command line arguments (gotten from 'withArgs') and returns the -- parsed value. This function should be enough for simple use-cases. -- -- May throw the following exceptions: -- -- - @'ExitFailure' 1@ in case of invalid options. Error messages are written -- to @stderr@. -- - @'ExitSuccess'@ in case @--help@ is given. (@'ExitSuccess'@ behaves like -- a normal exception, except that -- if uncaught -- the process will exit -- with exit-code @0@.) Help output is written to @stdout@. 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 case parseArguments progName modifiers args of Success a -> return a OutputAndExit message -> do putStrLn message exitWith ExitSuccess Errors errs -> do mapM_ (hPutStrLn stderr) errs exitWith $ ExitFailure 1 -- | Type to wrap results from the pure parsing functions. data Result a = Success a -- ^ The CLI was used correctly and a value of type @a@ was -- successfully constructed. | Errors [String] -- ^ The CLI was used incorrectly. The 'Result' contains a list of error -- messages. -- -- It can also happen that the data type you're trying to use isn't -- supported. See the -- for -- details. | OutputAndExit String -- ^ The CLI was used with @--help@. The 'Result' contains the help message. deriving (Show, Eq, Ord) -- | Pure variant of 'getArguments'. Also allows to declare 'Modifier's. -- -- Does not throw any exceptions. parseArguments :: forall a . (Generic a, HasDatatypeInfo a, All2 Option (Code a)) => String -> [Modifier] -> [String] -> Result a parseArguments header (mkModifiers -> modifiers) args = case normalizedDatatypeInfo (Proxy :: Proxy a) of ADT typeName _ (constructorInfo :* Nil) -> case constructorInfo of (Record _ fields) -> processFields header modifiers args fields Constructor{} -> err typeName "constructors without field labels" Infix{} -> err typeName "infix constructors" ADT typeName _ Nil -> err typeName "empty data types" ADT typeName _ (_ :* _ :* _) -> err typeName "sum-types" Newtype _ _ (Record _ fields) -> processFields header modifiers args fields Newtype typeName _ (Constructor _) -> err typeName "constructors without field labels" where err typeName message = Errors ["getopt-generics doesn't support " ++ message ++ " (" ++ typeName ++ ")."] processFields :: forall a xs . (Generic a, Code a ~ '[xs], SingI xs, All Option xs) => String -> Modifiers -> [String] -> NP FieldInfo xs -> Result a processFields header modifiers args fields = helpWrapper header modifiers args fields $ fmap (to . SOP . Z) $ case getOpt Permute (mkOptDescrs modifiers fields) args of (options, arguments, parseErrors) -> let result :: Either [String] (NP I xs) = collectErrors $ project options (mkEmptyArguments fields) allErrors = parseErrors ++ map mkUnknownArgumentError arguments ++ ignoreRight result in case allErrors of [] -> result _ -> Left allErrors where mkUnknownArgumentError :: String -> String mkUnknownArgumentError arg = "unknown argument: " ++ arg ignoreRight :: Monoid e => Either e o -> e ignoreRight = either id (const mempty) mkOptDescrs :: forall xs . All Option xs => Modifiers -> NP FieldInfo xs -> [OptDescr (NS FieldState xs)] mkOptDescrs modifiers fields = map toOptDescr $ sumList $ npMap (mkOptDescr modifiers) fields newtype OptDescrE a = OptDescrE (OptDescr (FieldState a)) mkOptDescr :: forall a . Option a => Modifiers -> FieldInfo a -> OptDescrE a mkOptDescr modifiers (FieldInfo name) = OptDescrE $ Option (mkShortOptions modifiers name) [mkLongOption modifiers name] _toOption "" toOptDescr :: NS OptDescrE xs -> OptDescr (NS FieldState xs) toOptDescr (Z (OptDescrE a)) = fmap Z a toOptDescr (S a) = fmap S (toOptDescr a) mkEmptyArguments :: forall xs . (SingI xs, All Option xs) => NP FieldInfo xs -> NP FieldState xs mkEmptyArguments fields = case (sing :: Sing xs, fields) of (SNil, Nil) -> Nil (SCons, FieldInfo name :* r) -> _emptyOption name :* mkEmptyArguments r _ -> uninhabited "mkEmpty" -- * showing help? data HelpFlag = HelpFlag helpWrapper :: (All Option xs) => String -> Modifiers -> [String] -> NP FieldInfo xs -> Either [String] a -> Result a helpWrapper header modifiers args fields result = case getOpt Permute [helpOption] args of ([], _, _) -> case result of -- no help flag given Left errs -> Errors errs Right a -> Success a (HelpFlag : _, _, _) -> OutputAndExit $ stripTrailingSpaces $ usageInfo header $ toOptDescrUnit (mkOptDescrs modifiers fields) ++ toOptDescrUnit [helpOption] where helpOption :: OptDescr HelpFlag helpOption = Option ['h'] ["help"] (NoArg HelpFlag) "show help and exit" toOptDescrUnit :: [OptDescr a] -> [OptDescr ()] toOptDescrUnit = map (fmap (const ())) stripTrailingSpaces :: String -> String stripTrailingSpaces = unlines . map stripLines . lines where stripLines = reverse . dropWhile isSpace . reverse -- * helper functions for NS and NP collectErrors :: NP FieldState xs -> Either [String] (NP I xs) collectErrors np = case np of Nil -> Right Nil (a :* r) -> case (a, collectErrors r) of (FieldSuccess a, Right r) -> Right (I a :* r) (ParseErrors errs, r) -> Left (errs ++ either id (const []) r) (Unset err, r) -> Left (err : either id (const []) r) (FieldSuccess _, Left errs) -> Left errs npMap :: (All Option xs) => (forall a . Option a => f a -> g a) -> NP f xs -> NP g xs npMap _ Nil = Nil npMap f (a :* r) = f a :* npMap f r sumList :: NP f xs -> [NS f xs] sumList Nil = [] sumList (a :* r) = Z a : map S (sumList r) project :: (SingI xs, All Option xs) => [NS FieldState xs] -> NP FieldState xs -> NP FieldState xs project sums empty = foldl' inner empty 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 -- * possible field types data FieldState a = Unset String | ParseErrors [String] | FieldSuccess a deriving (Functor) -- | Type class for all allowed field types. -- -- Implementing custom instances to allow different types is possible. In the -- easiest case you just implement 'argumentType' and 'parseArgument' (the -- minimal complete definition). -- -- (Unfortunately implementing instances for lists or 'Maybe's of custom types -- is not very straightforward.) class 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 :: String -> FieldState a _emptyOption flagName = Unset ("missing option: --" ++ flagName ++ "=" ++ argumentType (Proxy :: Proxy a)) -- | This is meant to be an internal function. _accumulate :: a -> a -> a _accumulate _ x = x parseAsFieldState :: forall a . Option a => String -> FieldState a parseAsFieldState s = case parseArgument s of Just a -> FieldSuccess a Nothing -> ParseErrors $ pure $ "cannot parse as " ++ argumentType (Proxy :: Proxy a) ++ ": " ++ s combine :: Option a => FieldState a -> FieldState a -> FieldState a combine _ (Unset _) = impossible "combine" combine (ParseErrors e) (ParseErrors f) = ParseErrors (e ++ f) combine (ParseErrors e) _ = ParseErrors e combine (Unset _) x = x combine (FieldSuccess _) (ParseErrors e) = ParseErrors e combine (FieldSuccess a) (FieldSuccess b) = FieldSuccess (_accumulate a b) instance Option Bool where argumentType _ = "bool" parseArgument = impossible "Option.Bool.parseArguments" _toOption = NoArg (FieldSuccess True) _emptyOption _ = FieldSuccess False instance Option String where argumentType _ = "string" parseArgument = Just instance Option (Maybe String) where argumentType _ = "string (optional)" parseArgument = Just . Just _emptyOption _ = FieldSuccess Nothing instance Option [String] where argumentType _ = "string (multiple possible)" parseArgument = Just . pure _emptyOption _ = FieldSuccess [] _accumulate = (++) instance Option Int where argumentType _ = "integer" parseArgument = readMaybe instance Option (Maybe Int) where argumentType _ = "integer (optional)" parseArgument s = case readMaybe s of Just i -> Just (Just i) Nothing -> Nothing _emptyOption _ = FieldSuccess Nothing instance Option [Int] where argumentType _ = "integer (multiple possible)" parseArgument s = case readMaybe s of Just a -> Just [a] Nothing -> Nothing _emptyOption _ = FieldSuccess [] _accumulate = (++)