module optparse;

/****************************************************************************
 * By Derick Eddington. Placed in the public domain.                        *
 * Inspired by Python's optparse.                                           *
 *                                                                          *
 * See bottom of this file for example usage.                               *
 ****************************************************************************/

// TODO: option sub-groups, slightly indented in help print

private import std.string,
               std.conv,
               std.stdarg,
               std.stdio,
               std.c.stdlib;


enum SupportedTypes
{
	NoValue,
	Int,
	IntArray,
/+ uint is causing some DMD bug...
	UInt,
	UIntArray,+/
/+	Long,
	LongArray,
	ULong,
	ULongArray,+/
	Float,
	FloatArray,
/+	Double,
	DoubleArray,+/
	String,
	StringArray,
}

private template getST (T)
{
	SupportedTypes getST ()
	{
		iftype (T : NoValue)        { return SupportedTypes.NoValue; }
		else iftype (T : int)       { return SupportedTypes.Int; }
		else iftype (T : int[])     { return SupportedTypes.IntArray; }
/+ uint is causing some DMD bug...
		else iftype (T : uint)       { return SupportedTypes.UInt; }
		else iftype (T : uint[])     { return SupportedTypes.UIntArray; }+/
/+		else iftype (T : long)       { return SupportedTypes.Long; }
		else iftype (T : long[])     { return SupportedTypes.LongArray; }
		else iftype (T : ulong)       { return SupportedTypes.ULong; }
		else iftype (T : ulong[])     { return SupportedTypes.ULongArray; }+/
		else iftype (T : float)     { return SupportedTypes.Float; }
		else iftype (T : float[])   { return SupportedTypes.FloatArray; }
/+		else iftype (T : double)     { return SupportedTypes.Double; }
		else iftype (T : double[])   { return SupportedTypes.DoubleArray; }+/
		else iftype (T : char[])    { return SupportedTypes.String; }
		else iftype (T : char[][])  { return SupportedTypes.StringArray; }
		else { throw new UnSupportedTypeError(typeid(T)); }
	}
}


/**
 * Used internally to catch client code attempting
 * to use unsupported types. Does not propogate out of module.
 */
private class UnSupportedTypeError : Error
{
	TypeInfo type;

	this (TypeInfo ti)
	{
		type = ti;
		super(null);
	}
}


/**
 * Errors with optparse module usage. Propogates to client code.
 */
class OptParseError : Error
{
	this (char[] msg)  { super(msg); }
}


/**
 * Errors with command-line options. Causes message to be printed
 * and program exited.  Does not propogate out of module.
 */
private class OptionError : Error
{
	this (char[] msg)  { super(msg); }
}


/**
 * The main class of this module.  Construct it with your program's name
 * and optionally a usage message and version string.  Use addOption!
 * to add command-line options you want to look for.
 */
class OptionParser
{
	private char[] progName, usage, ver;
	private BaseOption[] findOptions;
	private Options defaultValueOptions;

	this (char[] progName, char[] usage = null, char[] ver = null)
	in {
		assert (progName.length);
	}
	body {
		this.progName = progName;
		this.usage = usage;
		this.ver = ver;
		addOption!()("h", "help", "Show this help message and exit.");
		if (ver.length)
			addOption!()(null, "version", "Show program's version number and exit.");
	}

