harg 0.4.1.0 → 0.4.2.0
raw patch · 29 files changed
+1010/−1677 lines, 29 filesPVP: major bump suggested
API removals or changes: PVP suggests a major version bump
API changes (from Hackage documentation)
- Options.Harg: toOpt :: IsOpt o attr => o attr a -> Opt a
- Options.Harg.Cmdline: mkParser :: Compose Maybe f a -> Compose Opt f a -> Compose Parser f a
- Options.Harg.Cmdline: toArgumentParser :: Compose Maybe f a -> Compose Opt f a -> Compose Parser f a
- Options.Harg.Cmdline: toFlagParser :: Compose Maybe f a -> Compose Opt f a -> f a -> Compose Parser f a
- Options.Harg.Cmdline: toOptionParser :: Compose Maybe f a -> Compose Opt f a -> Compose Parser f a
- Options.Harg.Construct: class HasDefaultStr o (attr :: [OptAttr])
- Options.Harg.Construct: class HasDefaultVal o (attr :: [OptAttr])
- Options.Harg.Construct: class HasEnvVar o (attr :: [OptAttr])
- Options.Harg.Construct: class HasHelp o (attr :: [OptAttr])
- Options.Harg.Construct: class HasLong o (attr :: [OptAttr])
- Options.Harg.Construct: class HasMetavar o (attr :: [OptAttr])
- Options.Harg.Construct: class HasOptional o (attr :: [OptAttr])
- Options.Harg.Construct: class HasRequired o (attr :: [OptAttr])
- Options.Harg.Construct: class HasShort o (attr :: [OptAttr])
- Options.Harg.Construct: class IsOpt o (attr :: [OptAttr])
- Options.Harg.Construct: toOpt :: IsOpt o attr => o attr a -> Opt a
- Options.Harg.Construct: type DuplicateAttrErr attr = QuoteSym attr :<>: 'Text " is already specified."
- Options.Harg.Construct: type DuplicateAttrMultipleErr attr rest = QuoteSym attr :<>: 'Text (CommaSep rest) :<>: 'Text " has already been specified."
- Options.Harg.Construct: type IncompatibleAttrsErr l r = QuoteSym l :<>: 'Text " and " :<>: QuoteSym r :<>: 'Text " cannot be mixed in an option definition."
- Options.Harg.Construct: type QuoteSym (s :: Symbol) = 'Text "`" :<>: 'Text s :<>: 'Text "`"
- Options.Harg.Construct: type family CommaSep' (s :: Symbol) (xs :: [Symbol]) :: Symbol
- Options.Harg.Het.Proofs: class ProofNil xs
- Options.Harg.Het.Proofs: proofNil :: ProofNil xs => (xs ++ '[]) :~~: xs
- Options.Harg.Het.Variant: class FromVariantF xs result f
- Options.Harg.Het.Variant: class IgnoreF (args :: [(Type -> Type) -> Type]) result f
- Options.Harg.Het.Variant: ignoreF :: IgnoreF args result f => result -> FoldSignatureF args result f
- Options.Harg.Het.Variant: type family FoldSignatureF (xs :: [(Type -> Type) -> Type]) r f
- Options.Harg.Nested: type family Nest (a :: Type) (f :: Type -> Type) = (res :: Type) | res -> a
- Options.Harg.Operations: execParser :: HargCtx -> Parser a -> IO a
- Options.Harg.Operations: execParserPure :: [String] -> Parser a -> ParserResult a
- Options.Harg.Operations: failParser :: Parser a -> [SourceRunError] -> IO a
- Options.Harg.Pretty: ppEnvVar :: Maybe String -> String
- Options.Harg.Pretty: ppSource :: String -> String
- Options.Harg.Sources.DefaultStr: DefaultStrSourceVal :: DefaultStrSourceVal
- Options.Harg.Sources.DefaultStr: data DefaultStrSourceVal
- Options.Harg.Sources.DefaultStr: runDefaultStrSource :: forall a f. (FunctorB a, TraversableB a, Applicative f) => a (Compose Opt f) -> Either SourceRunError (a (Compose SourceRunResult f))
- Options.Harg.Sources.Env: lookupEnv :: Environment -> String -> Maybe String
- Options.Harg.Sources.Env: runEnvVarSource :: forall a f. (FunctorB a, TraversableB a, Applicative f) => Environment -> a (Compose Opt f) -> Either SourceRunError (a (Compose SourceRunResult f))
- Options.Harg.Sources.JSON: JSONSourceNotRequired :: JSONSourceVal
- Options.Harg.Sources.JSON: JSONSourceVal :: ByteString -> JSONSourceVal
- Options.Harg.Sources.JSON: data JSONSourceVal
- Options.Harg.Sources.JSON: runJSONSource :: forall a f. (FunctorB a, FromJSON (a Maybe), Applicative f) => ByteString -> a (Compose Opt f) -> Either SourceRunError (a (Compose SourceRunResult f))
- Options.Harg.Sources.NoSource: NoSource :: NoSource
- Options.Harg.Sources.YAML: YAMLSourceNotRequired :: YAMLSourceVal
- Options.Harg.Sources.YAML: YAMLSourceVal :: ByteString -> YAMLSourceVal
- Options.Harg.Sources.YAML: data YAMLSourceVal
- Options.Harg.Sources.YAML: runYAMLSource :: forall a f. (FunctorB a, FromJSON (a Maybe), Applicative f) => ByteString -> a (Compose Opt f) -> Either SourceRunError (a (Compose SourceRunResult f))
- Options.Harg.Subcommands: class ExplSubcommands (n :: Nat) (ts :: [Symbol]) (xs :: [(Type -> Type) -> Type]) (acc :: [(Type -> Type) -> Type])
- Options.Harg.Subcommands: explMapSubcommand :: (ExplSubcommands n ts xs acc, All (RunSource s) xs, Applicative f) => SNat n -> s -> AssocListF ts xs (Compose Opt f) -> ([SourceRunError], [Mod CommandFields (VariantF (acc ++ xs) f)])
- Options.Harg.Types: type Args = [String]
- Options.Harg.Util: printErrAndExit :: forall a. String -> IO a
- Options.Harg.Util: readFileWith :: (FilePath -> IO a) -> FilePath -> IO a
+ Options.Harg: execCommandsWithCtx :: forall c ts xs. (TraversableB (VariantF xs), TraversableB c, ProductB c, Subcommands ts xs, GetSource c Identity, All (RunSource (SourceVal (c :* HiddenSources))) xs, All (RunSource ()) xs, MapAssocList xs) => HargCtx -> c Opt -> AssocListF ts xs Opt -> IO (VariantF xs Identity)
+ Options.Harg: execCommandsWithCtxDef :: forall ts xs. (TraversableB (VariantF xs), Subcommands ts xs, All (RunSource (SourceVal (DefaultSources :* HiddenSources))) xs, All (RunSource ()) xs, MapAssocList xs) => HargCtx -> AssocListF ts xs Opt -> IO (VariantF xs Identity)
+ Options.Harg: execOptWithCtx :: forall c a. (TraversableB a, ProductB a, TraversableB c, ProductB c, GetSource c Identity, RunSource (SourceVal c) a) => HargCtx -> c Opt -> a Opt -> IO (a Identity)
+ Options.Harg: execOptWithCtxDef :: forall a. (TraversableB a, ProductB a) => HargCtx -> a Opt -> IO (a Identity)
- Options.Harg.Het.All: type family AllF (c :: k -> Constraint) (xs :: [(Type -> Type) -> Type]) (f :: Type -> Type) :: Constraint
+ Options.Harg.Het.All: type family All (c :: k -> Constraint) (xs :: [k]) :: Constraint
Files
- CHANGELOG.md +5/−0
- README.lhs +0/−812
- README.md +10/−790
- docs/docs.lhs +799/−0
- harg.cabal +4/−3
- src/Options/Harg.hs +5/−2
- src/Options/Harg/Cmdline.hs +4/−2
- src/Options/Harg/Config.hs +7/−4
- src/Options/Harg/Construct.hs +21/−1
- src/Options/Harg/Het/All.hs +4/−2
- src/Options/Harg/Het/HList.hs +7/−1
- src/Options/Harg/Het/Nat.hs +4/−1
- src/Options/Harg/Het/Prod.hs +4/−1
- src/Options/Harg/Het/Proofs.hs +5/−1
- src/Options/Harg/Het/Variant.hs +10/−1
- src/Options/Harg/Nested.hs +6/−1
- src/Options/Harg/Operations.hs +28/−19
- src/Options/Harg/Pretty.hs +4/−1
- src/Options/Harg/Single.hs +5/−2
- src/Options/Harg/Sources.hs +14/−10
- src/Options/Harg/Sources/DefaultStr.hs +3/−1
- src/Options/Harg/Sources/Env.hs +4/−1
- src/Options/Harg/Sources/JSON.hs +5/−3
- src/Options/Harg/Sources/NoSource.hs +5/−3
- src/Options/Harg/Sources/Types.hs +10/−3
- src/Options/Harg/Sources/YAML.hs +4/−2
- src/Options/Harg/Subcommands.hs +9/−7
- src/Options/Harg/Types.hs +16/−1
- src/Options/Harg/Util.hs +8/−2
CHANGELOG.md view
@@ -1,5 +1,10 @@ # Changelog for harg +## 0.4.2.0 [2020.01.27]++- Add explicit export and import lists+- Some documentation fixes+ ## 0.4.1.0 [2019.12.22] - Parsers now stop immediately if a source error is encountered
− README.lhs
@@ -1,812 +0,0 @@-# `harg`--[](https://travis-ci.org/alexpeits/harg)-[](https://hackage.haskell.org/package/harg)--`harg` is a library for configuring programs by scanning command line arguments,-environment variables, default values and more. Under the hood, it uses a subset-of [`optparse-applicative`](https://hackage.haskell.org/package/optparse-applicative)-to expose regular arguments, switch arguments and subcommands. The library-relies heavily on the use of higher kinded data (HKD) thanks to the-[`barbies`](https://hackage.haskell.org/package/barbies) library. Using-[`higgledy`](https://hackage.haskell.org/package/higgledy) also allows to have-significantly less boilerplate code.--The main goal while developing `harg` was to not have to go through the usual-pattern of manually `mappend`ing the results of command line parsing, env vars-and defaults.--# Usage--tl;dr: Take a look at the [example](Example.hs).--Here are some different usage scenarios. Let's first enable some language-extensions and add some imports:--``` haskell-{-# LANGUAGE DataKinds #-}-{-# LANGUAGE DeriveAnyClass #-}-{-# LANGUAGE DeriveGeneric #-}-{-# LANGUAGE FlexibleInstances #-}-{-# LANGUAGE GADTs #-}-{-# LANGUAGE KindSignatures #-}-{-# LANGUAGE StandaloneDeriving #-}-{-# LANGUAGE TypeApplications #-}-{-# LANGUAGE TypeOperators #-}--import Data.Functor.Identity (Identity (..))-import Data.Kind (Type)-import GHC.Generics (Generic)--import qualified Data.Barbie as B-import Data.Aeson (FromJSON)-import Data.Generic.HKD (HKD, build, construct)--import Options.Harg--main :: IO ()-main = putStrLn "this is a literate haskell file"-```--## One flat (non-nested) datatype--The easiest scenario is when the target configuration type is one single record-with no levels of nesting:--``` haskell-data FlatConfig- = FlatConfig- { _fcDbHost :: String- , _fcDbPort :: Int- , _fcDir :: String- , _fcLog :: Bool -- whether to log or not- }- deriving (Show, Generic)-```--(The `Generic` instance is required for section `3` later on)--Let's first create the `Opt`s for each value in `FlatConfig`. `Opt` is the-description for each component of the configuration.--``` haskell-dbHostOpt :: Opt String-dbHostOpt- = option strParser- ( long "host"- . short 'h'- . metavar "DB_HOST"- . help "The database host"- )--dbPortOpt :: Opt Int-dbPortOpt- = option readParser- ( long "port"- . help "The database port"- . envVar "DB_PORT"- . defaultVal 5432- )--dirOpt :: Opt String-dirOpt- = argument strParser- ( help "Some directory"- . defaultVal "/home/user/something"- )--logOpt :: Opt Bool-logOpt- = switch- ( long "log"- . help "Whether to log or not"- )-```--Here, we use `option` to define a command line argument that expects a value-after it, `argument` to define a standalone argument, not prefixed by a long or-short indicator, and `switch` to define a boolean command line flag that, if-present, sets the target value to `True`. The `opt*` functions (here applied-using `&` to make things look more declarative) modify the option configuration.-`help` adds help text, `defaultVal` adds a default value, `short` adds a short-command line option as an alternative to the long one (the string after `option`-or `switch`), `envVar` sets the associated environment variable and `metavar`-sets the metavariable to be shown in the help text generated by-`optparse-applicative`.---The first argument (`strParser` or `readParser`) is the parser for the argument,-be it from the command line or from an environment variable. The type of this-function should be `String -> Either String a`, which produces an error message-or the parsed value. `strParser` is equivalent to `pure` and always succeeds.-`readParser` requires the type to have a `Read` constraint. In order to use it-with newtypes that wrap a type that has a `Read` constraint, using the `Functor`-instance for `Opt` should be sufficient. E.g. for the newtype:--``` haskell-newtype Port = Port Int-```--we can define the following option:--``` haskell-dbPortOpt' :: Opt Port-dbPortOpt'- = Port <$> dbPortOpt-```--Of course, any user-defined function works as well. In addition, to use a-function of type `String -> Maybe a` use `parseWith`, which runs the parser and-in case of failure uses a default error message. For example, `readParser` is-defined as `parseWith readMaybe`.--Finally, an optional option with type `a` can be specified by setting its type-to `Maybe a`. The declaration is exactly the same as it would be for `a`, and-adding `optional` to the modifiers turns turns the parser from `String -> Either-String a` to `String -> Either String (Maybe a)` but without using the `Read`-instance for `Maybe`:--``` haskell ignore-someOpt :: Opt (Maybe Int)-someOpt- = option readParser- ( long "something"- . optional- )-```--Note that `optional` can't be used with `defaultVal`. Using them together raises-a type error at compile time, to ensure there's no ambiguous behaviour (e.g. the-order of declaration of modifiers should not influence the resulting option).--There are 3 ways to configure this datatype.--### 1. Using a `barbie` type--`barbie` types are types of kind `(Type -> Type) -> Type`. The `barbie` type for-`FlatConfig` looks like this:--``` haskell-data FlatConfigB f- = FlatConfigB- { _fcDbHostB :: f String- , _fcDbPortB :: f Int- , _fcDirB :: f String- , _fcLogB :: f Bool- }- deriving (Generic, B.FunctorB, B.TraversableB, B.ProductB)-```--I also derived some required instances that come from the `barbies` package.-These instances allow us to change the `f` (`bmap` from `FunctorB`) and traverse-all types in the record producing side effects (`btraverse` from-`TraversableB`).--Now let's define the value of this datatype, which holds our option-configuration. The type constructor needed for the options is `Opt`:--``` haskell-flatConfigOpt1 :: FlatConfigB Opt-flatConfigOpt1- = FlatConfigB dbHostOpt dbPortOpt dirOpt logOpt-```--Because `dbHostOpt`, `dbPortOpt` and `logOpt` all have type `Opt <actual type>`,-`flatConfigOpt1` has the correct type according to `FlatConfigB Opt`.--Now to actually run things:--``` haskell-getFlatConfig1 :: IO ()-getFlatConfig1 = do- FlatConfigB host port dir log <- execOptDef flatConfigOpt1- print $ runIdentity $- FlatConfig- <$> host- <*> port- <*> dir- <*> log-```--`execOpt` returns an `Identity x` where `x` is the type of the options we are-configuring, in this case `FlatConfigB`. Here, we pattern match on the-barbie-type, and then use the `Applicative` instance of `Identity` to get back-an `Identity FlatConfig`.--This is still a bit boilerplate-y. Let's look at another way.--### 2. Using a product type--Looking at `FlatConfigB`, it's only used because of it's `barbie`-like-capabilities. Other than that it's just a simple product type with the-additional `f` before all its sub-types.--`harg` defines a type almost similar to `Product` (from `Data.Functor.Product`),-which works in a similar fashion as servant's `:<|>` type. This type is defined-in `Options.Harg.Het.Prod` and is called `:*` (the `*` stands for product). This-type stores barbie-like types and also keeps the `f` handy: `data (a :* b) f = a-f :* b f`. This is also easily made an instance of `Generic`, `FunctorB`,-`TraversableB` and `ProductB`. With all that, let's rewrite the options value-and the function to get the configuration:--``` haskell-flatConfigOpt2- :: (Single String :* Single Int :* Single String :* Single Bool) Opt-flatConfigOpt2- = single dbHostOpt :* single dbPortOpt :* single dirOpt :* single logOpt--getFlatConfig2 :: IO ()-getFlatConfig2 = do- host :* port :* dir :* log <- execOptDef flatConfigOpt2- print $ runIdentity $- FlatConfig- <$> getSingle host- <*> getSingle port- <*> getSingle dir- <*> getSingle log-```--This looks aufully similar to the previous version, but without having to write-another datatype and derive all the instances. `:*` is both a type-level-constructor and a value-level function that acts like list's `:`. It is also-right-associative, so for example `a :* b :* c` is the same as `a :* (b :* c)`.--The `Single` type constructor is used when talking about a single value, rather-than a nested datatype. `Single a f` is a simple newtype over `f a`. The reason-for using that is simply to switch the order of application, so that we can-later apply the `f` (here `Opt`) to the compound type (`:*`). This makes type-definitions look more similar to datatype definitions:--``` haskell-type FlatConfigOpt2- = Single String- :* Single Int- :* Single Bool-```--In addition, `single` is used to wrap an `f a` into a `Single a f`, and-`getSingle` is used to unwrap it. Later on we'll see how to construct nested-configurations using `Nested`.--However, the real value when having flat datatypes comes from the ability to use-`higgledy`.--### 3. Using `HKD` from `higgledy`--``` haskell-flatConfigOpt3 :: HKD FlatConfig Opt-flatConfigOpt3- = build @FlatConfig dbHostOpt dbPortOpt dirOpt logOpt--getFlatConfig3 :: IO ()-getFlatConfig3 = do- result <- execOptDef flatConfigOpt3- print $ runIdentity (construct result)-```--This is the most straightforward way to work with flat configuration types. The-`build` function takes as arguments the options (`Opt a` where `a` is each type-in `FlatConfig`) **in the order they appear in the datatype**, and returns the-generic representation of a type that's exactly the same as `FlatConfigB`. This-means that we get all the `barbie` instances for free.--To go back from the `HKD` representation of a datatype to the base one, we use-`construct`. `construct` uses the applicative instance of the `f` which wraps-each type in `FlatConfig` to give back an `f FlatConfig` (in our case an-`Identity FlatConfig`).--## Nested datatypes--Let's say now that we have these two datatypes:--``` haskell-data DbConfig- = DbConfig- { _dcHost :: String- , _dcPort :: Int- }- deriving (Show, Generic)--data ServiceConfig- = ServiceConfig- { _scPort :: Int- , _scLog :: Bool- }- deriving (Show, Generic)-```--And the datatype to be configured is this:--``` haskell-data Config- = Config- { _cDb :: DbConfig- , _cService :: ServiceConfig- , _cDir :: String- }- deriving (Show, Generic)-```--And a new option required for the service port:--``` haskell-portOpt :: Opt Int-portOpt- = option readParser- ( long "port"- . help "The service port"- . defaultVal 8080- )-```--Again, there are several ways to configure these options.--### 1. Using `barbie` types--Since we now have 3 types, there's a bit more boilerplate to write:--``` haskell-data ConfigB f- = ConfigB- { _cDbB :: DbConfigB f- , _cServiceB :: ServiceConfigB f- , _cDirB :: f String- }- deriving (Generic, B.FunctorB, B.TraversableB, B.ProductB)--data DbConfigB f- = DbConfigB- { _dcHostB :: f String- , _dcPortB :: f Int- }- deriving (Generic, B.FunctorB, B.TraversableB, B.ProductB)--data ServiceConfigB f- = ServiceConfigB- { _scPortB :: f Int- , _scLogB :: f Bool- }- deriving (Generic, B.FunctorB, B.TraversableB, B.ProductB)-```--To define the option parser, we need option parsers for every type inside it.-This was true for flat configs too, but we have to manually construct a-`DbConfigB Opt` and `ServiceConfigB Opt`:--``` haskell-configOpt1 :: ConfigB Opt-configOpt1- = ConfigB dbOpt serviceOpt dirOpt--dbOpt :: DbConfigB Opt-dbOpt- = DbConfigB dbHostOpt dbPortOpt--serviceOpt :: ServiceConfigB Opt-serviceOpt- = ServiceConfigB portOpt logOpt-```--And to run the parser:--``` haskell-getConfig1 :: IO ()-getConfig1 = do- ConfigB (DbConfigB dbHost dbPort) (ServiceConfigB port log) dir <-- execOptDef configOpt1- let- db = DbConfig <$> dbHost <*> dbPort- service = ServiceConfig <$> port <*> log- print $ runIdentity $- Config- <$> db- <*> service- <*> dir-```--### 2. Using `higgledy`--`higgledy` puts an `f` before every type, so doing something like `HKD Config f`-doesn't make sense: looking at `ConfigB` it seems like the `f` needs to go to-the right hand side of the nested types. We can, however, avoid the boilerplate-of defining `barbie` types for the nested datatypes:--``` haskell-data ConfigH f- = ConfigH- { _cDbH :: HKD DbConfig f- , _cServiceH :: HKD ServiceConfig f- , _cDirH :: f String- }- deriving (Generic, B.FunctorB, B.TraversableB, B.ProductB)--configOpt2 :: ConfigH Opt-configOpt2- = ConfigH dbOptH serviceOptH dirOpt--dbOptH :: HKD DbConfig Opt-dbOptH- = build @DbConfig dbHostOpt dbPortOpt--serviceOptH :: HKD ServiceConfig Opt-serviceOptH- = build @ServiceConfig portOpt logOpt-```--And to run the parser:--``` haskell-getConfig2 :: IO ()-getConfig2 = do- ConfigH db service dir <- execOptDef configOpt2- print $ runIdentity $- Config- <$> construct db- <*> construct service- <*> dir-```--### 2. Using products--Recall from previously that there's the `Single` type which in general turns `f-b` into `b f`. This means that, by using `Single` for the directory option, all-`f`s are after their types, so we can just use `:*` instead of having to declare-a new datatype:--``` haskell-type ConfigP- = HKD DbConfig- :* HKD ServiceConfig- :* Single String--configOpt3 :: ConfigP Opt-configOpt3- = dbOptH :* serviceOptH :* single dirOpt--getConfig3 :: IO ()-getConfig3 = do- db :* service :* dir <- execOptDef configOpt3- print $ runIdentity $- Config- <$> construct db- <*> construct service- <*> getSingle dir-```--And, to make things look more orthogonal, `harg` defines a type called `Nested`,-which is exactly the same as `HKD`. There are functions that correspond to-`build` and `construct`, too:--```-Nested <-> HKD-nested <-> build-getNested <-> construct-```--This means that the previous code block might as well be:--``` haskell-type ConfigP'- = Nested DbConfig- :* Nested ServiceConfig- :* Single String--configOpt4 :: ConfigP' Opt-configOpt4- = dbOptN :* serviceOptN :* single dirOpt- where- dbOptN- = nested @DbConfig dbHostOpt dbPortOpt- serviceOptN- = nested @ServiceConfig portOpt logOpt--getConfig4 :: IO ()-getConfig4 = do- db :* service :* dir <- execOptDef configOpt4- print $ runIdentity $- Config- <$> getNested db- <*> getNested service- <*> getSingle dir-```--Pretty cool.--## Subcommands--`harg` also supports (somewhat limited) subcommands, again by using `optparse-applicative`-underneath.--Because of limitations with higher kinded data when it comes to sum types,-`harg` uses a different way to define subcommands. `optparse-applicative` allows-defining subcommands that result to the same type, which means the user needs to-define a sum type, and each subcommand results in a different constructor. In-contrast, `harg` defines subcommands that can return completely different types.-Instead of the result being a sum type, where the user has to pattern match on-constructors, the result is a `Variant`, which is defined (almost) like this:--``` haskell-data Variant (xs :: [Type]) where- Here :: x -> Variant (x ': xs)- There :: Variant xs -> Variant (y ': xs)-```--`Variant` is like a sum type which holds all the summands in a type-level list.-Instead of pattern matching in `Left` or `Right` like when using `Either`, we-pattern match on `Here x`, `There (Here x)` etc. For a pretty thorough-introduction to `Variant` and more heterogeneous types, check out-[this repo](https://github.com/i-am-tom/learn-me-a-haskell) by-[i-am-tom](https://github.com/i-am-tom).--``` haskell-x :: Variant '[Int, Bool, Char]-x = There (Here True)--run :: Variant '[Int, Bool, Char] -> Maybe Bool-run (Here _) = Nothing-run (There (Here b)) = Just b-run (There (There (Here _))) = Nothing---- > run x--- Just True-```--`harg` defines another kind of variant called `VariantF`:--``` haskell ignore-data VariantF (xs :: [(Type -> Type) -> Type]) (f :: Type -> Type) where-```--to hold a type-level list of `barbie` types and the `f` to wrap every type with.--To define a type to be used in a subcommand parser we need the target type and-the subcommand name, which is encoded as a type-level string `Symbol`. There's a-handy way to define this. Suppose that the the `Config` type above is the-configuration type when the command is `app` and another type, e.g.`TestConfig`-is the configuration when the command is `test`:--``` haskell-data TestConfig- = TestConfig- { _tcFoo :: String- , _tcBar :: Int- }- deriving Show--fooOpt :: Opt String-fooOpt- = option strParser- ( short 'f'- . help "Something foo"- . defaultVal "this is the default foo"- )--barOpt :: Opt Int-barOpt- = option readParser- ( short 'b'- . help "Something bar"- . defaultVal 42- )--type TestConfigP- = Single String :* Single Int--testConfigOpt :: TestConfigP Opt-testConfigOpt- = single fooOpt :* single barOpt-```--The subcommand type looks like this:--``` haskell-type SubcommandConfig- = "app" :-> ConfigP'- :+ "test" :-> TestConfigP-```--The `+` here stands for sum. The associated option type is:--``` haskell-subcommandOpt :: SubcommandConfig Opt-subcommandOpt- = configOpt4 :+ testConfigOpt :+ ANil-```--The `ANil` here marks the end of the association list (which is a heterogeneous-list that associates symbols with types).--Here's how to run this parser:--``` haskell-getSubcommand :: IO ()-getSubcommand = do- result <- execCommandsDef subcommandOpt- case result of- HereF (db :* service :* dir)- -> print $ runIdentity $- Config- <$> getNested db- <*> getNested service- <*> getSingle dir- ThereF (HereF (foo :* bar))- -> print $ runIdentity $- TestConfig- <$> getSingle foo- <*> getSingle bar-```--Or use `fromVariantF`, which is similar to the `either` function:--``` haskell-getSubcommand' :: IO ()-getSubcommand' = do- result <- execCommandsDef subcommandOpt- fromVariantF result- (\(db :* service :* dir)- -> print $ runIdentity $- Config- <$> getNested db- <*> getNested service- <*> getSingle dir- )- (\(foo :* bar)- -> print $ runIdentity $- TestConfig- <$> getSingle foo- <*> getSingle bar- )-```--The type of `fromVariantF` can be thought of as being:--``` haskell ignore-fromVariantF- :: VariantF '[a, b, c, ...] f- -> (a f -> r)- -> (b f -> r)- -> (c f -> r)- -> ...- -> r-```--The signature will accept the appropriate number of functions depending on the-length of the type level list.--## More than just environment variables--You may have noticed the use of `execOptDef` and `execCommandsDef` in all of the-examples up to now. There are actually more configurable versions of these,-called `execOpt` and `execCommands` respectively. With these functions the user-can select where to get options from. For example, `execOptDef` is a shorthand-for `execOpt EnvSource`, which means that options will be fetched from-environment variables only (along with the command line, which is always-required, and defaults, which can be optionally provided by the user).--The sources currently supported are environment variables, json and yaml files.--### Configuring using a json file--First of all, let's use `FlatConfig` from the first example:--``` haskell ignore-data FlatConfig- = FlatConfig- { _fcDbHost :: String- , _fcDbPort :: Int- , _fcDir :: String- , _fcLog :: Bool -- whether to log or not- }- deriving (Show, Generic)--dbHostOpt :: Opt String-dbHostOpt- = option strParser- ( long "host"- . short 'h'- . metavar "DB_HOST"- . help "The database host"- )--dbPortOpt :: Opt Int-dbPortOpt- = option readParser- ( long "port"- . help "The database port"- . envVar "DB_PORT"- . defaultVal 5432- )--dirOpt :: Opt String-dirOpt- = argument strParser- ( help "Some directory"- . defaultVal "/home/user/something"- )--logOpt :: Opt Bool-logOpt- = switch- ( long "log"- . help "Whether to log or not"- )--flatConfigOpt3 :: HKD FlatConfig Opt-flatConfigOpt3- = build @FlatConfig dbHostOpt dbPortOpt dirOpt logOpt-```--To use the JSON source, a `FromJSON` instance is required. Thankfully that's-easy, since `FlatConfig` has `Generic` instance:--``` haskell-instance FromJSON FlatConfig-```--In `harg`, sources are defined as products (using `:*`) of options, which means-that the definition of the sources is not very different than defining options!-If we only needed the environment variable source, the options would be:--``` haskell ignore-envSource :: EnvSource Opt-envSource = EnvSource-```--There's no need to actually define an option for the environment because there's-no meaningful configuration for this. To use the `EnvSource` along with a json-config, we use the following option:--``` haskell-sourceOpt :: (EnvSource :* JSONSource) Opt-sourceOpt- = EnvSource :* JSONSource jsonOpt- where- jsonOpt :: Opt ConfigFile- jsonOpt- = option strParser- ( long "json"- . short 'j'- . help "JSON config filepath"- )-```--Here, the type of the option for the JSON source is `ConfigFile`. This type is a-wrapper around `FilePath`, which looks like this:--``` haskell ignore-data ConfigFile- = ConfigFile FilePath- | NoConfigFile-```--This has the advantage that, if the user wants to specify an optional-configuration file, they can simply say:--``` haskell-jsonOpt :: Opt ConfigFile-jsonOpt- = option strParser- ( long "json"- . defaultVal NoConfigFile- )-```--Also, because `ConfigFile` has an `IsString` instance, there's no need to say-`long (ConfigFile "json")` (if `OverloadedStrings` is enabled).--There's a bit of a disconnect between `ConfigFile` and the ability to make-optional options using `Maybe` and `optional`. The reason for it is that the-type that `JSONSource` wraps is not polymorphic, since it needs to be a filepath-specifically.--# Roadmap--- Better errors using `optparse-applicative`'s internals-- Allow user to pass `optparse-applicative` preferences-- Write tests--# Credits--- [jcpetruzza](https://github.com/jcpetruzza)-- [i-am-tom](https://github.com/i-am-tom)-- [jmackie](https://github.com/jmackie)
README.md view
@@ -1,4 +1,4 @@-# `harg`+# harg :nut_and_bolt: [](https://travis-ci.org/alexpeits/harg) [](https://hackage.haskell.org/package/harg)@@ -9,803 +9,23 @@ to expose regular arguments, switch arguments and subcommands. The library relies heavily on the use of higher kinded data (HKD) thanks to the [`barbies`](https://hackage.haskell.org/package/barbies) library. Using-[`higgledy`](https://hackage.haskell.org/package/higgledy) also allows to have-significantly less boilerplate code.--The main goal while developing `harg` was to not have to go through the usual-pattern of manually `mappend`ing the results of command line parsing, env vars-and defaults.--# Usage--tl;dr: Take a look at the [example](Example.hs).--Here are some different usage scenarios. Let's first enable some language-extensions and add some imports:--``` haskell-{-# LANGUAGE DataKinds #-}-{-# LANGUAGE DeriveAnyClass #-}-{-# LANGUAGE DeriveGeneric #-}-{-# LANGUAGE FlexibleInstances #-}-{-# LANGUAGE GADTs #-}-{-# LANGUAGE KindSignatures #-}-{-# LANGUAGE StandaloneDeriving #-}-{-# LANGUAGE TypeApplications #-}-{-# LANGUAGE TypeOperators #-}--import Data.Functor.Identity (Identity (..))-import Data.Kind (Type)-import GHC.Generics (Generic)--import qualified Data.Barbie as B-import Data.Aeson (FromJSON)-import Data.Generic.HKD (HKD, build, construct)--import Options.Harg--main :: IO ()-main = putStrLn "this is a literate haskell file"-```--## One flat (non-nested) datatype--The easiest scenario is when the target configuration type is one single record-with no levels of nesting:--``` haskell-data FlatConfig- = FlatConfig- { _fcDbHost :: String- , _fcDbPort :: Int- , _fcDir :: String- , _fcLog :: Bool -- whether to log or not- }- deriving (Show, Generic)-```--(The `Generic` instance is required for section `3` later on)--Let's first create the `Opt`s for each value in `FlatConfig`. `Opt` is the-description for each component of the configuration.--``` haskell-dbHostOpt :: Opt String-dbHostOpt- = option strParser- ( long "host"- . short 'h'- . metavar "DB_HOST"- . help "The database host"- )--dbPortOpt :: Opt Int-dbPortOpt- = option readParser- ( long "port"- . help "The database port"- . envVar "DB_PORT"- . defaultVal 5432- )--dirOpt :: Opt String-dirOpt- = argument strParser- ( help "Some directory"- . defaultVal "/home/user/something"- )--logOpt :: Opt Bool-logOpt- = switch- ( long "log"- . help "Whether to log or not"- )-```--Here, we use `option` to define a command line argument that expects a value-after it, `argument` to define a standalone argument, not prefixed by a long or-short indicator, and `switch` to define a boolean command line flag that, if-present, sets the target value to `True`. The `opt*` functions (here applied-using `&` to make things look more declarative) modify the option configuration.-`help` adds help text, `defaultVal` adds a default value, `short` adds a short-command line option as an alternative to the long one (the string after `option`-or `switch`), `envVar` sets the associated environment variable and `metavar`-sets the metavariable to be shown in the help text generated by-`optparse-applicative`.---The first argument (`strParser` or `readParser`) is the parser for the argument,-be it from the command line or from an environment variable. The type of this-function should be `String -> Either String a`, which produces an error message-or the parsed value. `strParser` is equivalent to `pure` and always succeeds.-`readParser` requires the type to have a `Read` constraint. In order to use it-with newtypes that wrap a type that has a `Read` constraint, using the `Functor`-instance for `Opt` should be sufficient. E.g. for the newtype:--``` haskell-newtype Port = Port Int-```--we can define the following option:--``` haskell-dbPortOpt' :: Opt Port-dbPortOpt'- = Port <$> dbPortOpt-```--Of course, any user-defined function works as well. In addition, to use a-function of type `String -> Maybe a` use `parseWith`, which runs the parser and-in case of failure uses a default error message. For example, `readParser` is-defined as `parseWith readMaybe`.--Finally, an optional option with type `a` can be specified by setting its type-to `Maybe a`. The declaration is exactly the same as it would be for `a`, and-adding `optional` to the modifiers turns turns the parser from `String -> Either-String a` to `String -> Either String (Maybe a)` but without using the `Read`-instance for `Maybe`:--``` haskell ignore-someOpt :: Opt (Maybe Int)-someOpt- = option readParser- ( long "something"- . optional- )-```--Note that `optional` can't be used with `defaultVal`. Using them together raises-a type error at compile time, to ensure there's no ambiguous behaviour (e.g. the-order of declaration of modifiers should not influence the resulting option).--There are 3 ways to configure this datatype.--### 1. Using a `barbie` type--`barbie` types are types of kind `(Type -> Type) -> Type`. The `barbie` type for-`FlatConfig` looks like this:--``` haskell-data FlatConfigB f- = FlatConfigB- { _fcDbHostB :: f String- , _fcDbPortB :: f Int- , _fcDirB :: f String- , _fcLogB :: f Bool- }- deriving (Generic, B.FunctorB, B.TraversableB, B.ProductB)-```--I also derived some required instances that come from the `barbies` package.-These instances allow us to change the `f` (`bmap` from `FunctorB`) and traverse-all types in the record producing side effects (`btraverse` from-`TraversableB`).--Now let's define the value of this datatype, which holds our option-configuration. The type constructor needed for the options is `Opt`:--``` haskell-flatConfigOpt1 :: FlatConfigB Opt-flatConfigOpt1- = FlatConfigB dbHostOpt dbPortOpt dirOpt logOpt-```--Because `dbHostOpt`, `dbPortOpt` and `logOpt` all have type `Opt <actual type>`,-`flatConfigOpt1` has the correct type according to `FlatConfigB Opt`.--Now to actually run things:--``` haskell-getFlatConfig1 :: IO ()-getFlatConfig1 = do- FlatConfigB host port dir log <- execOptDef flatConfigOpt1- print $ runIdentity $- FlatConfig- <$> host- <*> port- <*> dir- <*> log-```--`execOpt` returns an `Identity x` where `x` is the type of the options we are-configuring, in this case `FlatConfigB`. Here, we pattern match on the-barbie-type, and then use the `Applicative` instance of `Identity` to get back-an `Identity FlatConfig`.--This is still a bit boilerplate-y. Let's look at another way.--### 2. Using a product type--Looking at `FlatConfigB`, it's only used because of it's `barbie`-like-capabilities. Other than that it's just a simple product type with the-additional `f` before all its sub-types.--`harg` defines a type almost similar to `Product` (from `Data.Functor.Product`),-which works in a similar fashion as servant's `:<|>` type. This type is defined-in `Options.Harg.Het.Prod` and is called `:*` (the `*` stands for product). This-type stores barbie-like types and also keeps the `f` handy: `data (a :* b) f = a-f :* b f`. This is also easily made an instance of `Generic`, `FunctorB`,-`TraversableB` and `ProductB`. With all that, let's rewrite the options value-and the function to get the configuration:--``` haskell-flatConfigOpt2- :: (Single String :* Single Int :* Single String :* Single Bool) Opt-flatConfigOpt2- = single dbHostOpt :* single dbPortOpt :* single dirOpt :* single logOpt--getFlatConfig2 :: IO ()-getFlatConfig2 = do- host :* port :* dir :* log <- execOptDef flatConfigOpt2- print $ runIdentity $- FlatConfig- <$> getSingle host- <*> getSingle port- <*> getSingle dir- <*> getSingle log-```--This looks aufully similar to the previous version, but without having to write-another datatype and derive all the instances. `:*` is both a type-level-constructor and a value-level function that acts like list's `:`. It is also-right-associative, so for example `a :* b :* c` is the same as `a :* (b :* c)`.--The `Single` type constructor is used when talking about a single value, rather-than a nested datatype. `Single a f` is a simple newtype over `f a`. The reason-for using that is simply to switch the order of application, so that we can-later apply the `f` (here `Opt`) to the compound type (`:*`). This makes type-definitions look more similar to datatype definitions:--``` haskell-type FlatConfigOpt2- = Single String- :* Single Int- :* Single Bool-```--In addition, `single` is used to wrap an `f a` into a `Single a f`, and-`getSingle` is used to unwrap it. Later on we'll see how to construct nested-configurations using `Nested`.--However, the real value when having flat datatypes comes from the ability to use-`higgledy`.--### 3. Using `HKD` from `higgledy`--``` haskell-flatConfigOpt3 :: HKD FlatConfig Opt-flatConfigOpt3- = build @FlatConfig dbHostOpt dbPortOpt dirOpt logOpt--getFlatConfig3 :: IO ()-getFlatConfig3 = do- result <- execOptDef flatConfigOpt3- print $ runIdentity (construct result)-```--This is the most straightforward way to work with flat configuration types. The-`build` function takes as arguments the options (`Opt a` where `a` is each type-in `FlatConfig`) **in the order they appear in the datatype**, and returns the-generic representation of a type that's exactly the same as `FlatConfigB`. This-means that we get all the `barbie` instances for free.--To go back from the `HKD` representation of a datatype to the base one, we use-`construct`. `construct` uses the applicative instance of the `f` which wraps-each type in `FlatConfig` to give back an `f FlatConfig` (in our case an-`Identity FlatConfig`).--## Nested datatypes--Let's say now that we have these two datatypes:--``` haskell-data DbConfig- = DbConfig- { _dcHost :: String- , _dcPort :: Int- }- deriving (Show, Generic)--data ServiceConfig- = ServiceConfig- { _scPort :: Int- , _scLog :: Bool- }- deriving (Show, Generic)-```--And the datatype to be configured is this:--``` haskell-data Config- = Config- { _cDb :: DbConfig- , _cService :: ServiceConfig- , _cDir :: String- }- deriving (Show, Generic)-```--And a new option required for the service port:--``` haskell-portOpt :: Opt Int-portOpt- = option readParser- ( long "port"- . help "The service port"- . defaultVal 8080- )-```--Again, there are several ways to configure these options.--### 1. Using `barbie` types--Since we now have 3 types, there's a bit more boilerplate to write:--``` haskell-data ConfigB f- = ConfigB- { _cDbB :: DbConfigB f- , _cServiceB :: ServiceConfigB f- , _cDirB :: f String- }- deriving (Generic, B.FunctorB, B.TraversableB, B.ProductB)--data DbConfigB f- = DbConfigB- { _dcHostB :: f String- , _dcPortB :: f Int- }- deriving (Generic, B.FunctorB, B.TraversableB, B.ProductB)--data ServiceConfigB f- = ServiceConfigB- { _scPortB :: f Int- , _scLogB :: f Bool- }- deriving (Generic, B.FunctorB, B.TraversableB, B.ProductB)-```--To define the option parser, we need option parsers for every type inside it.-This was true for flat configs too, but we have to manually construct a-`DbConfigB Opt` and `ServiceConfigB Opt`:--``` haskell-configOpt1 :: ConfigB Opt-configOpt1- = ConfigB dbOpt serviceOpt dirOpt--dbOpt :: DbConfigB Opt-dbOpt- = DbConfigB dbHostOpt dbPortOpt--serviceOpt :: ServiceConfigB Opt-serviceOpt- = ServiceConfigB portOpt logOpt-```--And to run the parser:--``` haskell-getConfig1 :: IO ()-getConfig1 = do- ConfigB (DbConfigB dbHost dbPort) (ServiceConfigB port log) dir <-- execOptDef configOpt1- let- db = DbConfig <$> dbHost <*> dbPort- service = ServiceConfig <$> port <*> log- print $ runIdentity $- Config- <$> db- <*> service- <*> dir-```--### 2. Using `higgledy`--`higgledy` puts an `f` before every type, so doing something like `HKD Config f`-doesn't make sense: looking at `ConfigB` it seems like the `f` needs to go to-the right hand side of the nested types. We can, however, avoid the boilerplate-of defining `barbie` types for the nested datatypes:--``` haskell-data ConfigH f- = ConfigH- { _cDbH :: HKD DbConfig f- , _cServiceH :: HKD ServiceConfig f- , _cDirH :: f String- }- deriving (Generic, B.FunctorB, B.TraversableB, B.ProductB)--configOpt2 :: ConfigH Opt-configOpt2- = ConfigH dbOptH serviceOptH dirOpt--dbOptH :: HKD DbConfig Opt-dbOptH- = build @DbConfig dbHostOpt dbPortOpt--serviceOptH :: HKD ServiceConfig Opt-serviceOptH- = build @ServiceConfig portOpt logOpt-```--And to run the parser:--``` haskell-getConfig2 :: IO ()-getConfig2 = do- ConfigH db service dir <- execOptDef configOpt2- print $ runIdentity $- Config- <$> construct db- <*> construct service- <*> dir-```--### 2. Using products--Recall from previously that there's the `Single` type which in general turns `f-b` into `b f`. This means that, by using `Single` for the directory option, all-`f`s are after their types, so we can just use `:*` instead of having to declare-a new datatype:--``` haskell-type ConfigP- = HKD DbConfig- :* HKD ServiceConfig- :* Single String--configOpt3 :: ConfigP Opt-configOpt3- = dbOptH :* serviceOptH :* single dirOpt--getConfig3 :: IO ()-getConfig3 = do- db :* service :* dir <- execOptDef configOpt3- print $ runIdentity $- Config- <$> construct db- <*> construct service- <*> getSingle dir-```--And, to make things look more orthogonal, `harg` defines a type called `Nested`,-which is exactly the same as `HKD`. There are functions that correspond to-`build` and `construct`, too:--```-Nested <-> HKD-nested <-> build-getNested <-> construct-```--This means that the previous code block might as well be:--``` haskell-type ConfigP'- = Nested DbConfig- :* Nested ServiceConfig- :* Single String--configOpt4 :: ConfigP' Opt-configOpt4- = dbOptN :* serviceOptN :* single dirOpt- where- dbOptN- = nested @DbConfig dbHostOpt dbPortOpt- serviceOptN- = nested @ServiceConfig portOpt logOpt--getConfig4 :: IO ()-getConfig4 = do- db :* service :* dir <- execOptDef configOpt4- print $ runIdentity $- Config- <$> getNested db- <*> getNested service- <*> getSingle dir-```--Pretty cool.--## Subcommands--`harg` also supports (somewhat limited) subcommands, again by using `optparse-applicative`-underneath.--Because of limitations with higher kinded data when it comes to sum types,-`harg` uses a different way to define subcommands. `optparse-applicative` allows-defining subcommands that result to the same type, which means the user needs to-define a sum type, and each subcommand results in a different constructor. In-contrast, `harg` defines subcommands that can return completely different types.-Instead of the result being a sum type, where the user has to pattern match on-constructors, the result is a `Variant`, which is defined (almost) like this:--``` haskell-data Variant (xs :: [Type]) where- Here :: x -> Variant (x ': xs)- There :: Variant xs -> Variant (y ': xs)-```--`Variant` is like a sum type which holds all the summands in a type-level list.-Instead of pattern matching in `Left` or `Right` like when using `Either`, we-pattern match on `Here x`, `There (Here x)` etc. For a pretty thorough-introduction to `Variant` and more heterogeneous types, check out-[this repo](https://github.com/i-am-tom/learn-me-a-haskell) by-[i-am-tom](https://github.com/i-am-tom).--``` haskell-x :: Variant '[Int, Bool, Char]-x = There (Here True)--run :: Variant '[Int, Bool, Char] -> Maybe Bool-run (Here _) = Nothing-run (There (Here b)) = Just b-run (There (There (Here _))) = Nothing---- > run x--- Just True-```--`harg` defines another kind of variant called `VariantF`:--``` haskell ignore-data VariantF (xs :: [(Type -> Type) -> Type]) (f :: Type -> Type) where-```--to hold a type-level list of `barbie` types and the `f` to wrap every type with.--To define a type to be used in a subcommand parser we need the target type and-the subcommand name, which is encoded as a type-level string `Symbol`. There's a-handy way to define this. Suppose that the the `Config` type above is the-configuration type when the command is `app` and another type, e.g.`TestConfig`-is the configuration when the command is `test`:--``` haskell-data TestConfig- = TestConfig- { _tcFoo :: String- , _tcBar :: Int- }- deriving Show--fooOpt :: Opt String-fooOpt- = option strParser- ( short 'f'- . help "Something foo"- . defaultVal "this is the default foo"- )--barOpt :: Opt Int-barOpt- = option readParser- ( short 'b'- . help "Something bar"- . defaultVal 42- )--type TestConfigP- = Single String :* Single Int--testConfigOpt :: TestConfigP Opt-testConfigOpt- = single fooOpt :* single barOpt-```--The subcommand type looks like this:--``` haskell-type SubcommandConfig- = "app" :-> ConfigP'- :+ "test" :-> TestConfigP-```--The `+` here stands for sum. The associated option type is:--``` haskell-subcommandOpt :: SubcommandConfig Opt-subcommandOpt- = configOpt4 :+ testConfigOpt :+ ANil-```--The `ANil` here marks the end of the association list (which is a heterogeneous-list that associates symbols with types).--Here's how to run this parser:--``` haskell-getSubcommand :: IO ()-getSubcommand = do- result <- execCommandsDef subcommandOpt- case result of- HereF (db :* service :* dir)- -> print $ runIdentity $- Config- <$> getNested db- <*> getNested service- <*> getSingle dir- ThereF (HereF (foo :* bar))- -> print $ runIdentity $- TestConfig- <$> getSingle foo- <*> getSingle bar-```--Or use `fromVariantF`, which is similar to the `either` function:--``` haskell-getSubcommand' :: IO ()-getSubcommand' = do- result <- execCommandsDef subcommandOpt- fromVariantF result- (\(db :* service :* dir)- -> print $ runIdentity $- Config- <$> getNested db- <*> getNested service- <*> getSingle dir- )- (\(foo :* bar)- -> print $ runIdentity $- TestConfig- <$> getSingle foo- <*> getSingle bar- )-```--The type of `fromVariantF` can be thought of as being:--``` haskell ignore-fromVariantF- :: VariantF '[a, b, c, ...] f- -> (a f -> r)- -> (b f -> r)- -> (c f -> r)- -> ...- -> r-```--The signature will accept the appropriate number of functions depending on the-length of the type level list.--## More than just environment variables--You may have noticed the use of `execOptDef` and `execCommandsDef` in all of the-examples up to now. There are actually more configurable versions of these,-called `execOpt` and `execCommands` respectively. With these functions the user-can select where to get options from. For example, `execOptDef` is a shorthand-for `execOpt EnvSource`, which means that options will be fetched from-environment variables only (along with the command line, which is always-required, and defaults, which can be optionally provided by the user).--The sources currently supported are environment variables, json and yaml files.--### Configuring using a json file--First of all, let's use `FlatConfig` from the first example:--``` haskell ignore-data FlatConfig- = FlatConfig- { _fcDbHost :: String- , _fcDbPort :: Int- , _fcDir :: String- , _fcLog :: Bool -- whether to log or not- }- deriving (Show, Generic)--dbHostOpt :: Opt String-dbHostOpt- = option strParser- ( long "host"- . short 'h'- . metavar "DB_HOST"- . help "The database host"- )--dbPortOpt :: Opt Int-dbPortOpt- = option readParser- ( long "port"- . help "The database port"- . envVar "DB_PORT"- . defaultVal 5432- )--dirOpt :: Opt String-dirOpt- = argument strParser- ( help "Some directory"- . defaultVal "/home/user/something"- )--logOpt :: Opt Bool-logOpt- = switch- ( long "log"- . help "Whether to log or not"- )--flatConfigOpt3 :: HKD FlatConfig Opt-flatConfigOpt3- = build @FlatConfig dbHostOpt dbPortOpt dirOpt logOpt-```--To use the JSON source, a `FromJSON` instance is required. Thankfully that's-easy, since `FlatConfig` has `Generic` instance:--``` haskell-instance FromJSON FlatConfig-```--In `harg`, sources are defined as products (using `:*`) of options, which means-that the definition of the sources is not very different than defining options!-If we only needed the environment variable source, the options would be:--``` haskell ignore-envSource :: EnvSource Opt-envSource = EnvSource-```--There's no need to actually define an option for the environment because there's-no meaningful configuration for this. To use the `EnvSource` along with a json-config, we use the following option:--``` haskell-sourceOpt :: (EnvSource :* JSONSource) Opt-sourceOpt- = EnvSource :* JSONSource jsonOpt- where- jsonOpt :: Opt ConfigFile- jsonOpt- = option strParser- ( long "json"- . short 'j'- . help "JSON config filepath"- )-```--Here, the type of the option for the JSON source is `ConfigFile`. This type is a-wrapper around `FilePath`, which looks like this:--``` haskell ignore-data ConfigFile- = ConfigFile FilePath- | NoConfigFile-```--This has the advantage that, if the user wants to specify an optional-configuration file, they can simply say:--``` haskell-jsonOpt :: Opt ConfigFile-jsonOpt- = option strParser- ( long "json"- . defaultVal NoConfigFile- )-```+[`higgledy`](https://hackage.haskell.org/package/higgledy) also helps reduce+boilerplate code significantly. -Also, because `ConfigFile` has an `IsString` instance, there's no need to say-`long (ConfigFile "json")` (if `OverloadedStrings` is enabled).+## Documentation -There's a bit of a disconnect between `ConfigFile` and the ability to make-optional options using `Maybe` and `optional`. The reason for it is that the-type that `JSONSource` wraps is not polymorphic, since it needs to be a filepath-specifically.+To find out more, check out the [docs](https://alexpeits.github.io/harg) or the+package page on [hackage](https://hackage.haskell.org/package/harg). There is+also an [example](https://github.com/alexpeits/harg/blob/master/Example.hs)+module that serves as an extensive demonstration of the library. -# Roadmap+## Roadmap - Better errors using `optparse-applicative`'s internals - Allow user to pass `optparse-applicative` preferences - Write tests -# Credits+## Credits - [jcpetruzza](https://github.com/jcpetruzza) - [i-am-tom](https://github.com/i-am-tom)
+ docs/docs.lhs view
@@ -0,0 +1,799 @@+<!-- -*- tab-width: 2; -*- -->+# harg++`harg` is a library for configuring programs by scanning command line arguments,+environment variables, default values and more. Under the hood, it uses a subset+of [`optparse-applicative`](https://hackage.haskell.org/package/optparse-applicative)+to expose regular arguments, switch arguments and subcommands. The library+relies heavily on the use of higher kinded data (HKD) thanks to the+[`barbies`](https://hackage.haskell.org/package/barbies) library. Using+[`higgledy`](https://hackage.haskell.org/package/higgledy) also helps reduce+boilerplate code significantly.++The main goal while developing `harg` was to not have to go through the usual+pattern of manually `mappend`ing the results of command line parsing, env vars+and defaults.++This file is also a literate haskell file which is checked when tests are run,+so all examples should compile and work, and be up to date with the latest+version of the library.++# Usage++Here are some different usage scenarios. Let's first enable some language+extensions and add some imports:++``` haskell+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE DeriveAnyClass #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE GADTs #-}+{-# LANGUAGE KindSignatures #-}+{-# LANGUAGE StandaloneDeriving #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE TypeOperators #-}++import Data.Functor.Identity (Identity (..))+import Data.Kind (Type)+import GHC.Generics (Generic)++import qualified Data.Barbie as B+import Data.Aeson (FromJSON)+import Data.Generic.HKD (HKD, build, construct)++import Options.Harg++main :: IO ()+main = putStrLn "this is a literate haskell file"+```++## One flat (non-nested) datatype++The easiest scenario is when the target configuration type is one single record+with no levels of nesting:++``` haskell+data FlatConfig+ = FlatConfig+ { _fcDbHost :: String+ , _fcDbPort :: Int+ , _fcDir :: String+ , _fcLog :: Bool -- whether to log or not+ }+ deriving (Show, Generic)+```++(The `Generic` instance is required for section `3` later on)++Let's first create the `Opt`s for each value in `FlatConfig`. `Opt` is the+description for each component of the configuration.++``` haskell+dbHostOpt :: Opt String+dbHostOpt+ = option strParser+ ( long "host"+ . short 'h'+ . metavar "DB_HOST"+ . help "The database host"+ )++dbPortOpt :: Opt Int+dbPortOpt+ = option readParser+ ( long "port"+ . help "The database port"+ . envVar "DB_PORT"+ . defaultVal 5432+ )++dirOpt :: Opt String+dirOpt+ = argument strParser+ ( help "Some directory"+ . defaultVal "/home/user/something"+ )++logOpt :: Opt Bool+logOpt+ = switch+ ( long "log"+ . help "Whether to log or not"+ )+```++Here, we use `option` to define a command line argument that expects a value+after it, `argument` to define a standalone argument, not prefixed by a long or+short indicator, and `switch` to define a boolean command line flag that, if+present, sets the target value to `True`. The `opt*` functions (here applied+using `&` to make things look more declarative) modify the option configuration.+`help` adds help text, `defaultVal` adds a default value, `short` adds a short+command line option as an alternative to the long one (the string after `option`+or `switch`), `envVar` sets the associated environment variable and `metavar`+sets the metavariable to be shown in the help text generated by+`optparse-applicative`.++The first argument (`strParser` or `readParser`) is the parser for the argument,+be it from the command line or from an environment variable. The type of this+function should be `String -> Either String a`, which produces an error message+or the parsed value. `strParser` is equivalent to `pure` and always succeeds.+`readParser` requires the type to have a `Read` constraint. In order to use it+with newtypes that wrap a type that has a `Read` constraint, using the `Functor`+instance for `Opt` should be sufficient. E.g. for the newtype:++``` haskell+newtype Port = Port Int+```++we can define the following option:++``` haskell+dbPortOpt' :: Opt Port+dbPortOpt'+ = Port <$> dbPortOpt+```++Of course, any user-defined function works as well. In addition, to use a+function of type `String -> Maybe a` use `parseWith`, which runs the parser and+in case of failure uses a default error message. For example, `readParser` is+defined as `parseWith readMaybe`.++Finally, an optional option with type `a` can be specified by setting its type+to `Maybe a`. The declaration is exactly the same as it would be for `a`, and+adding `optional` to the modifiers turns turns the parser from `String -> Either+String a` to `String -> Either String (Maybe a)` but without using the `Read`+instance for `Maybe`:++``` hs+someOpt :: Opt (Maybe Int)+someOpt+ = option readParser+ ( long "something"+ . optional+ )+```++Note that `optional` can't be used with `defaultVal`. Using them together raises+a type error at compile time, to ensure there's no ambiguous behaviour (e.g. the+order of declaration of modifiers should not influence the resulting option).++There are 3 ways to configure this datatype.++### 1. Using a `barbie` type++`barbie` types are types of kind `(Type -> Type) -> Type`. The `barbie` type for+`FlatConfig` looks like this:++``` haskell+data FlatConfigB f+ = FlatConfigB+ { _fcDbHostB :: f String+ , _fcDbPortB :: f Int+ , _fcDirB :: f String+ , _fcLogB :: f Bool+ }+ deriving (Generic, B.FunctorB, B.TraversableB, B.ProductB)+```++I also derived some required instances that come from the `barbies` package.+These instances allow us to change the `f` (`bmap` from `FunctorB`) and traverse+all types in the record producing side effects (`btraverse` from+`TraversableB`).++Now let's define the value of this datatype, which holds our option+configuration. The type constructor needed for the options is `Opt`:++``` haskell+flatConfigOpt1 :: FlatConfigB Opt+flatConfigOpt1+ = FlatConfigB dbHostOpt dbPortOpt dirOpt logOpt+```++Because `dbHostOpt`, `dbPortOpt` and `logOpt` all have type `Opt <actual type>`,+`flatConfigOpt1` has the correct type according to `FlatConfigB Opt`.++Now to actually run things:++``` haskell+getFlatConfig1 :: IO ()+getFlatConfig1 = do+ FlatConfigB host port dir log <- execOptDef flatConfigOpt1+ print $ runIdentity $+ FlatConfig+ <$> host+ <*> port+ <*> dir+ <*> log+```++`execOpt` returns an `Identity x` where `x` is the type of the options we are+configuring, in this case `FlatConfigB`. Here, we pattern match on the+barbie-type, and then use the `Applicative` instance of `Identity` to get back+an `Identity FlatConfig`.++This is still a bit boilerplate-y. Let's look at another way.++### 2. Using a product type++Looking at `FlatConfigB`, it's only used because of it's `barbie`-like+capabilities. Other than that it's just a simple product type with the+additional `f` before all its sub-types.++`harg` defines a type almost similar to `Product` (from `Data.Functor.Product`),+which works in a similar fashion as servant's `:<|>` type. This type is defined+in `Options.Harg.Het.Prod` and is called `:*` (the `*` stands for product). This+type stores barbie-like types and also keeps the `f` handy: `data (a :* b) f = a+f :* b f`. This is also easily made an instance of `Generic`, `FunctorB`,+`TraversableB` and `ProductB`. With all that, let's rewrite the options value+and the function to get the configuration:++``` haskell+flatConfigOpt2+ :: (Single String :* Single Int :* Single String :* Single Bool) Opt+flatConfigOpt2+ = single dbHostOpt :* single dbPortOpt :* single dirOpt :* single logOpt++getFlatConfig2 :: IO ()+getFlatConfig2 = do+ host :* port :* dir :* log <- execOptDef flatConfigOpt2+ print $ runIdentity $+ FlatConfig+ <$> getSingle host+ <*> getSingle port+ <*> getSingle dir+ <*> getSingle log+```++This looks aufully similar to the previous version, but without having to write+another datatype and derive all the instances. `:*` is both a type-level+constructor and a value-level function that acts like list's `:`. It is also+right-associative, so for example `a :* b :* c` is the same as `a :* (b :* c)`.++The `Single` type constructor is used when talking about a single value, rather+than a nested datatype. `Single a f` is a simple newtype over `f a`. The reason+for using that is simply to switch the order of application, so that we can+later apply the `f` (here `Opt`) to the compound type (`:*`). This makes type+definitions look more similar to datatype definitions:++``` haskell+type FlatConfigOpt2+ = Single String+ :* Single Int+ :* Single Bool+```++In addition, `single` is used to wrap an `f a` into a `Single a f`, and+`getSingle` is used to unwrap it. Later on we'll see how to construct nested+configurations using `Nested`.++However, the real value when having flat datatypes comes from the ability to use+`higgledy`.++### 3. Using `HKD` from `higgledy`++``` haskell+flatConfigOpt3 :: HKD FlatConfig Opt+flatConfigOpt3+ = build @FlatConfig dbHostOpt dbPortOpt dirOpt logOpt++getFlatConfig3 :: IO ()+getFlatConfig3 = do+ result <- execOptDef flatConfigOpt3+ print $ runIdentity (construct result)+```++This is the most straightforward way to work with flat configuration types. The+`build` function takes as arguments the options (`Opt a` where `a` is each type+in `FlatConfig`) **in the order they appear in the datatype**, and returns the+generic representation of a type that's exactly the same as `FlatConfigB`. This+means that we get all the `barbie` instances for free.++To go back from the `HKD` representation of a datatype to the base one, we use+`construct`. `construct` uses the applicative instance of the `f` which wraps+each type in `FlatConfig` to give back an `f FlatConfig` (in our case an+`Identity FlatConfig`).++## Nested datatypes++Let's say now that we have these two datatypes:++``` haskell+data DbConfig+ = DbConfig+ { _dcHost :: String+ , _dcPort :: Int+ }+ deriving (Show, Generic)++data ServiceConfig+ = ServiceConfig+ { _scPort :: Int+ , _scLog :: Bool+ }+ deriving (Show, Generic)+```++And the datatype to be configured is this:++``` haskell+data Config+ = Config+ { _cDb :: DbConfig+ , _cService :: ServiceConfig+ , _cDir :: String+ }+ deriving (Show, Generic)+```++And a new option required for the service port:++``` haskell+portOpt :: Opt Int+portOpt+ = option readParser+ ( long "port"+ . help "The service port"+ . defaultVal 8080+ )+```++Again, there are several ways to configure these options.++### 1. Using `barbie` types++Since we now have 3 types, there's a bit more boilerplate to write:++``` haskell+data ConfigB f+ = ConfigB+ { _cDbB :: DbConfigB f+ , _cServiceB :: ServiceConfigB f+ , _cDirB :: f String+ }+ deriving (Generic, B.FunctorB, B.TraversableB, B.ProductB)++data DbConfigB f+ = DbConfigB+ { _dcHostB :: f String+ , _dcPortB :: f Int+ }+ deriving (Generic, B.FunctorB, B.TraversableB, B.ProductB)++data ServiceConfigB f+ = ServiceConfigB+ { _scPortB :: f Int+ , _scLogB :: f Bool+ }+ deriving (Generic, B.FunctorB, B.TraversableB, B.ProductB)+```++To define the option parser, we need option parsers for every type inside it.+This was true for flat configs too, but we have to manually construct a+`DbConfigB Opt` and `ServiceConfigB Opt`:++``` haskell+configOpt1 :: ConfigB Opt+configOpt1+ = ConfigB dbOpt serviceOpt dirOpt++dbOpt :: DbConfigB Opt+dbOpt+ = DbConfigB dbHostOpt dbPortOpt++serviceOpt :: ServiceConfigB Opt+serviceOpt+ = ServiceConfigB portOpt logOpt+```++And to run the parser:++``` haskell+getConfig1 :: IO ()+getConfig1 = do+ ConfigB (DbConfigB dbHost dbPort) (ServiceConfigB port log) dir <-+ execOptDef configOpt1+ let+ db = DbConfig <$> dbHost <*> dbPort+ service = ServiceConfig <$> port <*> log+ print $ runIdentity $+ Config+ <$> db+ <*> service+ <*> dir+```++### 2. Using `higgledy`++`higgledy` puts an `f` before every type, so doing something like `HKD Config f`+doesn't make sense: looking at `ConfigB` it seems like the `f` needs to go to+the right hand side of the nested types. We can, however, avoid the boilerplate+of defining `barbie` types for the nested datatypes:++``` haskell+data ConfigH f+ = ConfigH+ { _cDbH :: HKD DbConfig f+ , _cServiceH :: HKD ServiceConfig f+ , _cDirH :: f String+ }+ deriving (Generic, B.FunctorB, B.TraversableB, B.ProductB)++configOpt2 :: ConfigH Opt+configOpt2+ = ConfigH dbOptH serviceOptH dirOpt++dbOptH :: HKD DbConfig Opt+dbOptH+ = build @DbConfig dbHostOpt dbPortOpt++serviceOptH :: HKD ServiceConfig Opt+serviceOptH+ = build @ServiceConfig portOpt logOpt+```++And to run the parser:++``` haskell+getConfig2 :: IO ()+getConfig2 = do+ ConfigH db service dir <- execOptDef configOpt2+ print $ runIdentity $+ Config+ <$> construct db+ <*> construct service+ <*> dir+```++### 2. Using products++Recall from previously that there's the `Single` type which in general turns `f+b` into `b f`. This means that, by using `Single` for the directory option, all+`f`s are after their types, so we can just use `:*` instead of having to declare+a new datatype:++``` haskell+type ConfigP+ = HKD DbConfig+ :* HKD ServiceConfig+ :* Single String++configOpt3 :: ConfigP Opt+configOpt3+ = dbOptH :* serviceOptH :* single dirOpt++getConfig3 :: IO ()+getConfig3 = do+ db :* service :* dir <- execOptDef configOpt3+ print $ runIdentity $+ Config+ <$> construct db+ <*> construct service+ <*> getSingle dir+```++And, to make things look more orthogonal, `harg` defines a type called `Nested`,+which is exactly the same as `HKD`. There are functions that correspond to+`build` and `construct`, too:++```+Nested <-> HKD+nested <-> build+getNested <-> construct+```++This means that the previous code block might as well be:++``` haskell+type ConfigP'+ = Nested DbConfig+ :* Nested ServiceConfig+ :* Single String++configOpt4 :: ConfigP' Opt+configOpt4+ = dbOptN :* serviceOptN :* single dirOpt+ where+ dbOptN+ = nested @DbConfig dbHostOpt dbPortOpt+ serviceOptN+ = nested @ServiceConfig portOpt logOpt++getConfig4 :: IO ()+getConfig4 = do+ db :* service :* dir <- execOptDef configOpt4+ print $ runIdentity $+ Config+ <$> getNested db+ <*> getNested service+ <*> getSingle dir+```++Pretty cool.++## Subcommands++`harg` also supports (somewhat limited) subcommands, again by using `optparse-applicative`+underneath.++Because of limitations with higher kinded data when it comes to sum types,+`harg` uses a different way to define subcommands. `optparse-applicative` allows+defining subcommands that result to the same type, which means the user needs to+define a sum type, and each subcommand results in a different constructor. In+contrast, `harg` defines subcommands that can return completely different types.+Instead of the result being a sum type, where the user has to pattern match on+constructors, the result is a `Variant`, which is defined (almost) like this:++``` haskell+data Variant (xs :: [Type]) where+ Here :: x -> Variant (x ': xs)+ There :: Variant xs -> Variant (y ': xs)+```++`Variant` is like a sum type which holds all the summands in a type-level list.+Instead of pattern matching in `Left` or `Right` like when using `Either`, we+pattern match on `Here x`, `There (Here x)` etc. For a pretty thorough+introduction to `Variant` and more heterogeneous types, check out+[this repo](https://github.com/i-am-tom/learn-me-a-haskell) by+[i-am-tom](https://github.com/i-am-tom).++``` haskell+x :: Variant '[Int, Bool, Char]+x = There (Here True)++run :: Variant '[Int, Bool, Char] -> Maybe Bool+run (Here _) = Nothing+run (There (Here b)) = Just b+run (There (There (Here _))) = Nothing++-- > run x+-- Just True+```++`harg` defines another kind of variant called `VariantF`:++``` hs+data VariantF (xs :: [(Type -> Type) -> Type]) (f :: Type -> Type) where+```++to hold a type-level list of `barbie` types and the `f` to wrap every type with.++To define a type to be used in a subcommand parser we need the target type and+the subcommand name, which is encoded as a type-level string `Symbol`. There's a+handy way to define this. Suppose that the the `Config` type above is the+configuration type when the command is `app` and another type, e.g.`TestConfig`+is the configuration when the command is `test`:++``` haskell+data TestConfig+ = TestConfig+ { _tcFoo :: String+ , _tcBar :: Int+ }+ deriving Show++fooOpt :: Opt String+fooOpt+ = option strParser+ ( short 'f'+ . help "Something foo"+ . defaultVal "this is the default foo"+ )++barOpt :: Opt Int+barOpt+ = option readParser+ ( short 'b'+ . help "Something bar"+ . defaultVal 42+ )++type TestConfigP+ = Single String :* Single Int++testConfigOpt :: TestConfigP Opt+testConfigOpt+ = single fooOpt :* single barOpt+```++The subcommand type looks like this:++``` haskell+type SubcommandConfig+ = "app" :-> ConfigP'+ :+ "test" :-> TestConfigP+```++The `+` here stands for sum. The associated option type is:++``` haskell+subcommandOpt :: SubcommandConfig Opt+subcommandOpt+ = configOpt4 :+ testConfigOpt :+ ANil+```++The `ANil` here marks the end of the association list (which is a heterogeneous+list that associates symbols with types).++Here's how to run this parser:++``` haskell+getSubcommand :: IO ()+getSubcommand = do+ result <- execCommandsDef subcommandOpt+ case result of+ HereF (db :* service :* dir)+ -> print $ runIdentity $+ Config+ <$> getNested db+ <*> getNested service+ <*> getSingle dir+ ThereF (HereF (foo :* bar))+ -> print $ runIdentity $+ TestConfig+ <$> getSingle foo+ <*> getSingle bar+```++Or use `fromVariantF`, which is similar to the `either` function:++``` haskell+getSubcommand' :: IO ()+getSubcommand' = do+ result <- execCommandsDef subcommandOpt+ fromVariantF result+ (\(db :* service :* dir)+ -> print $ runIdentity $+ Config+ <$> getNested db+ <*> getNested service+ <*> getSingle dir+ )+ (\(foo :* bar)+ -> print $ runIdentity $+ TestConfig+ <$> getSingle foo+ <*> getSingle bar+ )+```++The type of `fromVariantF` can be thought of as being:++``` hs+fromVariantF+ :: VariantF '[a, b, c, ...] f+ -> (a f -> r)+ -> (b f -> r)+ -> (c f -> r)+ -> ...+ -> r+```++The signature will accept the appropriate number of functions depending on the+length of the type level list.++## More than just environment variables++You may have noticed the use of `execOptDef` and `execCommandsDef` in all of the+examples up to now. There are actually more configurable versions of these,+called `execOpt` and `execCommands` respectively. With these functions the user+can select where to get options from. For example, `execOptDef` is a shorthand+for `execOpt EnvSource`, which means that options will be fetched from+environment variables only (along with the command line, which is always+required, and defaults, which can be optionally provided by the user).++The sources currently supported are environment variables, json and yaml files.++### Configuring using a json file++First of all, let's use `FlatConfig` from the first example:++``` hs+data FlatConfig+ = FlatConfig+ { _fcDbHost :: String+ , _fcDbPort :: Int+ , _fcDir :: String+ , _fcLog :: Bool -- whether to log or not+ }+ deriving (Show, Generic)++dbHostOpt :: Opt String+dbHostOpt+ = option strParser+ ( long "host"+ . short 'h'+ . metavar "DB_HOST"+ . help "The database host"+ )++dbPortOpt :: Opt Int+dbPortOpt+ = option readParser+ ( long "port"+ . help "The database port"+ . envVar "DB_PORT"+ . defaultVal 5432+ )++dirOpt :: Opt String+dirOpt+ = argument strParser+ ( help "Some directory"+ . defaultVal "/home/user/something"+ )++logOpt :: Opt Bool+logOpt+ = switch+ ( long "log"+ . help "Whether to log or not"+ )++flatConfigOpt3 :: HKD FlatConfig Opt+flatConfigOpt3+ = build @FlatConfig dbHostOpt dbPortOpt dirOpt logOpt+```++To use the JSON source, a `FromJSON` instance is required. Thankfully that's+easy, since `FlatConfig` has `Generic` instance:++``` haskell+instance FromJSON FlatConfig+```++In `harg`, sources are defined as products (using `:*`) of options, which means+that the definition of the sources is not very different than defining options!+If we only needed the environment variable source, the options would be:++``` hs+envSource :: EnvSource Opt+envSource = EnvSource+```++There's no need to actually define an option for the environment because there's+no meaningful configuration for this. To use the `EnvSource` along with a json+config, we use the following option:++``` haskell+sourceOpt :: (EnvSource :* JSONSource) Opt+sourceOpt+ = EnvSource :* JSONSource jsonOpt+ where+ jsonOpt :: Opt ConfigFile+ jsonOpt+ = option strParser+ ( long "json"+ . short 'j'+ . help "JSON config filepath"+ )+```++Here, the type of the option for the JSON source is `ConfigFile`. This type is a+wrapper around `FilePath`, which looks like this:++``` hs+data ConfigFile+ = ConfigFile FilePath+ | NoConfigFile+```++This has the advantage that, if the user wants to specify an optional+configuration file, they can simply say:++``` haskell+jsonOpt :: Opt ConfigFile+jsonOpt+ = option strParser+ ( long "json"+ . defaultVal NoConfigFile+ )+```++Also, because `ConfigFile` has an `IsString` instance, there's no need to say+`long (ConfigFile "json")` (if `OverloadedStrings` is enabled).++There's a bit of a disconnect between `ConfigFile` and the ability to make+optional options using `Maybe` and `optional`. The reason for it is that the+type that `JSONSource` wraps is not polymorphic, since it needs to be a filepath+specifically.
harg.cabal view
@@ -1,7 +1,7 @@ cabal-version: 2.2 name: harg-version: 0.4.1.0+version: 0.4.2.0 synopsis: Haskell program configuration using higher kinded data description: Please see the README on GitHub at <https://github.com/alexpeits/harg#readme> homepage: https://github.com/alexpeits/harg@@ -86,9 +86,10 @@ , harg default-language: Haskell2010 -test-suite readme-test+test-suite docs-test type: exitcode-stdio-1.0- main-is: README.lhs+ hs-source-dirs: docs+ main-is: docs.lhs ghc-options: -Wall -Wno-unticked-promoted-constructors -threaded
src/Options/Harg.hs view
@@ -15,8 +15,8 @@ , fromSingle , Nested (..)- , nested , getNested+ , nested , fromNested , AssocListF (..)@@ -37,7 +37,6 @@ , defaultStr , required , optional- , toOpt , Opt -- ** Option parsers@@ -50,8 +49,12 @@ -- ** Executing options , execOpt , execOptDef+ , execOptWithCtx+ , execOptWithCtxDef , execCommands , execCommandsDef+ , execCommandsWithCtx+ , execCommandsWithCtxDef -- ** Option sources , EnvSource (..)
src/Options/Harg/Cmdline.hs view
@@ -1,4 +1,6 @@-module Options.Harg.Cmdline where+module Options.Harg.Cmdline+ ( mkOptparseParser+ ) where import Control.Applicative ((<|>)) import Data.Functor.Compose (Compose (..))@@ -8,7 +10,7 @@ import qualified Data.Barbie as B import qualified Options.Applicative as Optparse -import Options.Harg.Pretty+import Options.Harg.Pretty (ppHelp) import Options.Harg.Types
src/Options/Harg/Config.hs view
@@ -1,4 +1,7 @@-module Options.Harg.Config where+module Options.Harg.Config+ ( mkConfigParser+ , getConfig+ ) where import Data.Functor.Compose (Compose (..)) import Data.Kind (Type)@@ -6,9 +9,9 @@ import qualified Data.Barbie as B import qualified Options.Applicative as Optparse -import Options.Harg.Cmdline-import Options.Harg.Sources-import Options.Harg.Sources.Env+import Options.Harg.Cmdline (mkOptparseParser)+import Options.Harg.Sources (accumSourceResults)+import Options.Harg.Sources.Env (EnvSourceVal(..)) import Options.Harg.Sources.Types import Options.Harg.Types
src/Options/Harg/Construct.hs view
@@ -2,7 +2,27 @@ {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE UndecidableInstances #-} {-# LANGUAGE UndecidableSuperClasses #-}-module Options.Harg.Construct where+module Options.Harg.Construct+ ( option+ , flag+ , switch+ , switch'+ , argument+ , long+ , short+ , help+ , metavar+ , envVar+ , defaultVal+ , defaultStr+ , required+ , optional+ , parseWith+ , readParser+ , strParser+ , boolParser+ , manyParser+ ) where import Data.Char (toLower) import Data.Kind (Constraint)
src/Options/Harg/Het/All.hs view
@@ -1,6 +1,8 @@+{-# LANGUAGE PolyKinds #-} {-# LANGUAGE TypeFamilies #-}-{-# LANGUAGE PolyKinds #-}-module Options.Harg.Het.All where+module Options.Harg.Het.All+ ( All+ ) where import Data.Kind (Type, Constraint)
src/Options/Harg/Het/HList.hs view
@@ -2,7 +2,13 @@ {-# LANGUAGE RankNTypes #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE UndecidableInstances #-}-module Options.Harg.Het.HList where+module Options.Harg.Het.HList+ ( MapAssocList (..)+ , AssocListF (..)+ , (:+)+ , pattern (:+)+ , (:->)+ ) where import Data.Kind (Type) import GHC.TypeLits (ErrorMessage(..), TypeError, Symbol)
src/Options/Harg/Het/Nat.hs view
@@ -1,4 +1,7 @@-module Options.Harg.Het.Nat where+module Options.Harg.Het.Nat+ ( Nat (..)+ , SNat (..)+ ) where -- | Type-level Peano natural number. data Nat
src/Options/Harg/Het/Prod.hs view
@@ -5,7 +5,10 @@ {-# LANGUAGE PolyKinds #-} {-# LANGUAGE StandaloneDeriving #-} {-# LANGUAGE UndecidableInstances #-}-module Options.Harg.Het.Prod where+module Options.Harg.Het.Prod+ ( (:*) (..)+ , Tagged (..)+ ) where import Data.Functor.Identity (Identity) import Data.Kind (Type)
src/Options/Harg/Het/Proofs.hs view
@@ -6,7 +6,11 @@ {-# LANGUAGE RankNTypes #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE UndecidableInstances #-}-module Options.Harg.Het.Proofs where+module Options.Harg.Het.Proofs+ ( type (++)+ , Proof (..)+ , hgcastWith+ ) where import Data.Kind (Type) import Data.Type.Equality
src/Options/Harg/Het/Variant.hs view
@@ -3,7 +3,16 @@ {-# LANGUAGE PatternSynonyms #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE UndecidableInstances #-}-module Options.Harg.Het.Variant where+module Options.Harg.Het.Variant+ ( VariantF (..)+ , fromVariantF+ , InjectPosF (..)+ , pattern In1+ , pattern In2+ , pattern In3+ , pattern In4+ , pattern In5+ ) where import Data.Kind (Type)
src/Options/Harg/Nested.hs view
@@ -7,7 +7,12 @@ {-# LANGUAGE UndecidableInstances #-} {-# OPTIONS_GHC -fno-warn-orphans #-}-module Options.Harg.Nested where+module Options.Harg.Nested+ ( Nested (..)+ , nested+ , getNested+ , fromNested+ ) where import Data.Coerce (Coercible, coerce) import Data.Functor.Identity (Identity(..))
src/Options/Harg/Operations.hs view
@@ -4,28 +4,37 @@ {-# LANGUAGE RankNTypes #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE UndecidableInstances #-}-module Options.Harg.Operations where+module Options.Harg.Operations+ ( execOpt+ , execOptDef+ , execOptWithCtx+ , execOptWithCtxDef+ , execCommands+ , execCommandsDef+ , execCommandsWithCtx+ , execCommandsWithCtxDef+ ) where -import Data.Functor.Identity (Identity(..))+import Data.Functor.Identity (Identity(..)) -import qualified Data.Barbie as B-import qualified Options.Applicative as Optparse+import qualified Data.Barbie as B+import qualified Options.Applicative as Optparse -import Options.Harg.Cmdline (mkOptparseParser)-import Options.Harg.Config (mkConfigParser, getConfig)-import Options.Harg.Het.All (All)-import Options.Harg.Het.HList (AssocListF, MapAssocList(..))-import Options.Harg.Het.Prod ((:*)(..))-import Options.Harg.Het.Variant (VariantF)-import Options.Harg.Pretty (ppSourceRunErrors)-import Options.Harg.Sources ( accumSourceResults- , DefaultSources, defaultSources- , HiddenSources, hiddenSources- )-import Options.Harg.Sources.Types (GetSource(..), RunSource(..), SourceRunError)-import Options.Harg.Subcommands (Subcommands(..))-import Options.Harg.Types (HargCtx(..), getCtx, Opt)-import Options.Harg.Util (toDummyOpts, allToDummyOpts, compose)+import Options.Harg.Cmdline (mkOptparseParser)+import Options.Harg.Config (mkConfigParser, getConfig)+import Options.Harg.Het.All (All)+import Options.Harg.Het.HList (AssocListF, MapAssocList(..))+import Options.Harg.Het.Prod ((:*)(..))+import Options.Harg.Het.Variant (VariantF)+import Options.Harg.Pretty (ppSourceRunErrors)+import Options.Harg.Sources ( accumSourceResults+ , DefaultSources, defaultSources+ , HiddenSources, hiddenSources+ )+import Options.Harg.Sources.Types (GetSource(..), RunSource(..), SourceRunError)+import Options.Harg.Subcommands (Subcommands(..))+import Options.Harg.Types (HargCtx(..), getCtx, Opt)+import Options.Harg.Util (toDummyOpts, allToDummyOpts, compose) -- | Run the option parser and combine with values from the specified sources, -- passing the context explicitly.
src/Options/Harg/Pretty.hs view
@@ -1,4 +1,7 @@-module Options.Harg.Pretty where+module Options.Harg.Pretty+ ( ppHelp+ , ppSourceRunErrors+ ) where import Control.Applicative ((<|>)) import Data.List (intercalate)
src/Options/Harg/Single.hs view
@@ -3,7 +3,11 @@ {-# LANGUAGE PolyKinds #-} {-# LANGUAGE StandaloneDeriving #-} {-# LANGUAGE UndecidableInstances #-}-module Options.Harg.Single where+module Options.Harg.Single+ ( Single (..)+ , single+ , fromSingle+ ) where import Data.Functor.Identity (Identity(..)) import qualified Data.Functor.Product as P@@ -11,7 +15,6 @@ import GHC.Generics (Generic) import qualified Data.Aeson as JSON- import qualified Data.Barbie as B
src/Options/Harg/Sources.hs view
@@ -1,12 +1,18 @@-module Options.Harg.Sources where+module Options.Harg.Sources+ ( accumSourceResults+ , HiddenSources+ , hiddenSources+ , DefaultSources+ , defaultSources+ ) where -import Data.Foldable (foldr')-import Data.Functor.Compose (Compose (..))+import Data.Foldable (foldr')+import Data.Functor.Compose (Compose(..)) -import qualified Data.Barbie as B+import qualified Data.Barbie as B -import Options.Harg.Sources.DefaultStr-import Options.Harg.Sources.Env+import Options.Harg.Sources.DefaultStr (DefaultStrSource(..))+import Options.Harg.Sources.Env (EnvSource(..)) import Options.Harg.Sources.Types @@ -44,12 +50,10 @@ -- | Sources hidden from user that are always enabled hiddenSources :: HiddenSources f-hiddenSources- = DefaultStrSource+hiddenSources = DefaultStrSource type DefaultSources = EnvSource -- | Default sources, equivalent to 'EnvSource' defaultSources :: DefaultSources f-defaultSources- = EnvSource+defaultSources = EnvSource
src/Options/Harg/Sources/DefaultStr.hs view
@@ -1,7 +1,9 @@ {-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE TypeFamilies #-}-module Options.Harg.Sources.DefaultStr where+module Options.Harg.Sources.DefaultStr+ ( DefaultStrSource (..)+ ) where import Data.Functor.Compose (Compose (..)) import Data.Kind (Type)
src/Options/Harg/Sources/Env.hs view
@@ -1,7 +1,10 @@ {-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE TypeFamilies #-}-module Options.Harg.Sources.Env where+module Options.Harg.Sources.Env+ ( EnvSource (..)+ , EnvSourceVal (..)+ ) where import Data.Functor.Compose (Compose (..)) import Data.Kind (Type)
src/Options/Harg/Sources/JSON.hs view
@@ -2,19 +2,21 @@ {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE UndecidableInstances #-}-module Options.Harg.Sources.JSON where+module Options.Harg.Sources.JSON+ ( JSONSource (..)+ ) where +import qualified Data.ByteString.Lazy as LBS import Data.Functor.Compose (Compose (..)) import Data.Functor.Identity (Identity(..)) import GHC.Generics (Generic)-import qualified Data.ByteString.Lazy as LBS import qualified Data.Aeson as JSON import qualified Data.Barbie as B import Options.Harg.Sources.Types import Options.Harg.Types-import Options.Harg.Util+import Options.Harg.Util (readFileLBS) -- | Source that enables a parser to read options from a JSON file. newtype JSONSource f = JSONSource (f ConfigFile)
src/Options/Harg/Sources/NoSource.hs view
@@ -1,7 +1,10 @@ {-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE TypeFamilies #-}-module Options.Harg.Sources.NoSource where+module Options.Harg.Sources.NoSource+ ( NoSource+ , noSources+ ) where import Data.Kind (Type) import GHC.Generics (Generic)@@ -21,5 +24,4 @@ -- | Shorthand for writing 'NoSource'. noSources :: NoSource f-noSources- = NoSource+noSources = NoSource
src/Options/Harg/Sources/Types.hs view
@@ -1,13 +1,20 @@ {-# LANGUAGE DeriveFunctor #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE UndecidableInstances #-}-module Options.Harg.Sources.Types where+module Options.Harg.Sources.Types+ ( GetSource (..)+ , RunSource (..)+ , ConfigFile (..)+ , SourceRunResult (..)+ , SourceRunError (..)+ , sourceRunError+ ) where -import Data.Functor.Compose (Compose (..))+import Data.Functor.Compose (Compose(..)) import Data.Kind (Type) import Data.String (IsString(..)) -import Options.Harg.Het.Prod+import Options.Harg.Het.Prod ((:*)(..)) import Options.Harg.Types
src/Options/Harg/Sources/YAML.hs view
@@ -2,7 +2,9 @@ {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE UndecidableInstances #-}-module Options.Harg.Sources.YAML where+module Options.Harg.Sources.YAML+ ( YAMLSource (..)+ ) where import Control.Exception (displayException) import qualified Data.ByteString as BS@@ -15,7 +17,7 @@ import Options.Harg.Sources.Types import Options.Harg.Types-import Options.Harg.Util+import Options.Harg.Util (readFileBS) -- | Source that enables a parser to read options from a YAML file.
src/Options/Harg/Subcommands.hs view
@@ -1,6 +1,8 @@ {-# LANGUAGE AllowAmbiguousTypes #-} {-# LANGUAGE UndecidableInstances #-}-module Options.Harg.Subcommands where+module Options.Harg.Subcommands+ ( Subcommands (..)+ ) where import Data.Functor.Compose (Compose (..)) import Data.Kind (Type)@@ -10,13 +12,13 @@ import qualified Data.Barbie as B import qualified Options.Applicative as Optparse -import Options.Harg.Cmdline-import Options.Harg.Het.All-import Options.Harg.Het.HList+import Options.Harg.Cmdline (mkOptparseParser)+import Options.Harg.Het.All (All)+import Options.Harg.Het.HList (AssocListF(..)) import Options.Harg.Het.Nat-import Options.Harg.Het.Proofs-import Options.Harg.Het.Variant-import Options.Harg.Sources+import Options.Harg.Het.Proofs (type (++), Proof(..), hgcastWith)+import Options.Harg.Het.Variant (VariantF, InjectPosF(..))+import Options.Harg.Sources (accumSourceResults) import Options.Harg.Sources.Types import Options.Harg.Types
src/Options/Harg/Types.hs view
@@ -1,5 +1,20 @@ {-# LANGUAGE DeriveFunctor #-}-module Options.Harg.Types where+module Options.Harg.Types+ ( Opt (..)+ , OptionOpt (..)+ , FlagOpt (..)+ , ArgumentOpt (..)+ , OptAttr (..)+ , OptType (..)+ , SomeOpt (..)+ , OptReader+ , HargCtx (..)+ , Environment+ , getCtx+ , ctxFromArgs+ , ctxFromEnv+ , pureCtx+ ) where import System.Environment (getArgs, getEnvironment)
src/Options/Harg/Util.hs view
@@ -1,5 +1,11 @@ {-# LANGUAGE RankNTypes #-}-module Options.Harg.Util where+module Options.Harg.Util+ ( toDummyOpts+ , allToDummyOpts+ , compose+ , readFileBS+ , readFileLBS+ ) where import qualified Control.Exception as Exc import qualified Data.ByteString as BS@@ -11,7 +17,7 @@ import qualified Data.Barbie as B -import Options.Harg.Het.HList+import Options.Harg.Het.HList (AssocListF, MapAssocList(..)) import Options.Harg.Types