This module provides a unified method of configuring a program, being able to read settings from a ini-format file, from environment variables, and from the command line in a predictable and uniform manner, with room for customisation.
As an example, let's create a simple data structure for a program's configuration:
data Foo = Foo
{ fooInt :: Int
, fooStrings :: [String]
, fooDoubles :: NonEmpty Double
, fooEnabled :: Map String Bool
, fooBar :: Either String Bar
, fooFile :: (FilePath, String)
} deriving Show
data Bar = Frob | Wibble | Snarf
deriving (Bounded, Enum, Show)
We need an Option Int to parse in the Int:
oInt :: Option Int
oInt = option auto (name "FOO" "int")
This defines an Option which when reading from an ini file will consult the
variable int in the section [FOO], when reading the enviroment variables it
will look for the XXX_FOO_INT environment variable, and when parsing the
command line it will match the --FOO.int option. The XXX is a configurable
prefix for the environment variables.
This definition uses the auto Reader, which uses a type's Show and Read
instances to parse the value from a string and to display it when required.
Now we need to read strings for fooStrings:
oStrings :: Option [String]
oStrings = optionMany str (name "FOO" "strings")
This Option will read the variable strings from the ini-file section [FOO]
as many times as it occurs, returning each one in a list. When reading from the
environment variables it will search for XXX_FOO_STRINGS_0,
XXX_FOO_STRINGS_1, etc, or if XXX_FOO_STRINGS_NONE exists the list will be
empty. On the command line it will match --FOO.strings options as many times
as they occur. Here the str Reader is used, which simply returns the string
as read from the source.
oDoubles :: Option (NonEmpty Double)
oDoubles = optionSome auto (name "FOO" "doubles")
This Option works in the same way as for optionMany, except that at least one
item needs to be read from some configuration source, and the ..._NONE
environment variable is not used. Again Show and Read instances for
Double are used in the auto Reader.
oEnabled :: Option (Map String Bool)
oEnabled = optionMap boolean (name "FOO" "enabled")
This Option reads all variables of the form enabled.<key> from section
[FOO] in ini files, and collects the values with the keys in the Map
returned. In environment variables it searches for XXX_FOO_ENABLED_<KEY>,
and XXX_FOO_ENABLED_NONE means an empty Map. On the command line
--FOO.enabled is matched where the option arguments have the form
<key>=<value>. In this Option the boolean Reader is used which
accepts the strings "true" and "false", ignoring case.
oBar :: Option (Either String Bar)
oBar = commands (name "FOO" "bar")
[ ("left", Nothing, Left <$> option str (name "FOO" "message"))
, ("right", Nothing, Right <$> option (enum f) (name "FOO" "value"))
]
where f Frob = "fr...ob"
f Wibble = "WIBBLEWIBBLE"
f Snarf = "snarF!!!"
This Option first checks a command setting, called [FOO].bar in ini files,
and XXX_FOO_BAR in environment variables. At the command line it defines two
mutually exclusive commands, FOO.bar.left and FOO.bar.right. These settings
define what options are then subsequently loaded, if the "left" value is used
then [FOO].message is consulted as a String, if the "right" value is used
then [FOO].value is used.
The [FOO].value option uses a custom enum Reader which is case sensitive,
and a function from Bar to String. The enum Reader requires the Bar
type to have instances of the Enum and Bounded type classes.
oFile :: Option (FilePath, String)
oFile =
withIO "read-file"
(\path -> (\contents -> Right (path, contents)) <$> readFile path)
(option str (name "FOO" "file"))
This Option first reads a string from [FOO].file, and then uses that string
as a file path to read the contents of the file and return both in a tuple. If
there is some problem the FilePath -> IO (Either String (FilePath, String))
function can return a Left value with an error message, which will be an error
tagged with the label "read-file". If the function raises an exception it
will not be caught.
Now that we have all the pieces to parse the components of a configuration, we
can combine them with the standard Applicative combinators:
oFoo :: Option Foo
oFoo = Foo <$> oInt <*> oStrings <*> oDoubles <*> oEnabled <*> oBar <*> oFile
Once an Option is defined, we need to run it to configure our program. First
build a parser:
parser
:: Ini -> [(String, String)]
-> Opt.Parser (IO (Either [ParseError] (Foo, Ini)))
parser ini env = mkParser "XXX" ini env oFoo
This gives us an optparse-applicative command line parser which will yield an
IO action if successful. We need to provide a parsed Ini value (from the
ini package) and the process environment which can be obtained from
System.Environment.getEnvironment.
The parser can be run with
getFoo
:: Ini -> [(String, String)]
-> IO (Either [ParseError] (Foo, Ini))
getFoo ini env = Control.Monad.join $ Opt.execParser $ Opt.info (parser ini env <**> Opt.helper) Opt.fullDesc
Tying the pieces together:
main :: IO ()
main = do
iniE <- Ini.readIniFile "test.ini"
case iniE of
Left err -> print err
Right ini -> do
env <- System.Environment.getEnvironment
r <- getFoo ini env
case r of
Left errs -> mapM_ print errs
Right (foo, ini') -> do
putStrLn $ Text.unpack $ Ini.printIniWith (Ini.WriteIniSettings Ini.EqualsKeySeparator) ini'
print foo
Now can we run the parser and receive back either a list of ParseErrors, or
the parsed Foo value along with a new Ini value which contains all the
actual final values that made up the Foo we received. If that Ini value
were written out to a file and used in a subsequent run, without any environment
variables or command line options, we would still receive the same Foo value.
>>> ./example --FOO.int 0 --FOO.strings xyz --FOO.strings uvw --FOO.doubles 3.2 --FOO.file test.txt FOO.bar.right --FOO.value "snarF!!!" --FOO.enabled abc=FALSE
[FOO]
int=0
strings=xyz
strings=uvw
doubles=3.2
enabled.abc=false
bar=right
value=snarF!!!
file=test.txt
Foo {fooInt = 0, fooStrings = ["xyz", "uvw"], fooDoubles = 3.2 :| [], fooEnabled = fromList [("abc",False)], fooBar = Right Snarf, fooFile = ("test.txt","this is the contents of test.txt\n")}
As with all optparse-applicative parsers, the usage information can be quite
helpful:
>>> ./example --help
Usage: ./example --FOO.int ARG [--FOO.strings ARG] --FOO.doubles ARG
[--FOO.enabled ARG] COMMAND --FOO.file ARG
Available options:
-h,--help Show this help text
Available commands:
FOO.bar.left
FOO.bar.right
This output is a bit sparse at the moment, but per-option help can be provided,
metavars customised, command line option aliases defined, and so forth, which
can make the --help output much friendlier.
A sample ini file to begin filling out can be easily generated using
genExample:
>>> putStrLn $ genExample "XXX" oFoo
[FOO]
# Valid values are "left", "right".
# When bar is left the following variables are required:
# - FOO.message
# When bar is right the following variables are required:
# - FOO.value
# Override using:
# - the XXX_FOO_BAR environment variable
# - the FOO.bar.left command
# - the FOO.bar.right command
bar = [left|right]
# May be given any number of times, but must be at least once.
# Override using:
# - the XXX_FOO_DOUBLES_0, _1, _2... environment variables
# - the --FOO.doubles command line option
doubles = <ARG>
# Valid values are "false", "true".
# May be given any number of times, with the key following the variable
# separated by a full stop.
# Override using:
# - the XXX_FOO_ENABLED_NONE or XXX_FOO_ENABLED_<key>... environment
# variables
# - the --FOO.enabled command line option
# The argument must be of the form "key=value" in command line options.
enabled.<key> = <ARG>
# Override using:
# - the XXX_FOO_FILE environment variable
# - the --FOO.file command line option
file = <ARG>
# Override using:
# - the XXX_FOO_INT environment variable
# - the --FOO.int command line option
int = <ARG>
# Override using:
# - the XXX_FOO_MESSAGE environment variable
# - the --FOO.message command line option
message = <ARG>
# May be given any number of times, or not at all.
# Override using:
# - the XXX_FOO_STRINGS_NONE or XXX_FOO_STRINGS_0, _1, _2... environment
# variables
# - the --FOO.strings command line option
strings = <ARG>
# Valid values are "fr...ob", "WIBBLEWIBBLE", "snarF!!!".
# Override using:
# - the XXX_FOO_VALUE environment variable
# - the --FOO.value command line option
value = <ARG>