	/**
	 * Use without a type for a no-value/toggle option; or use with one of the
	 * supported types to retrieve the option's value as that D-type.  One of
	 * shortName or longName must be specified, or both.  help is the help message
	 * for the option, printed when the program's help is printed.  metaVar is
	 * a desciptive name for the option's value used in printing options with values.
	 * The varargs is to allow 'T defaultValue' and 'bool mandatory' parameters.
	 * defaultValue is to set a default value for options with values and make that
	 * option always found with that value by default.  mandatory specifies whether
	 * the option must be given on the command-line.  If both defaultValue and
	 * and mandatory are given, mandatory will have no effect.  If defaultValue is
	 * null, it means no default value.  defaultValue must be null for no-value
	 * options that need to specifiy mandatory.
	 */
	template addOption (T = NoValue)
	{
	void addOption (char[] shortName, char[] longName = null, char[] help = null, char[] metaVar = null, ...)
	in {
		assert (shortName.length || longName.length);
		assert (_arguments.length <= 2);
	}
	body {
		debug writefln("%s.addOption()", this);

		foreach (BaseOption o; findOptions) {
			if (shortName.length && shortName == o.shortName)
				throw new OptParseError("conflicting option: -" ~ shortName);
			if (longName.length && longName == o.longName)
				throw new OptParseError("conflicting option: --" ~ longName);
		}

		Option!(T) option = new Option!(T)(shortName, longName, help, metaVar);

		// handle varargs which should be: T defaultValue, bool mandatory
		iftype (T : NoValue) {
			if (_arguments.length > 1) {
				// needed to advance _argptr because mandatory is next vararg
				typeof(null) ignore = va_arg!(typeof(null))(_argptr);
				if (_arguments[0] != typeid(typeof(null)) || ignore != null)
					throw new OptParseError(format("%s option has no value, default value must be null when specifiying mandatory", option));
			}
			else if (_arguments.length)
				throw new OptParseError(format("%s option has no value, so no default value can be specified", option));
		}
		else {
			if (_arguments.length) {
				if (_arguments[0] != typeid(typeof(null))) {
					if (_arguments[0] != typeid(T))
						throw new OptParseError(format("%s option's default value not type %s, is type %s", option, typeid(T), _arguments[0]));
					option.value = va_arg!(T)(_argptr);
					if (option.shortName.length)
						defaultValueOptions[option.shortName] = option;
					if (option.longName.length)
						defaultValueOptions[option.longName] = option;
					//debug writefln("\tdefault value = %s", option.value);
				}
				else if (_arguments.length > 1) {
					// needed to advance _argptr incase mandatory is next vararg
					typeof(null) ignore = va_arg!(typeof(null))(_argptr);
				}
			}
		}
		if (_arguments.length > 1) {
			if (_arguments[1] != typeid(bool))
				throw new OptParseError(format("%s option mandatory must be type %s", option, typeid(bool)));
			option.mandatory = va_arg!(bool)(_argptr);
			debug writefln("\tmandatory = %s", option.mandatory);
		}

		findOptions ~= option;

		debug writefln("\tsuccessfully added %s", option);
	} }

