GetOptFu

Haskell logo

Table of Contents

About

Introduction

Buying Groceries

Hacking Internet Routers

Requirements

About

We've written GetOptFu for easy command line argument parsing. It removes the boilerplate code that System.Console.GetOpt inspires. Shortly, this tutorial will describe two programs that make use of GetOptFu: a grocery checkout simulator and a password recovery system.

Introduction

Start by downloading the necessary ingredients.

http://www.delicious.com/mcandre/getoptfu

Open a terminal and your favorite text editor.

The examples can be run (chmodded) as ./example.hs, or as runhaskell example.hs.

Buying Groceries

This program simulates a hungry college student checking out groceries. The program's input and output are relatively simple:

$ chmod +x groceries.hs
$ ./groceries.hs
FoodCo welcomes you, Mr. Derp.
Mr. Derp bought 0 items.
Thank you, come again soon.
$ ./groceries.hs -fart
groceries.hs
  -s <name>       --store=<name>          Specify a store name
  -c <name>       --customer=<name>       Specify a customer name
  -p              --peanutbutter          Itemize peanut butter
  -j              --jelly                 Itemize jelly
  -b[<quantity>]  --bananas[=<quantity>]  Itemize one or more bananas
  -e              --expresslane           Use the express lane
  -h              --help                  Show usage information
  -d              --debug                 Show raw and parsed arguments
$ ./groceries.hs -pj --store FoodInc --customer "Mr. Smith"
FoodInc welcomes you, Mr. Smith.
YOU BOUGHT PEANUT BUTTER!!!
YOU BOUGHT JELLY!!!
Mr. Smith bought 2 items.
Thank you, come again soon.
$ ./groceries.hs -pj --expresslane
FoodCo welcomes you, Mr. Derp.
YOU BOUGHT PEANUT BUTTER!!!
YOU BOUGHT JELLY!!!
Mr. Derp bought 2 items.
Whoosh!
$ ./groceries.hs -pj --expresslane --bananas=9
FoodCo welcomes you, Mr. Derp.
YOU BOUGHT PEANUT BUTTER!!!
YOU BOUGHT JELLY!!!
YOU BOUGHT 9 BANANAS!!!
Mr. Derp bought 11 items.
Too many items for the express lane; items returned.

The interface is simple, but the internals are anything but.

#!/usr/bin/env runhaskell

