Using Haskell’s CmdArgs Package

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)