	/**
	 * Parse command-line arguments. args[0] is assumed to be the program-name
	 * and is skipped.  Checks args meets the specified options.  args is reset
	 * to be an array of all non-option arguments (those that do not start with
	 * '-' and do not follow a value-taking option).  Returns an Options which
	 * is used thus to check for found options: ("optname" in options); and
	 * used thus to retrieve option values: options["optname"].getValue!(T).
	 */
	Options parse (inout char[][] args)
	in {
		assert (args.length);
	}
	body {
		debug writefln("%s.parse(args.length=%d)", this, args.length);

		Options foundOptions;
		// copy defaultValueOptions into foundOptions so they're found by default
		foreach (char[] optstring, BaseOption o; defaultValueOptions)
			foundOptions[optstring] = o;
		char[][BaseOption] optsVals;
		char[][] leftoverArgs;

		try {
		// Find args that are options, and get their value strings if they require them.
		// If it's not an option, add it to leftoverArgs.
		// NOTE: args[0] is skipped as it's assumed to be program name
		for (size_t i = 1; i < args.length; i++)
		{	char[] a = args[i];

			if (a[0] == '-' && a.length > 1) {
				bool found;
				if (a[1] == '-' && a.length > 2) {
					// it's a long option
					debug writefln("\tprocessing long-option %s", a);
					foreach (BaseOption o; findOptions) {
						if (o.longName.length  &&  a.find(o.longName) == 2
							&& (a.length-2 == o.longName.length || (a.length-2 > o.longName.length && a[2+o.longName.length] == '=')))
						{
							// given option is one we're looking for
							found = true;
							char[] ov;
							if (o.type != SupportedTypes.NoValue) {
								// get option's arg
								if (a.length-2 > o.longName.length) {
									// option's value is concatenated with option=
									assert (a[2+o.longName.length] == '=');
									ov = a[2+o.longName.length+1 .. $];
								}
								else {
									// option's value should be next in args
									if (i == args.length-1)
										throw new OptionError(format("--%s option requires a value", o.longName));
									ov = args[++i];
								}
							}
							// else option requires no value and ov is null
							optsVals[o] = ov;
							break;  // found so skip the rest of options we're looking for
				}	}	}
				else {  // it's a short option
					debug writefln("\tprocessing short-option %s", a);
					foreach (BaseOption o; findOptions) {
						if (o.shortName.length  &&  a.find(o.shortName) == 1) {
							// given option is one we're looking for
							found = true;
							char[] ov;
							if (o.type != SupportedTypes.NoValue) {
								// get option's arg
								if (a.length-1 > o.shortName.length) {
									// option's value is concatenated with option
									ov = a[o.shortName.length+1 .. $];
								}
								else {
									// option's value should be next in args
									if (i == args.length-1)
										throw new OptionError(format("-%s option requires a value", o.shortName));
									ov = args[++i];
								}
							}
							else if (a.length-1 > o.shortName.length) {
								// no-value option didn't match, ie: -oWRONG
								found = false;
								break;
							}
							// else option requires no value and oa is null
							optsVals[o] = ov;
							break;  // found so skip the rest of options we're looking for
				}	}	}
				if (!found)
					throw new OptionError("no such option: " ~ a);
			}
			else {
				debug writefln("\tleft-over argument ", a);
				leftoverArgs ~= a;
			}
		}

		// Check the found options and parse and check their found values
		foreach (BaseOption o, char[] v; optsVals)
		{
			debug writefln("\tfound option %s with value-string = \"%s\"", o, v);

			if (o.shortName.length)
				foundOptions[o.shortName] = o;
			if (o.longName.length)
				foundOptions[o.longName] = o;

			try {
				switch (o.type)
				{
					case SupportedTypes.NoValue:
						assert (v is null);
						debug writefln("\t\t%s option: has no value to parse", o);
						break;
					case SupportedTypes.Int:
						debug writefln("\t\t%s option: parsing \"%s\" as int", o, v);
						(cast(Option!(int))o).value = toInt(v);
						break;
					case SupportedTypes.IntArray:
						debug writefln("\t\t%s option: parsing \"%s\" as int[]", o, v);
						(cast(Option!(int[]))o).value = parseArray!(int)(v, &toInt);
						break;
/+					case SupportedTypes.UInt:
						debug writefln("\t\t%s option: parsing \"%s\" as uint", o, v);
						(cast(Option!(uint))o).value = toUint(v);
						break;
					case SupportedTypes.UIntArray:
						debug writefln("\t\t%s option: parsing \"%s\" as uint[]", o, v);
						(cast(Option!(uint[]))o).value = parseArray!(uint)(v, &toUint);
						break;+/
/+					case SupportedTypes.Long:
						debug writefln("\t\t%s option: parsing \"%s\" as long", o, v);
						(cast(Option!(long))o).value = toLong(v);
						break;
					case SupportedTypes.LongArray:
						debug writefln("\t\t%s option: parsing \"%s\" as long[]", o, v);
						(cast(Option!(long[]))o).value = parseArray!(long)(v, &toLong);
						break;
					case SupportedTypes.ULong:
						debug writefln("\t\t%s option: parsing \"%s\" as ulong", o, v);
						(cast(Option!(ulong))o).value = toUlong(v);
						break;
					case SupportedTypes.ULongArray:
						debug writefln("\t\t%s option: parsing \"%s\" as ulong[]", o, v);
						(cast(Option!(ulong[]))o).value = parseArray!(ulong)(v, &toUlong);
						break;+/
					case SupportedTypes.Float:
						debug writefln("\t\t%s option: parsing \"%s\" as float", o, v);
						(cast(Option!(float))o).value = toFloat(v);
						break;
					case SupportedTypes.FloatArray:
						debug writefln("\t\t%s option: parsing \"%s\" as float[]", o, v);
						(cast(Option!(float[]))o).value = parseArray!(float)(v, &toFloat);
						break;
/+					case SupportedTypes.Double:
						debug writefln("\t\t%s option: parsing \"%s\" as double", o, v);
						(cast(Option!(double))o).value = toDouble(v);
						break;
					case SupportedTypes.DoubleArray:
						debug writefln("\t\t%s option: parsing \"%s\" as double[]", o, v);
						(cast(Option!(double[]))o).value = parseArray!(double)(v, &toDouble);
						break;+/
					case SupportedTypes.String:
						debug writefln("\t\t%s option: parsing \"%s\" as char[]", o, v);
						(cast(Option!(char[]))o).value = v;
						break;
					case SupportedTypes.StringArray:
						debug writefln("\t\t%s option: parsing \"%s\" as char[][]", o, v);
						(cast(Option!(char[][]))o).value = parseArray!(char[])(v, function char[] (char[] vs) {return vs;});
						break;
				}
			}
			catch (Error e)
				throw new OptionError(format("%s option error parsing value-string \"%s\" as %s", o, v, o._type));
		}

		if ("help" in foundOptions) {
			printHelp();
			exit(0);
		}
		if ("version" in foundOptions) {
			printVersion();
			exit(0);
		}

		// Check mandatory options are in foundOptions
		foreach (BaseOption o; findOptions) {
			if (o.mandatory) {
				bool found;
				foreach (BaseOption fo; foundOptions)
					if (fo is o)
						found = true;
				if (!found)
					throw new OptionError(format("%s option is mandatory", o));
		}	}

		} catch (OptionError oe)
			errorExit(oe.msg);

		debug writefln("%s.parse() done.", this);
		args = leftoverArgs;
		return foundOptions;
	}

