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).

Advertisements

Ugly Xmodmap Caps Lock Workaround

So, I decide to do a system upgrade (including all xorg packages) the other day and suddenly xmodmap isn’t working any more. Specifically, these lines in my .xinitrc no longer do the job (of making Caps lock my mod3 key (for use in Xmonad)):

xmodmap -e "remove Lock = Caps_Lock"
xmodmap -e "add mod3 = Caps_Lock"

So after tons of googling, I found this page: http://forums.gentoo.org/viewtopic-t-857625.html.

Basically, to get the same behavior back, I have to have

remove Lock = Caps_Lock
remove mod3 = Super_L
keysym Caps_Lock = Super_L
add mod3 = Super_L

in my (new) ~/.xmodmap file, and then do this in my ~/.xinitrc:

xmodmap -e "remove Lock = Caps_Lock"
xmodmap -e "add mod3 = Caps_Lock"
xmodmap ~/.xmodmap

This is a very, very ugly hack, but even after spending a solid hour trying manual xmodmap commands and this and that and all sorts of combinations, this is the only method that works.

I have no idea which package exactly broke xmodmap’s former 2-liner functionality, but it was sometime in the past couple months, probably. If the old 2-liner starts working again, I’ll let you all know…