Declarative command line option parser.
Declarative and easy to use command line option parser
optparse-declarative
optparse-declarative
is a declarative and easy-to-use command-line option parser.
Install
$ cabal install optparse-declarative
Usage
Writing a simple command
First, you need to enable DataKinds
extension. Then import Options.Declarative
module.
{-# LANGUAGE DataKinds #-}
import Options.Declarative
Next, define command line options as a type of the function. For example, this is a simple greeting program with -g
option that takes a message of type String
and an unnamed command-line argument that specifies a name:
greet :: Flag "g" '["greet"] "STRING" "greeting message" (Def "Hello" String)
-> Arg "NAME" String
-> Cmd "Greeting command" ()
greet msg name =
liftIO $ putStrLn $ get msg ++ ", " ++ get name ++ "!"
There are two types of options, Flag
and Arg
. Flag
represents a named argument (e.g., --greet "Hola"
), and Arg
an unnamed argument (e.g., John
of greet --greet Hola John
). The last argument of Flag
and Arg
is the type of the value of the argument; in this example, they are both String
. You can specify any type for the value as long as the type is an instance of ArgRead
typeclass, in which the conversion function from String
to the specified type is defined. Options.Declarative
provides following instances of ArgRead
typeclass.
- Int
- Integer
- Bool
- Double
- String
- (ArgRead a) => Maybe a
Users can add a new instance of ArgRead
to support any user-defined type. Please see Section "How to add a new instance of ArgRead
" for details.
If you wish to specify a default value for allowing users to omit a value, use the modifier Def
with the default value as the second type argument (and the third type argument is the type of the value). You need to specify the default value in String
instead of the final value of the target type; the string will be converted to the final value via ArgRead
typeclass.
In the example above, the variable msg
has a very complex type (Flag "g" '["greet"] "STRING" "greeting message" (Def "Hello" String)
). In order to get the value of the target type (in this case, that is String
), you can use get
function.
The whole type of command is Cmd
. Cmd
is an instance of MonadIO
and it has some extra information.
Finally, you can run the whole program by run_
.
main :: IO ()
main = run_ greet
Here is an example session with the program shown above.
$ ghc simple.hs
$ ./simple
simple: not enough arguments
Try 'simple --help' for more information.
$ ./simple --help
Usage: simple [OPTION...] NAME
Options:
-g STRING --greet=STRING greeting message
-? --help display this help and exit
$ ./simple World
Hello, World!
$ ./simple --greet=Goodbye World
Goodbye, World!
Note that only the final option is used when multiple options of the same name are given. This behavior emulates the behavior of a naive program that uses GNU Getopt.
$ ./simple --greet=Hello --greet=Goodbye World
Goodbye, World!
There is another way of interpreting multiple options of the same name. Suppose if you need to get multiple values from the same option. Say, you wish to get ["Hello", "Goodbye"]
from the command-line option --greet=Hello --greeet=Goodbye
. Then, you can use the type []
to indicate that it accepts multiple values. The first line of the function greet
in the example above would be changed as this:
greet :: Flag "g" '["greet"] "STRING" "greeting message" [String]
The value returned by get
will be a value of type [String]
. See the complete working example at example/list.hs
for details.
Writing multiple subcommands
You can write (nested) subcommands. You don't know what subcommands are? Imagine git
command. git
has subcommands such as git add
, git commit
, git log
, etc. git
has nested subcommands such as git remote add
, git remote rm
, etc. optparse-declarative
provides an easy way to provide such possibly nested subcommands.
Just group subcommands by Group
, then you get a subcommand parser. Here is an example with two subcommands greet
and connect
:
{-# LANGUAGE DataKinds #-}
import Options.Declarative
main :: IO ()
main = run_ $
Group "Test program for library"
[ subCmd "greet" greet
, subCmd "connect" connect
]
greet :: Flag "g" '["greet"] "STRING" "greeting message" (Def "Hello" String)
-> Flag "" '["decolate"] "" "decolate message" Bool
-> Arg "NAME" String
-> Cmd "Greeting command" ()
greet msg deco name = do
let f x | get deco = "*** " ++ x ++ " ***"
| otherwise = x
liftIO $ putStrLn $ f $ get msg ++ ", " ++ get name ++ "!"
connect :: Flag "h" '["host"] "HOST" "host name" (Def "localhost" String)
-> Flag "p" '["port"] "PORT" "port number" (Def "8080" Int )
-> Cmd "Connect command" ()
connect host port = do
let addr = get host ++ ":" ++ show (get port)
liftIO $ putStrLn $ "connect to " ++ addr
This is a sample session for the program above:
$ ./subcmd --help
Usage: subcmd [OPTION...] <COMMAND> [ARGS...]
Options:
-? --help display this help and exit
Commands:
greet Greeting command
port Server command
$ ./subcmd connect --port=1234
connect to localhost:1234
If you wish to specify the program name or the version number, use run
instead of run_
. The first argument of run
is a program name (of type String
). The second argument is a version number (of type Maybe String
).
main :: IO ()
main = run "program_name" (Just "1.3.2") $
Group "Test program for library"
[ subCmd "greet" greet
, subCmd "connect" connect
]
For more examples, please see example
directory.
Default options
optparse-declarative
provides a few default options. For example, --help
is defined automatically so users do not have to write it by their own. If run with run
and the version number, --version
is defined automatically. Also, --verbosity
option (-v
for short) is defined by default. getVerbosity
returns the verbosity level in Int
. -v
gives 1, -vv
gives 2, -vvv
gives 3. Alternatively, --verbose=3
would yield 3.
How to add a new instance of ArgRead
Users need to create an instance of ArgRead
for supporting a new type for the command line argument. Here is the definition of class ArgRead
.
class ArgRead a where
-- | Type of the argument
type Unwrap a :: *
type Unwrap a = a
-- | Get the argument's value
unwrap :: a -> Unwrap a
default unwrap :: a ~ Unwrap a => a -> Unwrap a
unwrap = id
-- | Argument parser
argRead :: [String] -> Maybe a
default argRead :: Read a => [String] -> Maybe a
argRead ss = getLast $ mconcat $ Last . readMaybe <$> ss
-- | Indicate this argument is mandatory
needArg :: Proxy a -> Bool
needArg _ = True
Suppose you are adding a support for your type T
. We explain which function to define explicitly, depending on the property of T
.
If T
is the type of the final value you take out of a command line, you do not have to define Unwrap
. If T
is a wrapper like Def
, define type Unwrap T = <unwrapped type>
. For Def x y
, type Unwrap (Def x y) = y
. If you defined Unwrap
, define unwrap
that takes an actual value out of the wrapped value.
argRead
is the main function that converts String into a value. If the type is an instance of Read
and you are satisfied with how read
converts a String
into value, there is no need to define your own argRead
. Otherwise, you define a function that converts a String
into a value of the target type. When parsing is successful, return Just
. When it fails, return Nothing
. If the input is []
, it indicates the option does not have an argument; otherwise the input is a list of a single String
. Last but not least, define needArg _ = False
when the option allows us to omit the associated value; consider a boolean option like --help
.