	void printHelp ()
	{
		char[] makeIndent (int l)
		{
			char[] i = new char[l];
			i[] = ' ';
			return i;
		}

		char[] alignOptHelpStr (char[] h, size_t ohl)
		{
			if (ohl > 21) {
				char[] ret = "\n";
				char[] indent = makeIndent(24);
				char[][] lines;
				char[][] words = h.split();
				char[] line;
				foreach (char[] word; words) {
					line ~= word ~ " ";
					if (24 + line.length > 72) {
						lines ~= line;
						line = null;
					}
				}
				if (line.length)
					lines ~= line;
				foreach (char[] l; lines)
					ret ~= indent ~ l ~ "\n";
				return ret[0..$-1];
			}
			else {
				return makeIndent(24 - ohl) ~ h;
			}
		}

		char[] help;
		if (usage.length && progName.length) {
			help ~= format("usage:  %s %s", progName, usage);
		}
		if (findOptions.length)
			help ~= "\n\noptions:\n";

		foreach (BaseOption o; findOptions) {
			char[] optHelp = "  ";
			if (o.shortName.length) {
				optHelp ~= "-" ~ o.shortName;
				if (o.metaVar.length)
					optHelp ~= o.metaVar;
				if (o.longName.length)
					optHelp ~= ", ";
			}
			if (o.longName.length) {
				optHelp ~= "--" ~ o.longName;
				if (o.metaVar.length)
					optHelp ~= "=" ~ o.metaVar;
			}
			if (o.help.length)
				optHelp ~= alignOptHelpStr(o.help, optHelp.length);
			help ~= optHelp ~ "\n";
		}

		writef(help);
	}

	void printVersion ()
	{
		writefln("%s %s", progName, ver);
	}

	void errorExit (char[] msg)
	{
		writefln("%s: error: %s\n", progName, msg);
		printHelp();
		exit(1);
	}

	private template parseArray (T) {
	T[] parseArray (char[] vs, T function (char[]) parseT)
	{
		debug writefln("%s.parseArray!(%s)(vs=\"%s\" parseT=%s)", this, typeid(T), vs, parseT);
		T[] a;
		char[][] ss = vs.split(",");
		foreach (char[] s; ss)
			if (s.length)
				a ~= parseT(s);
		return a;
	} }
}


/**
 * Returned from OptionParser.parse().
 * Intended to be used something like:
 * if ("option" in options)
 *     T blah = options["option"].getValue!(T);
 */
typedef BaseOption[char[]] Options;


/**
 * A type that means an option does not have a value.
 */
private class NoValue { }


/**
 * Implements members common to all options and provides template for
 * retrieving option values as the correct type.
 */
private abstract class BaseOption
{
	char[] shortName, longName;
	char[] help, metaVar;
	bool mandatory;
	SupportedTypes type;
	private TypeInfo _type;

	this (SupportedTypes t, TypeInfo ti, char[] sName, char[] lName, char[] h, char[] mVar)
	in {
		assert (sName.length || lName.length);
	}
	body {
		type = t;
		_type = ti;
		shortName = sName;
		longName = lName;
		help = h;
		metaVar = mVar;
		debug writefln("%s.this() done -- _type = %s", this, _type);
	}

	/**
	 * getValue!(T)() -- Returns this option's value as the correct type.
	 *                   If the wrong type is attempted, OptParseError is thrown.
	 */
	template getValue (T)
	{
	T getValue ()
	{
		if (type == SupportedTypes.NoValue)
			throw new OptParseError(format("%s option does not have a value to retrieve", this));

		try {
			if (getST!(T) != type)
				throw new OptParseError(format("%s option must be retrieved as type %s, not type %s", this, _type, typeid(T)));
		}
		catch (UnSupportedTypeError uste)
			throw new OptParseError(format("%s option can not be retrieved as unsupported type %s", this, uste.type));

		return (cast(Option!(T))this).value;
	} }

