I like to use Haskell to make little utilities for work. This can range from tools to analyze C code to code generation. When creating these utilities, the most tedious part for me is almost always the creation of some sort of command line interface to my code. Argument parsing is a pain in almost every language, and for some reason, it’s always felt worse to me in Haskell.
Well, recently I discovered the CmdArgs package by Neil Mitchell. This package takes nearly all of the pain out of creating command line interfaces around Haskell applications. It takes about as much effort as Ruby’s Thor library does to create something internally useful to your program and pleasing to the user.
Setting Up a Script to use CmdArgs
Setting up is simple.
First, install the CmdArgs package:
$ cabal install cmdargs
Next, import it into our script:
module Main where
import System.Console.CmdArgs
The library requires that we define a type that represents the options we want to expose to the user. This type needs to be an instance of both Data
and Typeable
. You can use GHC’s extension DeriveDataTypeable
to do this for you. Add the following line to the top of your script so that your whole file looks like this:
{-# LANGUAGE DeriveDataTypeable #-}
module Main where
import System.Console.CmdArgs
That’s it for setup. Let’s now let’s actually use CmdArgs
.
Usage
To use the library, we need to define a data type which will contain our options. After declaring the data type, we need to make an instance of that type with the fields filled in with what the default values are if nothing is passed on the command line for that option. Here’s a simple variant of a “hello world” program that allows us to specify to who we should say “hello”. If we don’t specify any one, the program will say “hello” to the world.
{-# LANGUAGE DeriveDataTypeable #-}
module Main where
import System.Console.CmdArgs
data Options = Options {
sayHelloTo :: String
} deriving (Data, Typeable)
options :: Options
options = Options { sayHelloTo = "World" }
main :: IO ()
main = do
opts <- cmdArgs options
putStrLn $ "Hello " ++ sayHelloTo opts
Here’s the output from this script:
$ runhaskell greeter.hs
Hello World
Okay, that’s not very interesting. We can check what flags were automatically assigned to our single option by passing the -?
flag to our program. Doing so yields this:
$ runhaskell greeter.hs -?
The options program
options [OPTIONS]
Common flags:
-s --sayhelloto=ITEM
-? --help Display help message
-V --version Print version information
Looks like we can use -s
or --sayhelloto
to specify the person we want to say hello to. Predictably, here’s what this looks like:
$ runhaskell greeter.hs -s John
Hello John
Let’s add another option. We can specify how many times to say “hello” by adding a line to each of our type and our instance:
data Options = Options {
sayHelloTo :: String,
times :: Int
} deriving (Data, Typeable)
options :: Options
options = Options { sayHelloTo = "World", times = 1 }
How do we specify this new parameter? We can consult -?
again to find out:
$ runhaskell greeter.hs -?
The options program
options [OPTIONS]
Common flags:
-s --sayhelloto=ITEM
-t --times=INT
-? --help Display help message
-V --version Print version information
After updating the program to account for the new times
option, we can say “Hello” to me 5 times like this:
$ runhaskell greeter.hs -s John -t 5
Hello John
Hello John
Hello John
Hello John
Hello John
Cool. That was easy. What about more complex types like lists? Say we wanted to greet from none to many people. Well, we can do that by making one change to each of our type and our instance and updating our code to handle the change:
data Options = Options {
sayHelloTo :: [String],
times :: Int
} deriving (Data, Typeable)
options :: Options
options = Options { sayHelloTo = [], times = 1 }
All we did here was convert our String
type to a list of strings ([String]
). Having done this, we can now specify the -s
flag a variable number of times:
$ runhaskell greeter.hs
Hello nobody.
$ runhaskell greeter.hs -s John
Hello John
$ runhaskell greeter.hs -s John -s Justin
Hello John and Justin
$ runhaskell greeter.hs -s John -s Justin -s Carl
Hello John, Justin and Carl
Now, let’s spend some time cleaning up the way our options are specified. The first thing we notice is that the ITEM
description on the -s
flag should probably be NAME
. We can change this by specifying an annotation on our default value in the instance. Changing our instance to the following will make this change:
options :: Options
options = Options { sayHelloTo = []
&= typ "NAME"
, times = 1
}
Rerunning with -?
now yeilds this:
$ runhaskell greeter.hs -?
The options program
options [OPTIONS]
Common flags:
-s --sayhelloto=NAME
-t --times=INT
-? --help Display help message
-V --version Print version information
We can also put some help text on both of these options:
options :: Options
options = Options { sayHelloTo = []
&= typ "NAME"
&= help "A name to greet"
, times = 1
&= help "The number of times to greet"
}
This change gives us:
$ runhaskell greeter.hs -?
The options program
options [OPTIONS]
Common flags:
-s --sayhelloto=NAME A name to greet.
-t --times=INT The number of times to greet.
-? --help Display help message
-V --version Print version information
Lastly, let’s clean up the summary text. We can do this with the summary
and program
annotations. Note: these annotations are applied directly to the instance rather than fields of the instance. Here is the final version of our instance:
options :: Options
options = Options { sayHelloTo = []
&= typ "NAME"
&= help "A name to greet"
, times = 1
&= help "The number of times to greet"
}
&= summary "Greeter v1.0, (C) John Van Enk 2012"
&= program "greeter"
Here is it’s final output:
$ runhaskell greeter.hs -?
Greeter v1.0, (C) John Van Enk 2012
greeter [OPTIONS]
Common flags:
-s --sayhelloto=NAME A name to greet
-t --times=INT The number of times to greet
-? --help Display help message
-V --version Print version information
In the end, we have a rich command line interface that results in a type we can easily integrate into our program.
You can read about more ways to use the CmdArgs package in its Haddock documentation found on Hackage.
Oh, and here’s the greeter.hs
program in its entirety:
{-# LANGUAGE DeriveDataTypeable #-}
module Main where
import Data.List
import System.Console.CmdArgs
data Options = Options {
sayHelloTo :: [String],
times :: Int
} deriving (Data, Typeable)
options :: Options
options = Options { sayHelloTo = []
&= typ "NAME"
&= help "A name to greet"
, times = 1
&= help "The number of times to greet"
}
&= summary "Greeter v1.0, (C) John Van Enk 2012"
&= program "greeter"
main :: IO ()
main = do
opts <- cmdArgs options let greeting' = greeting $ sayHelloTo opts mapM_ putStrLn $ take (times opts) (repeat greeting') greeting :: [String] -> String
greeting [] = "Hello nobody."
greeting [n] = "Hello " ++ n
greeting [n,m] = unwords ["Hello", n, "and", m]
greeting ns = let first = concat $ intersperse ", " (init ns)
in "Hello " ++ first ++ " and " ++ (last ns)