packages feed

getopt-generics 0.3 → 0.4

raw patch · 5 files changed

+258/−129 lines, 5 filesdep +QuickCheckPVP ok

version bump matches the API change (PVP)

Dependencies added: QuickCheck

API changes (from Hackage documentation)

- System.Console.GetOpt.Generics: instance Eq a => Eq (Result a)
- System.Console.GetOpt.Generics: instance Functor FieldState
- System.Console.GetOpt.Generics: instance Ord a => Ord (Result a)
- System.Console.GetOpt.Generics: instance Show a => Show (Result a)
+ System.Console.GetOpt.Generics: UseForPositionalArguments :: String -> Modifier
+ System.Console.GetOpt.Generics: instance Typeable FieldState
- System.Console.GetOpt.Generics: class Option a where _toOption = ReqArg parseAsFieldState (argumentType (Proxy :: Proxy a)) _emptyOption flagName = Unset ("missing option: --" ++ flagName ++ "=" ++ argumentType (Proxy :: Proxy a)) _accumulate _ x = x
+ System.Console.GetOpt.Generics: class Typeable a => Option a where _toOption = ReqArg parseAsFieldState (argumentType (Proxy :: Proxy a)) _emptyOption flagName = Unset ("missing option: --" ++ flagName ++ "=" ++ argumentType (Proxy :: Proxy a)) _accumulate _ x = x

Files