	char[] toString ()
	{
		return optToString(shortName, longName);
	}
}


private char[] optToString (char[] shortName, char[] longName)
{
	return (shortName.length ? "-"~shortName : "") ~ ((shortName.length && longName.length) ? " / " : "") ~ (longName.length ? "--"~longName : "");
}


/**
 * A command-line option.  Parameterized by a type to specifiy a value
 * is expected and what type, or NoValue to mean the option does not
 * expect a value.
 */
private class Option (T) : BaseOption
{
	iftype (T : NoValue) {}
	else { T value; }

	this (char[] shortName, char[] longName, char[] help, char[] metaVar)
	{
		try {
			super(getST!(T), typeid(T), shortName, longName, help, metaVar);
		}
		catch (UnSupportedTypeError uste) {
			throw new OptParseError(format("%s option can not be unsupported type %s", optToString(shortName,longName), uste.type));
		}
	}
}


unittest
{
	debug writefln("begin %s unittest", __FILE__);

	OptionParser optionParser;
	char[][] args;
	static char[][] shabangDflt = ["zxy","one","qwerty"];

	void newOptionParser ()
	{
		optionParser = new OptionParser(__FILE__[0..$-2], "[option [value]]", "0.0");
		optionParser.addOption!()("v","verbose");
		optionParser.addOption!(float)("f","foo",null,null,null,true);
		optionParser.addOption!(int)("x","xeno","does this and that","THING",123);
		optionParser.addOption!(char[])(null,"bar","make it happen","BLOB","default-value");
		optionParser.addOption!(int[])("a","apple","make applesauce",null);
		optionParser.addOption!(char[][])("sb","shabang","blow up","IT",shabangDflt);
		//optionParser.addOption!()();
		debug writefln("--- new OptionParser's help ---");
		debug optionParser.printHelp();
		debug writefln("--- end ---");
	}

	newOptionParser();
	args = split("myapp lala -v --foo=987.65 --bar bar-string hoho -a1,2,3,4,5 -sb abc,d,ef,zzzzzz asdf");
	Options options = optionParser.parse(args);
	static char[][] leftover = ["lala","hoho","asdf"];
	assert (args == leftover);
	assert ("verbose" in options);
	assert ("f" in options);
	assert (options["foo"].getValue!(float) == 987.65f);
	assert ("bar" in options);
	assert (options["bar"].getValue!(char[]) == "bar-string");
	assert ("apple" in options);
	static int[] appleIA = [1,2,3,4,5];
	assert (options["a"].getValue!(int[]) == appleIA);
	assert ("sb" in options);
	static char[][] shabangCAA = ["abc","d","ef","zzzzzz"];
	assert (options["shabang"].getValue!(char[][]) == shabangCAA);

	args = split("myapp --verbose --apple=, lala hoho asdf --shabang onlyOne -f-0.931e-5");
	options = optionParser.parse(args);
	assert (args == leftover);
	assert ("foo" in options);
	assert (options["f"].getValue!(float) == -0.931e-5f);
	assert ("a" in options);
	assert (options["apple"].getValue!(int[]) == null);
	assert ("shabang" in options);
	static char[][] shabangCAA2 = ["onlyOne"];
	assert (options["sb"].getValue!(char[][]) == shabangCAA2);

	newOptionParser();
	args = split("myapp --foo 1.2");
	options = optionParser.parse(args);
	assert ("shabang" in options);
	assert (options["sb"].getValue!(char[][]) == shabangDflt);
	assert ("x" in options);
	assert (options["xeno"].getValue!(int) == 123);
	assert ("bar" in options);
	assert (options["bar"].getValue!(char[]) == "default-value");

	// check OptionError (these cause process to exit, so can't uncomment by default.
	//                    I checked them all one by one)
	// missing values
	//newOptionParser();
	/+args = split("myapp --bar");
	options = optionParser.parse(args);+/
	// no such option
	/+args = split("myapp --no-such-option");
	options = optionParser.parse(args);+/
	// value parse error
	//args = split("myapp --xeno=oops");
	//options = optionParser.parse(args);
	/+args = split("myapp -f oops");
	options = optionParser.parse(args);+/
	// missing mandatory option --foo
	/+args = split("myapp");
	options = optionParser.parse(args);+/
/+
	bool fails (void delegate () dg)
	{
		bool failed;
		try {
			//debug writefln("fails about to call dg %s", &dg);
			dg();
		}
		catch (OptParseError ope) {
			failed = true;
			debug writefln("expected fail: %s", ope);
		}
		return failed;
	}
+/
	// check OptParseError
	// DMD bug with nested delegates throwing/catching prevents fails() from working,
	// so until that's fixed, below are not assert(fails(...)) so I could test them each
	//newOptionParser();
	// conflicting options
	//assert (fails(delegate void () {optionParser.addOption!(char[])("x");})); // DMD bug: the OptParseError isn't caught by fails
	// optionParser.addOption!(char[])("x");
	// unsupported type
	//optionParser.addOption!(cdouble)("z","zzz");
	//optionParser.addOption!(Object)("o","object");
	// wrong defaultValue type
	//optionParser.addOption!(char[])("z",null,null,null,666);
	// wrong mandatory type
	//optionParser.addOption!(int)("z",null,null,null,null,1);
	// NoValue can not have default value
	//optionParser.addOption!()("z",null,null,null,123);
	// NoValue defaultValue must == null when mandatory
	//optionParser.addOption!()("z",null,null,null,123,true);
	// wrong getValue type
	//args = split("myapp -v asdf --xeno=54321 xyz -f0.99");
	//options = optionParser.parse(args);
	//double wrong = options["xeno"].getValue!(double);
	// getValue on no value option
	//int novalue = options["verbose"].getValue!(int);
	//assert (fails(delegate void () { options["verbose"].getValue!(bool); }));  // causes DMD bug I can't reproduce simpler, if bool b = statment is done, it works...
	//options["verbose"].getValue!(bool);  // similar DMD bug

	debug writefln("%s unittest success", __FILE__);
}


