I'm releasing the optparse-generic
library which uses Haskell's support for generic programming to auto-generate command-line interfaces for a wide variety of types.
For example, suppose that you define a record with two fields:
data Example = Example { foo :: Int, bar :: Double }
You can auto-generate a command-line interface tailored to that record like this:
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE OverloadedStrings #-}
import Options.Generic
data Example = Example { foo :: Int, bar :: Double }
deriving (Generic, Show)
instance ParseRecord Example
main = do
x <- getRecord "Test program"
print (x :: Example)
This generates the following command-line interface:
$ stack runghc Example.hs -- --help
Test program
Usage: Example.hs --foo INT --bar DOUBLE
Available options:
-h,--help Show this help text
... and we can verify that the interface works by supplying the appropriate arguments:
$ stack runghc Example.hs -- --foo 1 --bar 2.5
Example {foo = 1, bar = 2.5}
You can also compile the program into a native executable binary:
$ stack ghc Example.hs
[1 of 1] Compiling Main ( Example.hs, Example.o )
Linking Example ...
$ ./Example --foo 1 --bar 2.5
Example {foo = 1, bar = 2.5}
Features
The auto-generated interface tries to be as intelligent as possible. For example, if you omit the record labels:
data Example = Example Int Double
... then the fields will become positional arguments:
$ ./Example --help
Test program
Usage: Example INT DOUBLE
Available options:
-h,--help Show this help text
$ ./Example 1 2.5
Example 1 2.5
If you wrap a field in Maybe
:
data Example = Example { foo :: Maybe Int }
... then the corresponding command-line flag/argument becomes optional:
$ ./Example --help
Test program
Usage: Example [--foo INT]
Available options:
-h,--help Show this help text
$ ./Example
Example {foo = Nothing}
$ ./Example --foo 2
Example {foo = Just 2}
If a field is a list of values:
data Example = Example { foo :: [Int] }
... then the corresponding command-line flag/argument can be repeated:
$ ./Example --foo 1 --foo 2
Example {foo = [1,2]}
$ ./Example
Example {foo = []}
If you wrap a value in First
or Last
:
data Example = Example { foo :: First Int, bar :: Last Int }
... then you will get the first or last match, respectively:
$ ./Example --foo 1 --foo 2 --bar 1 --bar 2
Example {foo = First {getFirst = Just 1}, bar = Last {getLast = Just 2}}
$ ./Example
Example {foo = First {getFirst = Nothing}, bar = Last {getLast = Nothing}}
You can even do fancier things like ask for the Sum
or Product
of all matching fields:
data Example = Example { foo :: Sum Int, bar :: Product Int }
... and it will do the "right thing":
$ ./Example --foo 1 --foo 2 --bar 1 --bar 2
Example {foo = Sum {getSum = 3}, bar = Product {getProduct = 2}}
$ ./Example
Example {foo = Sum {getSum = 0}, bar = Product {getProduct = 1}}
If a data type has multiple constructors:
data Example
= Create { name :: Text, duration :: Maybe Int }
| Kill { name :: Text }
... then that translates to subcommands named after each constructor:
$ ./Example --help
Test program
Usage: Example (create | kill)
Available options:
-h,--help Show this help text
Available commands:
create
kill
$ ./Example create --help
Usage: Example create --name TEXT [--duration INT]
Available options:
-h,--help Show this help text
$ ./Example kill --help
Usage: Example kill --name TEXT
Available options:
-h,--help Show this help text
$ ./Example create --name foo --duration 60
Create {name = "foo", duration = Just 60}
$ ./Example kill --name foo
Kill {name = "foo"}
This library also supports many existing Haskell data types out of the box. For example, if you just need to get a Double
and Int
from the command line you could just write:
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE OverloadedStrings #-}
import Options.Generic
main = do
x <- getRecord "Test program"
print (x :: (Double, Int))
... and that will parse two positional arguments:
$ ./Example --help
Test program
Usage: Example DOUBLE INT
Available options:
-h,--help Show this help text
$ ./Example 1.1 2
(1.1,2)
Compile-time safety
Haskell's support for generic programming is done completely at compile time. This means that if you ask for something that cannot be sensibly converted into a command-line interface your program will fail to compile.
For example, if you ask for a list of lists:
data Example = Example { foo :: [[Int]] }
.. then the compiler will fail with the following error message since you can't (idiomatically) model "repeated (repeated Int
s)" on the command line:
No instance for (ParseField [Int])
arising from a use of ‘Options.Generic.$gdmparseRecord’
In the expression: Options.Generic.$gdmparseRecord
In an equation for ‘parseRecord’:
parseRecord = Options.Generic.$gdmparseRecord
In the instance declaration for ‘ParseRecord Example’
Conclusion
If you would like to use this package or learn more you can find this package:
I also plan to re-export this package's functionality from turtle
to further simplify command-line programming.