getopt-generics.cabal view
@@ -1,6 +1,6 @@ -- This file has been generated from package.yaml by Cabalize. name: getopt-generics-version: 0.3+version: 0.4 synopsis: Simple command line argument parsing description:   "getopt-generics" tries to make it very simple to create command line argument parsers. An introductory example can be found in the <https://github.com/zalora/getopt-generics#getopt-generics README>.@@ -24,6 +24,7 @@   other-modules:       System.Console.GetOpt.Generics.Internal       System.Console.GetOpt.Generics.Modifier+      System.Console.GetOpt.Generics.Result   build-depends:       base == 4.*     , base-compat >= 0.7@@ -37,7 +38,8 @@   hs-source-dirs: src, test, examples   main-is: Spec.hs   build-depends:-      base == 4.*+      QuickCheck+    , base == 4.*     , base-compat >= 0.7     , generics-sop     , hspec
src/System/Console/GetOpt/Generics.hs view
@@ -1,5 +1,6 @@ {-# LANGUAGE ConstraintKinds       #-} {-# LANGUAGE DataKinds             #-}+{-# LANGUAGE DeriveDataTypeable    #-} {-# LANGUAGE DeriveFunctor         #-} {-# LANGUAGE DeriveGeneric         #-} {-# LANGUAGE FlexibleContexts      #-}@@ -9,6 +10,7 @@ {-# LANGUAGE RankNTypes            #-} {-# LANGUAGE ScopedTypeVariables   #-} {-# LANGUAGE TypeFamilies          #-}+{-# LANGUAGE TypeOperators         #-} {-# LANGUAGE TypeSynonymInstances  #-} {-# LANGUAGE ViewPatterns          #-} @@ -35,8 +37,11 @@ import           Prelude () import           Prelude.Compat +import           Control.Monad (when) import           Data.Char import           Data.List+import           Data.Maybe+import           Data.Typeable import           Generics.SOP import           System.Console.GetOpt.Compat import           System.Environment@@ -46,6 +51,7 @@  import           System.Console.GetOpt.Generics.Modifier import           System.Console.GetOpt.Generics.Internal+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.@@ -76,111 +82,128 @@       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-    --   <https://github.com/zalora/getopt-generics#getopt-generics README> 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"+parseArguments header modifiersList args = do+    (modifiers, datatypeInfo) <- (,) <$>+      mkModifiers modifiersList <*>+      normalizedDatatypeInfo (Proxy :: Proxy a)+    case datatypeInfo 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 ++ ")."]+      Errors ["getopt-generics doesn't support " ++ message +++              " (" ++ typeName ++ ")."] -processFields :: forall a xs . (Generic a, Code a ~ '[xs], SingI xs, All Option xs) =>+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+    mkInitialFieldStates modifiers fields >>= \ initialFieldStates ->++    showHelp *>++    let (options, arguments, parseErrors) =+          getOpt Permute (mkOptDescrs modifiers fields) args+    in++    reportParseErrors parseErrors *>++    reportInvalidPositionalArguments arguments *>++    produceResult initialFieldStates options arguments   where-    mkUnknownArgumentError :: String -> String-    mkUnknownArgumentError arg = "unknown argument: " ++ arg+    showHelp :: Result ()+    showHelp = helpWrapper header modifiers args fields -    ignoreRight :: Monoid e => Either e o -> e-    ignoreRight = either id (const mempty)+    reportParseErrors :: [String] -> Result ()+    reportParseErrors parseErrors = case parseErrors of+      [] -> pure ()+      errs -> Errors errs +    reportInvalidPositionalArguments :: [String] -> Result ()+    reportInvalidPositionalArguments arguments =+      when (not $ hasPositionalArgumentsField modifiers) $+        case arguments of+          [] -> pure ()+          _ -> Errors (map ("unknown argument: " ++) arguments)++    produceResult :: NP FieldState xs -> [NS FieldState xs] -> [String] -> Result a+    produceResult initialFieldStates options arguments =+      (to . SOP . Z) <$>+        collectResult arguments (project options initialFieldStates)++ mkOptDescrs :: forall xs . All Option xs =>   Modifiers -> NP FieldInfo xs -> [OptDescr (NS FieldState xs)] mkOptDescrs modifiers fields =-  map toOptDescr $ sumList $ npMap (mkOptDescr modifiers) fields+  mapMaybe toOptDescr $ sumList $ npMap (mkOptDescr modifiers) fields -newtype OptDescrE a = OptDescrE (OptDescr (FieldState a))+newtype OptDescrE a = OptDescrE (Maybe (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-    ""+  if isPositionalArgumentsField modifiers name+    then Nothing+    else Just $ 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)+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) -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+mkInitialFieldStates :: forall xs . (SingI xs, All Option xs) =>+  Modifiers -> NP FieldInfo xs -> Result (NP FieldState xs)+mkInitialFieldStates modifiers fields = case (sing :: Sing xs, fields) of+  (SNil, Nil) -> return Nil   (SCons, FieldInfo name :* r) ->-    _emptyOption name :* mkEmptyArguments r+    (:*) <$> inner name <*> mkInitialFieldStates modifiers r   _ -> uninhabited "mkEmpty" + where+  inner :: forall x . Option x => String -> 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)) ->+        return $ id' PositionalArguments+      Nothing -> Errors+        ["UseForPositionalArguments can only be used " +++         "for fields of type [String] not " +++         show (typeOf (impossible "mkInitialFieldStates" :: x))]+    else return $ _emptyOption name + -- * 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 =+  String -> Modifiers -> [String] -> NP FieldInfo xs -> Result ()+helpWrapper header modifiers args fields =     case getOpt Permute [helpOption] args of-      ([], _, _) -> case result of+      ([], _, _) -> return ()         -- no help flag given-        Left errs -> Errors errs-        Right a -> Success a       (HelpFlag : _, _, _) -> OutputAndExit $         stripTrailingSpaces $         usageInfo header $@@ -200,14 +223,16 @@  -- * 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+collectResult :: [String] -> NP FieldState xs -> Result (NP I xs)+collectResult positionalArguments np = case np of+  Nil -> Success Nil+  (a :* r) -> (:*) <$> inner a <*> collectResult positionalArguments r+  where+    inner :: FieldState a -> Result (I a)+    inner (FieldSuccess v) = Success (I v)+    inner (ParseErrors errs) = Errors errs+    inner (Unset err) = Errors [err]+    inner PositionalArguments = Success (I positionalArguments)  npMap :: (All Option xs) => (forall a . Option a => f a -> g a) -> NP f xs -> NP g xs npMap _ Nil = Nil@@ -236,11 +261,12 @@  -- * possible field types -data FieldState a-  = Unset String-  | ParseErrors [String]-  | FieldSuccess a-  deriving (Functor)+data FieldState a where+  Unset :: String -> FieldState a+  ParseErrors :: [String] -> FieldState a+  FieldSuccess :: a -> FieldState a+  PositionalArguments :: FieldState [String]+  deriving (Typeable)  -- | Type class for all allowed field types. --@@ -250,7 +276,7 @@ -- --   (Unfortunately implementing instances for lists or 'Maybe's of custom types --   is not very straightforward.)-class Option a where+class Typeable a => Option a where   {-# MINIMAL argumentType, parseArgument #-}   -- | Name of the argument type, e.g. "bool" or "integer".   argumentType :: Proxy a -> String@@ -279,11 +305,13 @@  combine :: Option a => FieldState a -> FieldState a -> FieldState a combine _ (Unset _) = impossible "combine"+combine _ PositionalArguments = 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)+combine PositionalArguments _ = PositionalArguments  instance Option Bool where   argumentType _ = "bool"
src/System/Console/GetOpt/Generics/Internal.hs view
@@ -1,31 +1,63 @@-{-# LANGUAGE ConstraintKinds     #-}-{-# LANGUAGE GADTs               #-}-{-# LANGUAGE RankNTypes          #-}-{-# LANGUAGE TypeFamilies        #-}+{-# LANGUAGE ConstraintKinds #-}+{-# LANGUAGE GADTs           #-}+{-# LANGUAGE RankNTypes      #-}+{-# LANGUAGE TypeFamilies    #-}  module System.Console.GetOpt.Generics.Internal where +import           Control.Applicative import           Data.Char import           Generics.SOP +import           System.Console.GetOpt.Generics.Result+ normalizedDatatypeInfo :: (HasDatatypeInfo a, Code a ~ xss, SingI xss) =>-  Proxy a -> DatatypeInfo xss-normalizedDatatypeInfo p = mapFieldInfo (\ (FieldInfo s) -> FieldInfo (slugify s)) (datatypeInfo p)+  Proxy a -> Result (DatatypeInfo xss)+normalizedDatatypeInfo p =+  mapFieldInfoM+    (\ (FieldInfo s) -> FieldInfo <$> normalizeFieldName s)+    (datatypeInfo p) -mapFieldInfo :: (SingI xss) =>-  (forall b . FieldInfo b -> FieldInfo b) -> DatatypeInfo xss -> DatatypeInfo xss-mapFieldInfo f info = case info of-    (ADT mod name constructors) -> ADT mod name (hliftA (mapSingleCons f) constructors)-    (Newtype mod name constructor) -> Newtype mod name (mapSingleCons f constructor)+mapFieldInfoM :: (SingI xss, Applicative m) =>+     (forall a . FieldInfo a -> m (FieldInfo a))+  -> DatatypeInfo xss -> m (DatatypeInfo xss)+mapFieldInfoM f info = case info of+    (ADT mod name constructors) ->+      ADT mod name <$> (mapConsInfo (mapSingleCons f) constructors)+    (Newtype mod name constructor) ->+      Newtype mod name <$> (mapSingleCons f constructor)   where-    mapSingleCons :: (forall b . FieldInfo b -> FieldInfo b) -> ConstructorInfo xs -> ConstructorInfo xs+    mapSingleCons :: forall m xs . (Applicative m) =>+         (forall a . FieldInfo a -> m (FieldInfo a))+      -> ConstructorInfo xs -> m (ConstructorInfo xs)     mapSingleCons f c = case c of-      (Record name fields) -> Record name (hliftA f fields)-      cons@Infix{} -> cons-      cons@Constructor{} -> cons+      (Record name fields) -> Record name <$> (mapFieldInfo f fields)+      cons@Infix{} -> pure cons+      cons@Constructor{} -> pure cons -slugify :: String -> String-slugify [] = []-slugify (x : xs)-  | isUpper x = '-' : toLower x : slugify xs-  | otherwise = x : slugify xs+    mapConsInfo :: forall m xs . Applicative m =>+         (forall a . ConstructorInfo a -> m (ConstructorInfo a))+      -> NP ConstructorInfo xs -> m (NP ConstructorInfo xs)+    mapConsInfo _ Nil = pure Nil+    mapConsInfo f (a :* r) = (:*) <$> f a <*> mapConsInfo f r++    mapFieldInfo :: forall m xs . Applicative m =>+         (forall a . FieldInfo a -> m (FieldInfo a))+      -> NP FieldInfo xs -> m (NP FieldInfo xs)+    mapFieldInfo _ Nil = pure Nil+    mapFieldInfo f (a :* r) = (:*) <$> f a <*> mapFieldInfo f r++normalizeFieldName :: String -> Result String+normalizeFieldName s =+    let normalized = dropWhile (== '-') $+          filter (\ c -> (isAscii c && isAlpha c) || (c == '-')) s+    in case normalized of+      "" -> Errors ["unsupported field name: " ++ s]+      x -> Success $ slugify x+  where+    slugify (a : r)+      | isUpper a = slugify (toLower a : r)+    slugify (a : b : r)+      | isUpper b = a : '-' : slugify (toLower b : r)+      | otherwise = a : slugify (b : r)+    slugify x = x
src/System/Console/GetOpt/Generics/Modifier.hs view
@@ -1,5 +1,6 @@ {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE GADTs            #-}+{-# LANGUAGE TupleSections    #-}  module System.Console.GetOpt.Generics.Modifier (   Modifier(..),@@ -7,18 +8,24 @@   mkModifiers,   mkShortOptions,   mkLongOption,+  hasPositionalArgumentsField,+  isPositionalArgumentsField,    deriveShortOptions,    -- exported for testing+  mkShortModifiers,   insertWith,  ) where -import           Data.List                               (foldl', isPrefixOf)+import           Control.Applicative+import           Control.Monad+import           Data.Char import           Data.Maybe import           Generics.SOP  import           System.Console.GetOpt.Generics.Internal+import           System.Console.GetOpt.Generics.Result  -- | 'Modifier's can be used to customize the command line parser. data Modifier@@ -28,32 +35,47 @@   | RenameOption String String     -- ^ @RenameOption fieldName customName@ renames the option generated     --   through the @fieldName@ by @customName@.+  | UseForPositionalArguments String   deriving (Show, Eq, Ord)  data Modifiers = Modifiers {   _shortOptions :: [(String, [Char])],-  _renamings :: [(String, String)]+  _renamings :: [(String, String)],+  positionalArgumentsField :: Maybe String  }+ deriving (Show, Eq, Ord) -mkModifiers :: [Modifier] -> Modifiers-mkModifiers = foldl' inner (Modifiers [] [])+mkModifiers :: [Modifier] -> Result Modifiers+mkModifiers = foldM inner (Modifiers [] [] Nothing)   where-    inner :: Modifiers -> Modifier -> Modifiers-    inner (Modifiers shorts renamings) (AddShortOption option short) =-      Modifiers-        (insertWith (++) (slugify option) [short] shorts)-        renamings-    inner (Modifiers shorts renamings) (RenameOption from to) =-      Modifiers shorts (insert (slugify from) to renamings)+    inner :: Modifiers -> Modifier -> Result Modifiers+    inner (Modifiers shorts renamings args) (AddShortOption option short) = do+      normalized <- normalizeFieldName option+      return $ Modifiers+        (insertWith (++) normalized [short] shorts)+        renamings args+    inner (Modifiers shorts renamings args) (RenameOption from to) = do+      fromNormalized <- normalizeFieldName from+      return $ Modifiers shorts (insert fromNormalized to renamings) args+    inner (Modifiers shorts renamings _) (UseForPositionalArguments option) = do+      normalized <- normalizeFieldName option+      return $ Modifiers shorts renamings (Just normalized)  mkShortOptions :: Modifiers -> String -> [Char]-mkShortOptions (Modifiers shortMap _) option =+mkShortOptions (Modifiers shortMap _ _) option =     fromMaybe [] (lookup option shortMap)  mkLongOption :: Modifiers -> String -> String-mkLongOption (Modifiers _ renamings) option =+mkLongOption (Modifiers _ renamings _) option =   fromMaybe option (lookup option renamings) +hasPositionalArgumentsField :: Modifiers -> Bool+hasPositionalArgumentsField = isJust . positionalArgumentsField++isPositionalArgumentsField :: Modifiers -> String -> Bool+isPositionalArgumentsField modifiers field =+  Just field == positionalArgumentsField modifiers+ -- * deriving Modifiers  -- | Derives 'AddShortOption's for all fields of the datatype that start with a@@ -65,7 +87,7 @@  flags :: (SingI (Code a), HasDatatypeInfo a) =>   Proxy a -> [String]-flags proxy = case normalizedDatatypeInfo proxy of+flags proxy = case datatypeInfo proxy of     ADT _ _ ci -> fromNPConstructorInfo ci     Newtype _ _ ci -> fromConstructorInfo ci   where@@ -86,14 +108,20 @@  mkShortModifiers :: [String] -> [Modifier] mkShortModifiers fields =-    mapMaybe inner fields+    let withShorts = mapMaybe (\ field -> (field, ) <$> toShort field) fields+        allShorts = map snd withShorts+        isUnique c = case filter (== c) allShorts of+          [_] -> True+          _ -> False+    in (flip mapMaybe) withShorts $ \ (field, short) ->+          if isUnique short+            then Just (AddShortOption field short)+            else Nothing   where-    inner :: String -> Maybe Modifier-    inner field@(short : _) =-      case filter ([short] `isPrefixOf`) fields of-        [_] -> Just $ AddShortOption field short-        _ -> Nothing-    inner [] = Nothing+    toShort :: String -> Maybe Char+    toShort s = case dropWhile (\ c -> not (isAscii c && isAlpha c)) s of+      [] -> Nothing+      (a : _) -> Just (toLower a)  -- * list utils to replace Data.Map 
+ src/System/Console/GetOpt/Generics/Result.hs view
@@ -0,0 +1,39 @@+{-# LANGUAGE DeriveFunctor #-}++module System.Console.GetOpt.Generics.Result where++import           Control.Applicative++-- | 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+    --   <https://github.com/zalora/getopt-generics#getopt-generics README> for+    --   details.+  | OutputAndExit String+    -- ^ The CLI was used with @--help@. The 'Result' contains the help message.+  deriving (Show, Eq, Ord, Functor)++instance Applicative Result where+  pure = Success+  OutputAndExit message <*> _ = OutputAndExit message+  _ <*> OutputAndExit message = OutputAndExit message+  Success f <*> Success x = Success (f x)+  Errors a <*> Errors b = Errors (a ++ b)+  Errors errs <*> Success _ = Errors errs+  Success _ <*> Errors errs = Errors errs++instance Monad Result where+  return = pure+  Success a >>= b = b a+  Errors errs >>= _ = Errors errs+  OutputAndExit message >>= _ = OutputAndExit message++  (>>) = (*>)