Haskell: Using CmdArgs (Single and Multi-Mode)

I use the CmdArgs module all the time for all of my personal Haskell projects. CmdArgs is really the one-stop solution for all of your command-line option handling needs. Its “killer” feature is the ability to support multi-mode options (e.g., “myprog mode1 [mode1 options]“, “myprog mode2 [mode2 options]“, etc.). Unfortunately, not many people know about CmdArgs, it seems. So, I’m writing this post to help spread its popularity among fellow newbie/intermediate Haskellers. I’m including two sample programs — a traditional single-mode program, and a multi-mode program, for your tinkering pleasure. They are hereby released into the PUBLIC DOMAIN.

The following is a simple single-mode program, singleMode.hs.

-- singleMode.hs
-- License: PUBLIC DOMAIN
{-# LANGUAGE DeriveDataTypeable, RecordWildCards #-}

import System.Console.CmdArgs
import System.Environment (getArgs, withArgs)
import System.Exit
import Control.Monad (when)

data MyOptions = MyOptions
    { color :: Bool
    , first_name :: String
    , age :: Int
    , directory :: FilePath
    } deriving (Data, Typeable, Show, Eq)

-- Customize your options, including help messages, shortened names, etc.
myProgOpts :: MyOptions
myProgOpts = MyOptions
    { color = def &= help "use color"
    , first_name = def &= help "your first name"
    , age = def &= explicit &= name "g" &= name "age" &= help "your age"
    , directory = def &= typDir &= help "your first name"
    }

getOpts :: IO MyOptions
getOpts = cmdArgs $ myProgOpts
    &= verbosityArgs [explicit, name "Verbose", name "V"] []
    &= versionArg [explicit, name "version", name "v", summary _PROGRAM_INFO]
    &= summary (_PROGRAM_INFO ++ ", " ++ _COPYRIGHT)
    &= help _PROGRAM_ABOUT
    &= helpArg [explicit, name "help", name "h"]
    &= program _PROGRAM_NAME

_PROGRAM_NAME = "myProg"
_PROGRAM_VERSION = "0.1.2.3"
_PROGRAM_INFO = _PROGRAM_NAME ++ " version " ++ _PROGRAM_VERSION
_PROGRAM_ABOUT = "a sample CmdArgs program for you tinkering pleasure"
_COPYRIGHT = "(C) Your Name Here 2011"

main :: IO ()
main = do
    args <- getArgs
    -- If the user did not specify any arguments, pretend as "--help" was given
    opts <- (if null args then withArgs ["--help"] else id) getOpts
    optionHandler opts

-- Before directly calling your main program, you should warn your user about incorrect arguments, if any.
optionHandler :: MyOptions -> IO ()
optionHandler opts@MyOptions{..}  = do
    -- Take the opportunity here to weed out ugly, malformed, or invalid arguments.
    when (null first_name) $ putStrLn "--first-name is blank!" >> exitWith (ExitFailure 1)
    when (age < 5) $ putStrLn "you must be at least 5 years old to run this program" >> exitWith (ExitFailure 1)
    -- When you're done, pass the (corrected, or not) options to your actual program.
    exec opts

exec :: MyOptions -> IO ()
exec opts@MyOptions{..} = do
    putStrLn $ "Hello, " ++ firstname ++ "!"
    putStrLn $ "You are " ++ showAge ++ " years old."
    where
        firstname = if color
            then "\x1b[1;31m" ++ first_name ++ "\x1b[0m"
            else first_name
        showAge = if color
            then "\x1b[1;32m" ++ show age ++ "\x1b[0m"
            else show age

Screenshot:

Don't you just love how CmdArgs handles all the formatting and line breaking for you automagically? Anyway, onto the discussion.

You first specify your program's options by declaring a data type (in this case, MyOptions). Pretty self-explanatory. The myProgOpts function is where you specify your program's options. You can pass in "annotations" (as CmdArgs calls them) to your options. Here, I used the explicit annotation to prevent CmdArgs from auto-generating the shorter name for the age option. The typ &= "NAME" makes it so that we get "NAME" in the help message for the --first-name option. typDir is just a shortcut for typ &= "DIR" because it's so common.

The def function is just a sane default type for many data types, such as Bool (False), String (""), Int (0), and others. Generally, you should use def unless you really want a different default value for an option.

Notice how the option full_name turned into --full-name? The use of underscores in your options is converted into dashes by CmdArgs. Neat!

The getOpts function has some customizations: we use -v for --version instead of -V (the default), and also use -h for the short name for --help (-? is the default). We also specify the program's metadata (copyright info, etc.).

In the main function, if no arguments are given, we display the default help message (and exit). It took me a while to figure this one out, so take heed!

If you don't want the --verbose or --quiet options, then just remove the line "&= verbosityArgs ...".

The RecordWildCards pragma makes the code extremely concise and easy to use. I must admit, ever since stumbling upon this pragma, I've used it everywhere in my own code in other projects as well.

OK, so that's what a simple single-mode program looks like. How about multi-mode programs? Here is one, multiMode.hs:

-- multiMode.hs
-- License: PUBLIC DOMAIN
{-# LANGUAGE DeriveDataTypeable, RecordWildCards #-}

import System.Console.CmdArgs
import System.Environment (getArgs, withArgs)
import System.Exit
import Control.Monad (when)

data MyOptions =
    Mode1   { first_name :: String
            , last_name :: String
            }
    |
    Mode2   { height :: Double
            , weight :: Double
            } deriving (Data, Typeable, Show, Eq)

mode1 :: MyOptions
mode1 = Mode1
    { first_name = "FIRSTNAME" &= help "your first name"
    , last_name = "LASTNAME" &= help "your last name"
    }
    &= details  [ "Examples:"
                , "Blah blah blah."
                ]

mode2 :: MyOptions
mode2 = Mode2
    { height = def &= help "your height, in centimeters"
    , weight = def &= help "your weight, in kilograms"
    }
    &= details  [ "Examples:"
                , "Blah blah blah again."
                ]

myModes :: Mode (CmdArgs MyOptions)
myModes = cmdArgsMode $ modes [mode1, mode2]
    &= verbosityArgs [explicit, name "Verbose", name "V"] []
    &= versionArg [explicit, name "version", name "v", summary _PROGRAM_INFO]
    &= summary (_PROGRAM_INFO ++ ", " ++ _COPYRIGHT)
    &= help _PROGRAM_ABOUT
    &= helpArg [explicit, name "help", name "h"]
    &= program _PROGRAM_NAME

_PROGRAM_NAME = "myProg"
_PROGRAM_VERSION = "0.1.2.3"
_PROGRAM_INFO = _PROGRAM_NAME ++ " version " ++ _PROGRAM_VERSION
_PROGRAM_ABOUT = "a sample CmdArgs program for you tinkering pleasure"
_COPYRIGHT = "(C) Your Name Here 2011"

main :: IO ()
main = do
    args <- getArgs
    -- If the user did not specify any arguments, pretend as "--help" was given
    opts <- (if null args then withArgs ["--help"] else id) $ cmdArgsRun myModes
    optionHandler opts

optionHandler :: MyOptions -> IO ()
optionHandler opts@Mode1{..}  = do
    when (null first_name) $ putStrLn "warning: --first-name is blank"
    when (null last_name) $ putStrLn "warning: --last-name is blank"
    exec opts
optionHandler opts@Mode2{..}  = do
    when (height == 0.0) $ putStrLn "warning: --height is 0.0"
    when (weight == 0.0) $ putStrLn "warning: --weight is 0.0"
    exec opts

exec :: MyOptions -> IO ()
exec opts@Mode1{..} = putStrLn $ "Hello, " ++ first_name ++ " " ++ last_name ++ "!"
exec opts@Mode2{..} = putStrLn $ "You are " ++ show height ++ "cm tall, and weigh " ++ show weight ++ "kg!"

Screenshot:

The program uses 2 modes, mode1 and mode2. If you use modes without overlapping letters, then you can invoke them with the least amount of unambiguous letters. E.g., if the modes were named foo and bar, you could just do “multiMode f …” and foo mode would be in effect. CmdArgs does this for us.

If your modes have lots and lots of options, then calling “multiMode -h” would give a brief summary, instead of giving each mode’s detailed help message. Here, the modes have only a couple options each, so that’s why the default help message is very detailed.

You might have noticed that I included a details annotation for both modes. This annotation is useful if you want to give examples on which options to use in what way.

Happy coding!

UPDATE April 26, 2011: Victor from the comments asked a question about including a custom type as an option parameter. I never needed this option, but, it’s do-able (with the new CmdArgs 0.6.9). Anyway, here is my minimal example:

instance Default MyComplexType where
    def = MyComplexType { brand = "BRAND"
                        , eggs = 0
                        , flowers = 0
                        , nest = Nestor { books = 0, tag = "TAG", author = "AUTHOR" }
                        }

data MyComplexType = MyComplexType
    { brand :: String
    , eggs :: Int
    , flowers :: Integer
    , nest :: Nestor
    } deriving (Data, Typeable, Show, Eq)

data Nestor = Nestor
    { books :: Int
    , tag :: String
    , author :: String
    } deriving (Data, Typeable, Show, Eq)

data MyOptions = MyOptions
    { color :: Bool
    , first_name :: String
    , age :: Int
    , directory :: FilePath
    , custom :: MyComplexType
    } deriving (Data, Typeable, Show, Eq)

Can you believe it? CmdArgs can even handle nested, custom complex types! Here’s how CmdArgs renders the above, in the help message:

CmdArgs will automatically expect a string of comma-separate values (CSVs). If you don’t like the auto-generated “ITEM,INT,INT,INT,ITEM,…” description, you could easily replace it with a custom string, with a &= typ “X,Y,Z,…” annotation.

If you don’t like the CSV format that CmdArgs imposes on your custom complex type, you’re out of luck. Your best option is to just use a single String type as the option argument, and then parse that with Parsec on your own (probably in the optionHandler function, in case of any errors) and then pass on the parsed data into your main program.

UPDATE July 21, 2011: I tried out CmdArgs with a custom algebraic datatype (something like “data Color = Red | Green | Blue | ColorNone”) and it still works! Your argument for this sort of data type will be, e.g., “–color-rgb red” and the “red” argument will be interpreted as the “Red” type. Actually, CmdArgs is very lenient and will accept any non-ambiguous argument, so here, “–color-rgb r” will also be interpreted as “Red”. Here is an example of the relevant portions:

instance Default Color where
    def = ColorNone

data Color = Red
           | Green
           | Blue
           | ColorNone
    deriving (Data, Typeable, Show, Eq)

myProgOpts = MyOptions
    { ...
    , color_rgb = def &= help "select an RGB color (Red, Green, or Blue)"
    ...
    }

UPDATE August 5, 2011: You can even specify an option as a list:

data Format     = Text
                | TeX
                | HTML
                | FNone
    deriving (Data, Typeable, Show, Eq)

data MyOptions = MyOptions
    { ...
    , format :: [Format]
    , ...
    } deriving (Data, Typeable, Show, Eq)

myProgOpts = MyOptions
    { ...
    , format = [] &= help "format of output"
    , ...
    }

Here, you can do something like “-f TeX -f HTML -f Text” to set the format option to the value [TeX, HTML, Text]. This possibility to use a list as an option parameter is quite useful (such as the example here, where you want to specify multiple output targets).

6 thoughts on “Haskell: Using CmdArgs (Single and Multi-Mode)

  1. Hi Victor — nice timing! Good thing I put this post up a few days before you posted your Stackoverflow question, haha. Cheers.

  2. Something I’m curious about is how CmdArgs translates command line arguments into types (e.g. age actually becoming an Int).

    I would like to do this for my custom types, and (given the error messages) am guessing it has to do with Data/Typeable which are unfamiliar to me. Do you have any insights?

  3. Ah, I had missed this in the docs:

    “Supported Types: Each field in the record must be one of the supported atomic types (String, Int, Integer, Float, Double, Bool, an enumeration, a tuple of atomic types) or a list ([]) or Maybe wrapping at atomic type.”

    So I guess there’s no “neat” way of preprocessing/validating arguments (as with your optionHandler) that are supposed to be used as custom types. My problem is essentially: I have a type “Sfs” that can be parsed from strings with Parsec (returning Either String Sfs for errors). The user should be able to enter a string representation of an Sfs as a command line argument and I obviously want to use it as simply as possible (as a Sfs) inside the program. Oh well.

  4. @Victor: Hmm, I actually have never tried putting in a custom type into my own command line arguments before (just Strings, Ints, etc. are enough for me). But seeing how you need to use the Sfs type which is based on a String type, why not just make it a String type? You could then provide a default, “sane” valid Sfs-parseable String as the default argument, and then when you get the String from the command line, just parse it from the optionHandler. If there are any parsing errors, you could abort your program from optionHandler before turning to the “exec” function. If there are no parsing errors, you could just pass on the resulting Sfs type to the “exec” function as an additional argument. This way, you only parse it once, and abort if there is an error.

    I think the only way to directly insert your custom type into your option’s type is to make typeclass instances of your custom type for the Data and Typeable typeclasses. But that seems unnecessarily painful to me…

  5. @Victor: Oops, that was a mental blackout. Here’s how to directly inject your custom type “Sfs” into the options:

    instance Default Sfs where def = Sfs    { brand = "BRAND"
                                            , eggs = 0
                                            }
    
    data Sfs = Sfs
        { brand :: String
        , eggs :: Int
        } deriving (Data, Typeable, Show, Eq)
    
    data MyOptions = MyOptions
        { color :: Bool
        , first_name :: String
        , age :: Int
        , directory :: FilePath
        , custom :: Sfs
        } deriving (Data, Typeable, Show, Eq)
    

    Pretty darn simple, if you ask me! I will update the post to reflect this possibility.

Comments are closed.