-- Allows Flag to derive Typeable and Data
{-# LANGUAGE ScopedTypeVariables, DeriveDataTypeable #-}

-- Andrew Pennebaker
-- [email protected]
-- 3 Dec 2010

module Groceries where

import Control.Monad (when)
import GetOptFu

In this bit, the shebang (1) allows Groceries to be run as if it were a Unix utility; the GHC option in line (4) allows automatic data/type derivation; line (10) creates the Groceries module; line (12) imports the when monad; and line (13) imports GetOptFu's three essential functions, parseOpts, getOption, and maybeRead.

Then there is the definition of a Flag data type:

data Flag
	= Store String
	| Customer String
	| PeanutButter
	| Jelly
	| Bananas (Maybe String)
	| ExpressLane
	| Help
	| Debug
	deriving (Eq, Ord, Show, Read, Typeable, Data)

Command line arguments will be converted into one of Store String, Customer String, PeanutButter, ... and so on.

The Flag type is defined in both groceries.hs and ios7crypt.hs. It should be custom made in each particular command line program.

GetOpt doesn't even know what a Flag is, just how to parse them.

options :: [OptDescr Flag]
options = [
	Option "s" ["store"] (ReqArg Store "<name>") "Specify a store name",
	Option "c" ["customer"] (ReqArg Customer "<name>") "Specify a customer name",
	Option "p" ["peanutbutter"] (NoArg PeanutButter) "Itemize peanut butter",
	Option "j" ["jelly"] (NoArg Jelly) "Itemize jelly",
	Option "b" ["bananas"] (OptArg Bananas "<quantity>") "Itemize one or more bananas",
	Option "e" ["expresslane"] (NoArg ExpressLane) "Use the express lane",
	Option "h" ["help"] (NoArg Help) "Show usage information",
	Option "d" ["debug"] (NoArg Debug) "Show raw and parsed arguments"
	]

Each record describes the character arguments (-s); long arguments (--store); whether the argument value is required (ReqArg), optional (OptArg), or omitted (NoArg); the value's format ("<name>") for arguments that take a value; and the function of the argument ("Specify a store name").

Most getopt tutorials stop here. But the hard part it still ahead, the construction of a working main function.

main :: IO ()
main = do
	program <- getProgName
	args <- getArgs
	pArgs <- parseOpts program args options

	let debugMode = getOption pArgs Debug == Just Debug

	when debugMode
		(do
			putStrLn $ "Raw Args: " ++ show args
			putStrLn $ "Parsed Args: " ++ show pArgs)

The functions getProgName and getArgs get the program's filename and arguments, respectively.

parseOpts is a helpful wrapper for Haskell's built-in argument parser, System.Console.GetOpt.getopt.

If the user enters malformed arguments, parseOpts prints out the usage info from options then exits the program. Otherwise, it hands the parsed arguments to the local pArgs variable.

The debugMode is set by attempting to retrieve the (Debug) argument. If the user did not use -d or --debug, then getOption pArgs (Debug) returns Nothing. This would fail the equivalence check with Just Debug, and debugMode would be set to False.

Finally, when examines debugMode. If enabled, two print statements will inform the user of the raw and parsed command line arguments.

Pretty bland, but you gotta start somewhere. You wouldn't believe how elated we were when YOU BOUGHT PEANUT BUTTER!!! finally worked.

Hacking Internet Routers

Disclaimer: The encryption algorithm's weakness is freely acknowledged by the industry. This is no longer super secret spy stuff but mere academia. We do NOT endorse hacking Internet routers other than your own personal property. We are NOT responsible for anything you do as a result of reading this excellent tutorial.

If you haven't done so already, download the necessary ingredients.

This program does something a bit more interesting.

$ ./ios7crypt.hs -e cashmoney
104d080a0d1a1d05091d
$ ./ios7crypt.hs -d 104d080a0d1a1d05091d
cashmoney
$ ./ios7crypt.hs -d 07362e590e1b1c041b1e124c0a2f2e206832752e1a01134d
You really need a life.

The first few lines of code are boilerplate:

#!/usr/bin/env runhaskell

-- Allows Flag to derive Typeable and Data
{-# LANGUAGE ScopedTypeVariables, DeriveDataTypeable #-}

-- Andrew Pennebaker
-- [email protected]
-- 30 Nov 2010

module IOS7Crypt where

import Random (randomRIO)
import Numeric (showInt, showHex)
import Data.List (intercalate)
import Data.Char (ord, chr)
import Bits (xor)
import Test.QuickCheck
import Data.Maybe (fromJust)
import Control.Monad (when)
import GetOptFu

Typical shebang line, module declaration, and imports. Then a module-scoped variable:

xlat' = [
	0x64, 0x73, 0x66, 0x64, 0x3b, 0x6b, 0x66, 0x6f,
	0x41, 0x2c, 0x2e, 0x69, 0x79, 0x65, 0x77, 0x72,
	0x6b, 0x6c, 0x64, 0x4a, 0x4b, 0x44, 0x48, 0x53,
	0x55, 0x42, 0x73, 0x67, 0x76, 0x63, 0x61, 0x36,
	0x39, 0x38, 0x33, 0x34, 0x6e, 0x63, 0x78, 0x76,
	0x39, 0x38, 0x37, 0x33, 0x32, 0x35, 0x34, 0x6b,
	0x3b, 0x66, 0x67, 0x38, 0x37
	]

xlat :: Int -> [Int]
xlat s = (drop s . cycle) xlat'

What's this ugly looking thing? It's a static key used to encrypt Cisco router passwords. Pick a seed, xor the password bytes with key bytes, format in hex, and you're done.

In other words:

encrypt' :: Int -> String -> String
encrypt' seed password
	| seed < 0 || seed > 15 = encrypt' 0 password
	| otherwise = pad2 showInt seed ++ (intercalate "" . map (pad2 showHex) . zipWith xor (xlat seed) . map ord) password

The seed in inexplicably formatted in decimal, though its limits are 0 and 15—the algorithm could have specified it as a single hex digit. But that's neither here nor there.

Flags are of prime importance.

data Flag
	= Encrypt String
	| Decrypt String
	| Test
	| Help
	deriving (Eq, Ord, Show, Read, Typeable, Data)

Then the option descriptions.

options :: [OptDescr Flag]
options = [
	Option "e" ["encrypt"] (ReqArg Encrypt "") "Encrypt a password",
	Option "d" ["decrypt"] (ReqArg Decrypt "") "Decrypt a hash",
	Option "t" ["test"] (NoArg Test) "Unit test IOS7Crypt",
	Option "h" ["help"] (NoArg Help) "Display usage information"
	]

Then the main. Ho hum.

main :: IO ()
main = do
	program <- getProgName
	args <- getArgs
	pArgs <- parseOpts program args options

The most fun you've had all day:

	when (null justTheArgs || getOption pArgs Help == Just Help)
		(do
			putStrLn $ usageInfo program options
			exitSuccess)

	-- Only the first command is observed.
	case head justTheArgs of
		(Encrypt password) -> encrypt password >>= putStrLn
		(Decrypt hash) -> case decrypt hash of
			Just password -> putStrLn password
			_ -> putStrLn "Invalid hash."
		Test -> quickCheck propReversible

The when block quits with usage information if i) no args are present or ii) -h/--help are supplied. This is good interface design, because ios7crypt.hs only does three things:

The many Nothings and Justs are Maybe Flags that provide validation. For GetOptFu, they validate command line input. For IOS7Crypt, they validate hash formats.

And last but not least, quickCheck propReversible throws a hundred random test cases at the encrypt/decrypt functions.

$ ./ios7crypt.hs -t
+++ OK, passed 100 tests.

Requirements