version (OptParseMain)
{
	/**
	 * Working example usage.
	 */
	int main (char[][] args)
	{
		int verbosity;
		char[] outfile;
		char[][] files;

		// constructor args are used in printing help
		// option -h / --help is always added by default and uses program-name and usage
		// option --version (no short name) is added by default if version is given
		//                                   program-name               usage                version
		OptionParser op = new OptionParser(__FILE__[0..$-2], "[options] --files=f1,fN [outfile]", "0.2");

		// int, short and long, help, metavar, no default, not mandatory
		op.addOption!(int)("v", "verbose", "Be more and more and more and more and more and more and more verbose.", "LEVEL");
		// comma-separated string list, short and long, help, metavar, no default, mandatory
		op.addOption!(char[][])("fs", "files", "Files to process, and make this a really long help string as well to test aligning.", "FILE,FILE,...", null, true);
		// float, long-only, help, metavar, default, not mandatory
		op.addOption!(float)(null, "thing", "Do it with this.", "FLOAT", -4.297e21f);
		// no-value, short-only, with help, no metavar, no default, not mandatory
		op.addOption!()("a", null, "Use alternate method.");

		// Parse command-line arguments, including args[0] program-name (it's skipped).
		// Args will be reset to the remaining non-option command-line
		// arguments in the order they appeared.
		Options options = op.parse(args);

		// Check if option was given on the command-line
		// by using (char[] in Options).
		// If the option has both short and long names, either can be used.
		if ("verbose" in options)
			// If option was given and it has a value,
			// retrieve it by indexing Options with short or long name (or only one if only one)
			// and do getValue!(ValueType)
			verbosity = options["verbose"].getValue!(int);

		if (verbosity > 0)
			writefln("I'll say a little.");
		if (verbosity > 1)
			writefln("I'll say a lot more.");

		// args was reset by parse to the remaining non-option command-line arguments
		if (args.length == 1)
			// use the first non-option argument
			outfile = args[0];
		else if (args.length > 1)
			// OptionError.errorExit("message") can be used to print help and exit process;
			// useful when checking non-option arguments
			op.errorExit("more than one output file specified");

		// mandatory options can be assumed to be there after parse(args)
		assert ("files" in options);
		files = options["files"].getValue!(char[][]);
		if (files.length == 0)
			op.errorExit("need to specifiy at least one input file");

		if (verbosity > 0)
		{
			if ("thing" in options)
				writefln("my thing is: ", options["thing"].getValue!(float));

			// no-value options are simply tested for being in Options
			if ("a" in options)
				writefln("I would process files using alternate method:");
			else
				writefln("I would process files using normal method:");
			foreach (char[] f; files)
				writefln("\t",f);
			writefln("and output to ", outfile.length ? outfile : "<stdout>");
		}

		return 0;
	}
}
