[cmdreader] 01/05: Imported Upstream version 1.5

komal sukhani komal-guest at moszumanska.debian.org
Mon Aug 3 15:18:12 UTC 2015


This is an automated email from the git hooks/post-receive script.

komal-guest pushed a commit to branch master
in repository cmdreader.

commit 1e95d746ae836e2d5230a2962db51f3ef144ebdf
Author: Komal Sukhani <komaldsukhani at gmail.com>
Date:   Mon Aug 3 19:26:22 2015 +0530

    Imported Upstream version 1.5
---
 LICENSE                                            |  19 +
 README.markdown                                    | 105 ++++
 build.xml                                          | 203 +++++++
 buildScripts/ivy.xml                               |  12 +
 buildScripts/ivysettings.xml                       |  13 +
 lib/.gitignore                                     |   3 +
 src/com/zwitserloot/cmdreader/CmdReader.java       | 600 +++++++++++++++++++++
 src/com/zwitserloot/cmdreader/Description.java     |  38 ++
 src/com/zwitserloot/cmdreader/Excludes.java        |  39 ++
 src/com/zwitserloot/cmdreader/ExcludesGroup.java   |  40 ++
 src/com/zwitserloot/cmdreader/FullName.java        |  39 ++
 .../cmdreader/InvalidCommandLineException.java     |  36 ++
 src/com/zwitserloot/cmdreader/Mandatory.java       |  43 ++
 src/com/zwitserloot/cmdreader/ParseItem.java       | 393 ++++++++++++++
 src/com/zwitserloot/cmdreader/Requires.java        |  40 ++
 src/com/zwitserloot/cmdreader/Sequential.java      |  47 ++
 src/com/zwitserloot/cmdreader/Shorthand.java       |  49 ++
 src/com/zwitserloot/cmdreader/package-info.java    |  27 +
 test/com/zwitserloot/cmdreader/TestCmdReader.java  | 333 ++++++++++++
 19 files changed, 2079 insertions(+)

diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..513a630
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,19 @@
+Copyright © 2010-2011 Reinier Zwitserloot.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/README.markdown b/README.markdown
new file mode 100644
index 0000000..83938ea
--- /dev/null
+++ b/README.markdown
@@ -0,0 +1,105 @@
+# com.zwitserloot.cmdreader
+
+## How to use
+
+First, create a class to represent your command line options. Create one field for each command line option you want, then use the annotations of the `com.zwitserloot.cmdreader` package on the field to configure that option. Then, create a `CmdReader` instance and use it to parse a command line into an instance of your class. You can also use a `CmdReader` instance to generate comprehensive command line help.
+
+For the full manual, you should visit the javadoc, which you can build yourself with `ant javadoc`, after which it'll be available in `doc/api/index.html`. For a quick intro, check out the example program at the end of this documentation.
+
+### Example program
+
+	import com.zwitserloot.cmdreader.*;
+	
+	public class Test {
+		static class CmdArgs {
+			@Shorthand("x")
+			@Description("Excludes the given file.")
+			java.util.List<String> exclude;
+			
+			@Sequential
+			@Mandatory
+			@Description("The directory to turn into a compressed archive.")
+			String compress;
+		}
+		
+		public static void main(String[] rawArgs) {
+			CmdReader<CmdArgs> reader = CmdReader.of(CmdArgs.class);
+			CmdArgs args;
+			try {
+				args = reader.make(rawArgs);
+			} catch (InvalidCommandLineException e) {
+				System.err.println(e.getMessage());
+				System.err.println(reader.generateCommandLineHelp("java Test"));
+				return;
+			}
+			
+			System.err.println("If this was a real program, I would now compress " + args.compress);
+			System.err.println("And I'd be excluding: " + args.exclude);
+		}
+	}
+
+### All annotation options
+
+#### @Description
+Describes this option. This description is used by `CmdReader.generateCommandLineHelp` to generate the command line help message, and nowhere else.
+
+#### @FullName
+By default an option's full name is equal to its field name. If you want to override this, for example because you'd like your option to include a dash
+which is not a legal java identifier character, you can do so here. Any option can be accessed using the "--fullname(=value)" syntax, and full names are also used
+to refer to other options.
+
+#### @Shorthand
+A shorthand is a single character which can be used as a short alternative. For example:
+
+	@Shorthand("h") boolean help;
+
+Can be set to true either via `--help` or via `-h`. You can include more than one shorthand character if you want.
+Note that multiple shorthands can be chained together if they are booleans, i.e. if 'x' and  'y' are booleans, and 'b' is a String::
+
+	java -jar yourapp.jar -xyb valueForB
+
+#### @Excludes
+Tells CmdReader that this option cannot co-exist with the listed other options. For example:
+
+	String foo;
+	@Excludes("foo") boolean bar;
+
+Means that an error will be generated when the user attempts to pass `--foo Hello --bar` on the command line.
+
+Multiple options can be listed in one `@Excludes`.
+
+#### @ExcludesGroup
+List any number of unique group names as parameter to the `@ExcludesGroup` method. Any command line that lists more than one option that both share
+an excludes group is treated as an invalid command line. This is useful if you have a number of 'modes' which are all mutually exclusive. For example, a
+backup or unzip tool can various mutually exclusive modes:
+
+	@Shorthand("c") @ExcludesGroup("mode") boolean createArchive;
+	@Shorthand("x") @ExcludesGroup("mode") boolean extractArchive;
+	@Shorthand("v") @ExcludesGroup("mode") boolean verifyArchive;
+
+#### @Mandatory
+Used to indicate an option is required. Optionally you can include the `onlyIf` or `onlyIfNot` parameter, listing any number of other options, to fine-tune
+when this option is mandatory and when it isn't. `@Mandatory` on a field with a collection type implies at least one such option must be present.
+
+#### @Sequential
+Used to indicate that this option is the 'default' and that no switch is required on the command line for it. For example, if your command line tool works on one
+file, then you can allow your app to be invoked as `java -jar yourapp.jar filename` if you add this annotation:
+
+	@Mandatory @Sequential String fileName;
+
+Multiple sequential arguments are allowed; exactly one such argument may be a collection type, and it does not have to be the last one. Example for a copy program:
+
+	@Mandatory @Sequential(1) List<String> from;
+	@Mandatory @Sequential(2) String to;
+
+#### @Requires
+Used to indicate that if this option is present, the listed option must also be present. Like `@Mandatory` but this one is put on the other option.
+
+## How to compile / develop
+
+run:
+
+	ant
+
+and that's that. All dependencies will be downloaded automatically. The jar file you need will be in the `dist` directory. If you want to work on the code, run 'ant eclipse' or 'ant intellij' to set up the project dir as an eclipse or intellij project.
+
diff --git a/build.xml b/build.xml
new file mode 100644
index 0000000..880e090
--- /dev/null
+++ b/build.xml
@@ -0,0 +1,203 @@
+<!--
+  Copyright © 2010-2011 Reinier Zwitserloot.
+  
+  Permission is hereby granted, free of charge, to any person obtaining a copy
+  of this software and associated documentation files (the "Software"), to deal
+  in the Software without restriction, including without limitation the rights
+  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the Software is
+  furnished to do so, subject to the following conditions:
+  
+  The above copyright notice and this permission notice shall be included in
+  all copies or substantial portions of the Software.
+  
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+  THE SOFTWARE.
+-->
+<project name="com.zwitserloot.cmdreader" default="dist" xmlns:ivy="antlib:com.zwitserloot.ivyplusplus">
+	<description>
+com.zwitserloot.cmdreader is a library to read command line arguments for java.
+To use it, first build a class containing fields, with each field representing a command line option.
+Then, annotate each field to configure settings (such as whether a parameter can show up more than once, is mandatory, etc),
+and finally call CmdReader.of(ThatClass.class).make(cmdargs) to get an instance of your class, with all fields filled in.
+	</description>
+	<property name="version" value="1.5" />
+	
+	<property name="build.compiler" value="javac1.6" />
+	<property name="ivy.retrieve.pattern" value="lib/[conf]/[artifact].[ext]" />
+	<property name="ivyplusplus.location" value="http://projectlombok.org/downloads/ivyplusplus.jar" />
+	<available file="lib/ivyplusplus.jar" property="ivyplusplus.available" />
+	
+	<path id="build.path">
+		<fileset dir="lib/build">
+			<include name="*.jar" />
+		</fileset>
+	</path>
+	
+	<path id="test.path">
+		<fileset dir="lib/test">
+			<include name="*.jar" />
+		</fileset>
+	</path>
+	
+	<target name="download-ipp" unless="ivyplusplus.available">
+		<mkdir dir="lib" />
+		<get src="${ivyplusplus.location}" dest="lib/ivyplusplus.jar" usetimestamp="true" />
+	</target>
+	
+	<target name="load-ipp" depends="download-ipp">
+		<taskdef classpath="lib/ivyplusplus.jar" resource="com/zwitserloot/ivyplusplus/antlib.xml" uri="antlib:com.zwitserloot.ivyplusplus" />
+		<ivy:ensureippversion version="1.5" property="ipp.versionOkay" />
+	</target>
+	
+	<target name="checkver-ipp" depends="load-ipp" unless="ipp.versionOkay">
+		<get src="${ivyplusplus.location}" dest="lib/ivyplusplus.jar" />
+		<fail>ivyplusplus has been updated to a new version. Restart the script to continue.</fail>
+	</target>
+	
+	<target name="ensure-ipp" depends="load-ipp, checkver-ipp" />
+	
+	<target name="config-ivy" depends="ensure-ipp" unless="ivy.config">
+		<ivy:configure file="buildScripts/ivysettings.xml" />
+		<property name="ivy.config" value="true" />
+	</target>
+	
+	<target name="deps" depends="ensureBuildDeps, ensureRuntimeDeps, ensureTestDeps" description="Downloads all dependencies." />
+	
+	<target name="ensureBuildDeps" depends="config-ivy">
+		<mkdir dir="lib/build" />
+		<ivy:resolve file="buildScripts/ivy.xml" refresh="true" conf="build" />
+		<ivy:retrieve />
+	</target>
+	
+	<target name="ensureRuntimeDeps" depends="config-ivy">
+		<mkdir dir="lib/runtime" />
+		<ivy:resolve file="buildScripts/ivy.xml" refresh="true" conf="runtime" />
+		<ivy:retrieve />
+	</target>
+	
+	<target name="ensureTestDeps" depends="config-ivy">
+		<mkdir dir="lib/test" />
+		<ivy:resolve file="buildScripts/ivy.xml" refresh="true" conf="test" />
+		<ivy:retrieve />
+	</target>
+	
+	<target name="clean" description="Deletes build artifacts.">
+		<delete quiet="true" dir="dist" />
+		<delete quiet="true" dir="build" />
+		<delete quiet="true" dir="doc/api" />
+	</target>
+	
+	<target name="distclean" depends="clean" description="Deletes everything this build script has ever generated.">
+		<delete dir="lib" quiet="true" />
+		<delete dir="dist" quiet="true" />
+		<delete file=".project" quiet="true" />
+		<delete file=".classpath" quiet="true" />
+		<delete dir=".settings" quiet="true" />
+		<delete dir=".idea" quiet="true" />
+		<delete file="cmdreader.iml" quiet="true" />
+	</target>
+	
+	<target name="compile" depends="ensureBuildDeps" description="Compiles program code">
+		<mkdir dir="build/prog" />
+		<ivy:compile destdir="build/prog" target="1.5" source="1.5">
+			<src path="src" />
+			<classpath refid="build.path" />
+		</ivy:compile>
+	</target>
+	
+	<target name="compileTests" depends="compile, ensureTestDeps" description="Compiles test code">
+		<ivy:compile destdir="build/tests" source="1.5" target="1.5">
+			<src path="test" />
+			<classpath refid="test.path" />
+			<classpath>
+				<pathelement path="build/prog" />
+			</classpath>
+		</ivy:compile>
+	</target>
+	
+	<target name="dist" depends="compile, -test.quiet, -test, javadoc" description="Creates the distributable">
+		<mkdir dir="dist" />
+		<jar destfile="dist/com.zwitserloot.cmdreader-${version}.jar" basedir="build/prog" />
+		<copy file="dist/com.zwitserloot.cmdreader-${version}.jar" tofile="dist/com.zwitserloot.cmdreader.jar" />
+		<zip destfile="dist/com.zwitserloot.cmdreader-javadoc-${version}.zip" basedir="doc/api" />
+		<zip destfile="dist/com.zwitserloot.cmdreader-src-${version}.zip">
+			<zipfileset dir="." prefix="com.zwitserloot.cmdreader/">
+				<include name="LICENSE" />
+				<include name="README.markdown" />
+				<include name="src/**" />
+				<include name="build.xml" />
+				<include name="buildScripts/**" />
+				<include name="test/**" />
+			</zipfileset>
+		</zip>
+	</target>
+	
+	<target name="-test.quiet">
+		<property name="tests.quiet" value="true" />
+	</target>
+	
+	<target name="-test" depends="compileTests" unless="skipTests">
+		<junit haltonfailure="yes" fork="on">
+			<formatter type="plain" usefile="false" unless="tests.quiet" />
+			<classpath refid="test.path" />
+			<classpath>
+				<pathelement path="build/prog" />
+				<pathelement path="build/tests" />
+			</classpath>
+			<batchtest>
+				<fileset dir="test">
+					<include name="**/Test*.java" />
+				</fileset>
+			</batchtest>
+		</junit>
+		<echo level="info">All tests successful.</echo>
+	</target>
+	
+	<target name="test" depends="-test" description="Runs the unit tests" />
+	
+	<target name="javadoc" description="Generates the javadoc to doc/api">
+		<delete dir="doc/api" quiet="true" />
+		<mkdir dir="doc/api" />
+		<javadoc sourcepath="src" defaultexcludes="yes" destdir="doc/api" windowtitle="com.zwitserloot.cmdreader">
+			<classpath refid="build.path" />
+			<link href="http://java.sun.com/javase/6/docs/api/" />
+		</javadoc>
+	</target>
+	
+	<target name="contrib" depends="config-ivy" description="Downloads various non-crucial documentation, sources, etc that are useful when developing lombok.ast.">
+		<ivy:resolve file="buildScripts/ivy.xml" refresh="true" conf="contrib" />
+		<ivy:retrieve />
+	</target>
+	
+	<target name="intellij" depends="deps, contrib" description="Creates intellij project files and downloads all dependencies. Open this directory as a project in IntelliJ after running this target.">
+		<ivy:intellijgen>
+			<conf name="build" sources="contrib" />
+			<conf name="test" sources="contrib" />
+			<module name="cmdreader" depends="build, test">
+				<srcdir dir="src" />
+				<srcdir dir="test" test="true" />
+			</module>
+			<settings>
+				<url url="http://projectlombok.org/downloads/lombok.intellij.settings" />
+			</settings>
+		</ivy:intellijgen>
+	</target>
+	
+	<target name="eclipse" depends="deps, contrib" description="Creates eclipse project files and downloads all dependencies. Open this directory as project in eclipse after running this target.">
+		<ivy:eclipsegen>
+			<srcdir dir="src" />
+			<srcdir dir="test" />
+			<conf name="build" sources="contrib" />
+			<conf name="test" sources="contrib" />
+			<settings>
+				<url url="http://projectlombok.org/downloads/lombok.eclipse.settings" />
+			</settings>
+		</ivy:eclipsegen>
+	</target>
+</project>
diff --git a/buildScripts/ivy.xml b/buildScripts/ivy.xml
new file mode 100644
index 0000000..7237863
--- /dev/null
+++ b/buildScripts/ivy.xml
@@ -0,0 +1,12 @@
+<ivy-module version="2.0">
+	<info organisation="com.zwitserloot" module="cmdreader" />
+	<configurations>
+		<conf name="build" />
+		<conf name="runtime" />
+		<conf name="test" extends="build, runtime" />
+		<conf name="contrib" />
+	</configurations>
+	<dependencies>
+		<dependency org="junit" name="junit" rev="4.8.2" conf="test->default" />
+	</dependencies>
+</ivy-module>
diff --git a/buildScripts/ivysettings.xml b/buildScripts/ivysettings.xml
new file mode 100644
index 0000000..5ec5356
--- /dev/null
+++ b/buildScripts/ivysettings.xml
@@ -0,0 +1,13 @@
+<ivysettings>
+	<resolvers>
+		<chain name="projectRepos">
+			<filesystem name="projectLocalRepo">
+				<ivy pattern="${ivy.settings.dir}/ivy-repo/[organization]-[module]-[revision].xml" />
+			</filesystem>
+			<ibiblio name="maven-repo2" m2compatible="true" root="http://repo2.maven.org/maven2" />
+			<ibiblio name="maven-repo2" m2compatible="true" root="http://uk.maven.org/maven2" />
+		</chain>
+	</resolvers>
+	<settings defaultResolver="projectRepos" />
+	<caches defaultCacheDir="${ivy.basedir}/ivyCache" />
+</ivysettings>
diff --git a/lib/.gitignore b/lib/.gitignore
new file mode 100644
index 0000000..57d3346
--- /dev/null
+++ b/lib/.gitignore
@@ -0,0 +1,3 @@
+build
+runtime
+ivy.jar
diff --git a/src/com/zwitserloot/cmdreader/CmdReader.java b/src/com/zwitserloot/cmdreader/CmdReader.java
new file mode 100644
index 0000000..715b28b
--- /dev/null
+++ b/src/com/zwitserloot/cmdreader/CmdReader.java
@@ -0,0 +1,600 @@
+/*
+ * Copyright © 2010-2011 Reinier Zwitserloot.
+ * 
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ * 
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ * 
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.zwitserloot.cmdreader;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Parses command line arguments.
+ * 
+ * The CmdReader can turn strings like<br>
+ * <code>-fmv /foo/bar /bar/baz --color=white *.xyzzy *.cheese</code><br>
+ * into something that easier to work with programatically.
+ * <p>
+ * To use it, first create a 'descriptor' class; this is a class that contains just fields and no code.
+ * Each field represents a command line option. The type of each field can be any primitive or primitive wrapper, or String,
+ * or arrays / lists / sets of such types.
+ * 
+ * Annotate each field with the various annotations in the cmdreader package
+ * to influence its behaviour. A short description of all annotations (they are all optional):
+ * <dl>
+ * <dt>FullName
+ * <dd>(defaults to field name). The name of the option. Used in the Excludes and Mandatory annotations,
+ *  and also allowed on the command line itself as --fullname(=value)
+ * <dt>Shorthand
+ * <dd>Instead of --fullname, -shorthand is also allowed. You can have as many shorthands as you want. Usually single characters.
+ * <dt>Description
+ * <dd>A human readable description of the field. Used to auto-generate command line help.
+ * <dt>Excludes
+ * <dd>A list of names (see FullName) that cannot co-exist together with this option. If this option is present
+ *  as well as one of the excluded ones, an error will be generated.
+ * <dt>ExcludesGroup
+ * <dd>A list of keywords. An error is generated if two or more options that share an <code>@ExcludesGroup</code> keyword are present.
+ * This feature is useful for selecting various mutually exclusive modes of operation, such as 'pack, unpack, test' for a compression tool.
+ * <dt>Mandatory
+ * <dd>Indicates that the option must be present. You may optionally specify 'onlyIf' and 'onlyIfNot', which are lists of
+ *  names (see FullName). onlyIf means: This is mandatory only if at least one of these options is present. onlyIfNot means:
+ *  I am not optional only if one of these options is present, otherwise I am optional. On fields of a collection type, this means at least
+ *  one such option must be present.
+ *  <dt>Sequential
+ *  <dd>Use me if there is no option name. In other words, those command line options that do not 'belong' to a -something,
+ *  go here. You can have as many as you like, but only one can be a collection type. It is an error if a Sequential
+ *  annotated field is of type boolean. If you have multiple {@code @Sequential} options, you need to specify the ordering.
+ *  </dl>
+ *  
+ *  Fields that do not show up in the command line arguments aren't modified, so if you want a default, just set the field in the descriptor class.
+ */
+public class CmdReader<T> {
+	private final Class<T> settingsDescriptor;
+	private final List<ParseItem> items;
+	private final Map<Character, ParseItem> shorthands;
+	private final List<ParseItem> seqList;
+	private final int collectionSeqIndex;
+	
+	private CmdReader(Class<T> settingsDescriptor) {
+		this.settingsDescriptor = settingsDescriptor;
+		this.items = Collections.unmodifiableList(init());
+		this.shorthands = ParseItem.makeShortHandMap(this.items);
+		this.seqList = makeSeqList(this.items);
+		int cSI = -1;
+		for (int i = 0; i < seqList.size(); i++) if (seqList.get(i).isCollection()) cSI = i;
+		this.collectionSeqIndex = cSI;
+	}
+	
+	/**
+	 * Create a new CmdReader with the specified class as the template that defines what each option means.
+	 * 
+	 * See the class javadoc for more information on how to make these classes.
+	 * 
+	 * @param settingsDescriptor Class with fields which will be filled with the command line variables.
+	 * @throws IllegalArgumentException If the <em>settingsDescriptor</em> contains flaws, for example
+	 *   if two different fields both use the same 'full name'. See the class javadoc for a more complete list of causes.
+	 */
+	public static <T> CmdReader<T> of(Class<T> settingsDescriptor) {
+		return new CmdReader<T>(settingsDescriptor);
+	}
+	
+	private List<ParseItem> init() {
+		Class<?> c = settingsDescriptor;
+		List<ParseItem> out = new ArrayList<ParseItem>();
+		
+		while (c != Object.class) {
+			Field[] fields = settingsDescriptor.getDeclaredFields();
+			for (Field field : fields) {
+				field.setAccessible(true);
+				if (Modifier.isStatic(field.getModifiers())) continue;
+				out.add(new ParseItem(field));
+			}
+			c = c.getSuperclass();
+		}
+		
+		ParseItem.multiSanityChecks(out);
+		return out;
+	}
+	
+	private static List<ParseItem> makeSeqList(List<ParseItem> items) {
+		List<ParseItem> seqList = new ArrayList<ParseItem>();
+		int lowest = Integer.MAX_VALUE;
+		for (ParseItem  item : items) if (item.isSeq()) lowest = Math.min(item.getSeqOrder(), lowest);
+		
+		while (true) {
+			int nextLowest = lowest;
+			for (ParseItem item : items) if (item.isSeq() && item.getSeqOrder() >= lowest) {
+				if (item.getSeqOrder() == lowest) seqList.add(item);
+				else if (nextLowest == lowest) nextLowest = item.getSeqOrder();
+				else if (item.getSeqOrder() < nextLowest) nextLowest = item.getSeqOrder();
+			}
+			if (nextLowest == lowest) break;
+			lowest = nextLowest;
+		}
+		
+		ParseItem.multiSeqSanityChecks(seqList);
+		
+		return seqList;
+	}
+	
+	private static final int SCREEN_WIDTH = 72;
+	
+	/**
+	 * Generates an extensive string explaining the command line options.
+	 * You should print this if the make() method throws an InvalidCommandLineException, for example.
+	 * The detail lies between your average GNU manpage and your average GNU command's output if you specify --help.
+	 * 
+	 * Is automatically wordwrapped at standard screen width (72 characters). Specific help for each option is mostly
+	 * gleaned from the {@link com.zwitserloot.cmdreader.Description} annotations.
+	 * 
+	 * @param commandName used to prefix the example usages.
+	 */
+	public String generateCommandLineHelp(String commandName) {
+		StringBuilder out = new StringBuilder();
+		
+		int maxFullName = 0;
+		int maxShorthand = 0;
+		
+		for (ParseItem item : items) {
+			if (item.isSeq()) continue;
+			maxFullName = Math.max(maxFullName, item.getFullName().length() + (item.isParameterized() ? 4 : 0));
+			maxShorthand = Math.max(maxShorthand, item.getShorthand().length());
+		}
+		
+		if (maxShorthand == 0) maxShorthand++;
+		
+		maxShorthand = maxShorthand * 3 -1;
+		
+		generateShortSummary(commandName, out);
+		generateSequentialArgsHelp(out);
+		generateMandatoryArgsHelp(maxFullName, maxShorthand, out);
+		generateOptionalArgsHelp(maxFullName, maxShorthand, out);
+		return out.toString();
+	}
+	
+	private void generateShortSummary(String commandName, StringBuilder out) {
+		if (commandName != null && commandName.length() > 0) out.append(commandName).append(" ");
+		
+		StringBuilder sb = new StringBuilder();
+		for (ParseItem item : items) if (!item.isSeq() && !item.isMandatory()) sb.append(item.getShorthand());
+		if (sb.length() > 0) {
+			out.append("[-").append(sb).append("] ");
+			sb.setLength(0);
+		}
+		
+		for (ParseItem item : items) if (!item.isSeq() && item.isMandatory()) sb.append(item.getShorthand());
+		if (sb.length() > 0) {
+			out.append("-").append(sb).append(" ");
+			sb.setLength(0);
+		}
+		
+		for (ParseItem item : items) if (!item.isSeq() && item.isMandatory() && item.getShorthand().length() == 0) {
+			out.append("--").append(item.getFullName()).append("=val ");
+		}
+		
+		for (ParseItem item : items) if (item.isSeq()) {
+			if (!item.isMandatory()) out.append('[');
+			out.append(item.getFullName());
+			if (!item.isMandatory()) out.append(']');
+			out.append(' ');
+		}
+		out.append("\n");
+	}
+	
+	private void generateSequentialArgsHelp(StringBuilder out) {
+		List<ParseItem> items = new ArrayList<ParseItem>();
+		
+		for (ParseItem item : this.seqList) if (item.getFullDescription().length() > 0) items.add(item);
+		if (items.size() == 0) return;
+		
+		int maxSeqArg = 0;
+		for (ParseItem item : items) maxSeqArg = Math.max(maxSeqArg, item.getFullName().length());
+		
+		out.append("\n  Sequential arguments:\n");
+		for (ParseItem item : items) generateSequentialArgHelp(maxSeqArg, item, out);
+	}
+	
+	private void generateMandatoryArgsHelp(int maxFullName, int maxShorthand, StringBuilder out) {
+		List<ParseItem> items = new ArrayList<ParseItem>();
+		for (ParseItem item : this.items) if (item.isMandatory() && !item.isSeq()) items.add(item);
+		
+		if (items.size() == 0) return;
+		
+		out.append("\n  Mandatory arguments:\n");
+		for (ParseItem item : items) generateArgHelp(maxFullName, maxShorthand, item, out);
+	}
+	
+	private void generateOptionalArgsHelp(int maxFullName, int maxShorthand, StringBuilder out) {
+		List<ParseItem> items = new ArrayList<ParseItem>();
+		for (ParseItem item : this.items) if (!item.isMandatory() && !item.isSeq()) items.add(item);
+		
+		if (items.size() == 0) return;
+		
+		out.append("\n  Optional arguments:\n");
+		for (ParseItem item : items) generateArgHelp(maxFullName, maxShorthand, item, out);
+	}
+	
+	private void generateArgHelp(int maxFullName, int maxShorthand, ParseItem item, StringBuilder out) {
+		out.append("    ");
+		String fn = item.getFullName() + (item.isParameterized() ? "=val" : "");
+		out.append(String.format("--%-" + maxFullName + "s ", fn));
+		
+		StringBuilder sh = new StringBuilder();
+		for (char c : item.getShorthand().toCharArray()) {
+			if (sh.length() > 0) sh.append(" ");
+			sh.append("-").append(c);
+		}
+		
+		out.append(String.format("%-" + maxShorthand + "s ", sh));
+		
+		int left = SCREEN_WIDTH - 8 - maxShorthand - maxFullName;
+		
+		String description = item.getFullDescription();
+		if (description.length() == 0 || description.length() < left) {
+			out.append(description).append("\n");
+			return;
+		}
+		
+		for (String line : wordbreak(item.getFullDescription(), SCREEN_WIDTH -8)) {
+			out.append("\n        ").append(line);
+		}
+		out.append("\n");
+	}
+	
+	private void generateSequentialArgHelp(int maxSeqArg, ParseItem item, StringBuilder out) {
+		out.append("    ");
+		out.append(String.format("%-" + maxSeqArg + "s   ", item.getFullName()));
+		
+		int left = SCREEN_WIDTH - 7 - maxSeqArg;
+		
+		String description = item.getFullDescription();
+		if (description.length() == 0 || description.length() < left) {
+			out.append(description).append("\n");
+			return;
+		}
+		
+		for (String line : wordbreak(item.getFullDescription(), SCREEN_WIDTH - 8)) {
+			out.append("\n        ").append(line);
+		}
+		out.append("\n");
+	}
+	
+	private static List<String> wordbreak(String text, int width) {
+		StringBuilder line = new StringBuilder();
+		List<String> out = new ArrayList<String>();
+		int lastSpace = -1;
+		
+		for (char c : text.toCharArray()) {
+			if (c == '\t') c = ' ';
+			
+			if (c == '\n') {
+				out.add(line.toString());
+				line.setLength(0);
+				lastSpace = -1;
+				continue;
+			}
+			
+			if (c == ' ') {
+				lastSpace = line.length();
+				line.append(' ');
+			} else line.append(c);
+			
+			if (line.length() > width && lastSpace > 8) {
+				out.add(line.substring(0, lastSpace));
+				String left = line.substring(lastSpace+1);
+				line.setLength(0);
+				line.append(left);
+				lastSpace = -1;
+			}
+		}
+		
+		if (line.length() > 0) out.add(line.toString());
+		
+		return out;
+	}
+	
+	/**
+	 * Parses the provided command line into an instance of your command line descriptor class.
+	 * 
+	 * The string is split on spaces. However, quote symbols can be used to prevent this behaviour. To write literal quotes,
+	 * use the \ escape. a double \\ is seen as a single backslash.
+	 * 
+	 * @param in A command line string, such as -frl "foo bar"
+	 * @throws InvalidCommandLineException If the user entered a wrong command line.
+	 *           The exception's message is english and human readable - print it back to the user!
+	 * @throws IllegalArgumentException If your descriptor class has a bug in it. A list of causes can be found in the class javadoc.
+	 */
+	public T make(String in) throws InvalidCommandLineException, IllegalArgumentException {
+		List<String> out = new ArrayList<String>();
+		StringBuilder sb = new StringBuilder();
+		boolean inQuote = false;
+		boolean inBack = false;
+		
+		for (char c : in.toCharArray()) {
+			if (inBack) {
+				inBack = false;
+				if (c == '\n') continue;
+				sb.append(c);
+			}
+			
+			if (c == '\\') {
+				inBack = true;
+				continue;
+			}
+			
+			if (c == '"') {
+				inQuote = !inQuote;
+				continue;
+			}
+			
+			if (c == ' ' && !inQuote) {
+				String p = sb.toString();
+				sb.setLength(0);
+				if (p.equals("")) continue;
+				out.add(p);
+				continue;
+			}
+			sb.append(c);
+		}
+		
+		if (sb.length() > 0) out.add(sb.toString());
+		
+		return make(out.toArray(new String[out.size()]));
+	}
+	
+	/**
+	 * Parses the provided command line into an instance of your command line descriptor class.
+	 * 
+	 * Each part of the string array is taken literally as a single argument; quotes are not parsed and things are not split
+	 * on spaces. Normally the shell does this for you.
+	 * 
+	 * If you want to parse the String[] passed to java <code>main</code> methods, use this method.
+	 * 
+	 * @param in A command line string chopped into pieces, such as ["-frl", "foo bar"].
+	 * @throws InvalidCommandLineException If the user entered a wrong command line.
+	 *           The exception's message is english and human readable - print it back to the user!
+	 * @throws IllegalArgumentException If your descriptor class has a bug in it. A list of causes can be found in the class javadoc.
+	 */
+	public T make(String[] in) throws InvalidCommandLineException {
+		final T obj = construct();
+		
+		if (in == null) in = new String[0];
+		
+		class State {
+			List<ParseItem> used = new ArrayList<ParseItem>();
+			
+			void handle(ParseItem item, String value) {
+				item.set(obj, value);
+				used.add(item);
+			}
+			
+			void finish() throws InvalidCommandLineException {
+				checkForGlobalMandatories();
+				checkForExcludes();
+				checkForGroupExcludes();
+				checkForRequires();
+				checkForMandatoriesIf();
+				checkForMandatoriesIfNot();
+			}
+			
+			private void checkForGlobalMandatories() throws InvalidCommandLineException {
+				for (ParseItem item : items) if (item.isMandatory() && !used.contains(item))
+					throw new InvalidCommandLineException(
+						"You did not specify mandatory parameter " + item.getFullName());
+			}
+			
+			private void checkForExcludes() throws InvalidCommandLineException {
+				for (ParseItem item : items) if (used.contains(item)) {
+					for (String n : item.getExcludes()) {
+						for (ParseItem i : items) if (i.getFullName().equals(n) && used.contains(i))
+							throw new InvalidCommandLineException(
+									"You specified parameter " + i.getFullName() +
+									" which cannot be used together with " + item.getFullName());
+					}
+				}
+			}
+			
+			private void checkForGroupExcludes() throws InvalidCommandLineException {
+				for (ParseItem item : items) if (used.contains(item)) {
+					for (String n : item.getExcludesGroup()) {
+						for (ParseItem i : items) {
+							if (i == item || !used.contains(i)) continue;
+							if (i.getExcludesGroup().contains(n)) {
+								throw new InvalidCommandLineException(
+										"You specified parameter " + i.getFullName() +
+										" which cannot be used together with " + item.getFullName());
+							}
+						}
+					}
+				}
+			}
+			
+			private void checkForRequires() throws InvalidCommandLineException {
+				for (ParseItem item : items) if (used.contains(item)) {
+					for (String n : item.getRequires()) {
+						for (ParseItem i : items) if (i.getFullName().equals(n) && !used.contains(i))
+							throw new InvalidCommandLineException(
+									"You specified parameter " + item.getFullName() +
+									" which requires that you also supply " + i.getFullName());
+					}
+				}
+			}
+			
+			private void checkForMandatoriesIf() throws InvalidCommandLineException {
+				for (ParseItem item : items) {
+					if (used.contains(item) || item.getMandatoryIf().size() == 0) continue;
+					for (String n : item.getMandatoryIf()) {
+						for (ParseItem i : items) if (i.getFullName().equals(n) && used.contains(i))
+							throw new InvalidCommandLineException(
+									"You did not specify parameter " + item.getFullName() +
+									" which is mandatory if you use " + i.getFullName());
+					}
+				}
+			}
+			
+			private void checkForMandatoriesIfNot() throws InvalidCommandLineException {
+				nextItem:
+				for (ParseItem item : items) {
+					if (used.contains(item) || item.getMandatoryIfNot().size() == 0) continue;
+					for (String n : item.getMandatoryIfNot()) {
+						for (ParseItem i : items) if (i.getFullName().equals(n) && used.contains(i))
+							continue nextItem;
+					}
+					
+					StringBuilder alternatives = new StringBuilder();
+					if (item.getMandatoryIfNot().size() > 1) alternatives.append("one of ");
+					for (String n : item.getMandatoryIfNot()) alternatives.append(n).append(", ");
+					alternatives.setLength(alternatives.length() - 2);
+					
+					throw new InvalidCommandLineException(
+							"You did not specify parameter " + item.getFullName() +
+							" which is mandatory unless you use " + alternatives);
+				}
+			}
+		}
+		
+		State state = new State();
+		List<String> seqArgs = new ArrayList<String>();
+		
+		for (int i = 0; i < in.length; i++) {
+			if (in[i].startsWith("--")) {
+				int idx = in[i].indexOf('=');
+				String key = idx == -1 ? in[i].substring(2) : in[i].substring(2, idx);
+				String value = idx == -1 ? "" : in[i].substring(idx+1);
+				if (value.length() == 0 && idx != -1) throw new InvalidCommandLineException(
+						"invalid command line argument - you should write something after the '=': " + in[i]);
+				boolean handled = false;
+				for (ParseItem item : items) if (item.getFullName().equalsIgnoreCase(key)) {
+					if (item.isParameterized() && value.length() == 0) {
+						if (i < in.length - 1 && !in[i+1].startsWith("-")) value = in[++i];
+						else throw new InvalidCommandLineException(String.format(
+								"invalid command line argument - %s requires a parameter but there is none.", key));
+					}
+					state.handle(item, !item.isParameterized() && value.length() == 0 ? null : value);
+					handled = true;
+					break;
+				}
+				if (!handled) throw new InvalidCommandLineException(
+						"invalid command line argument - I don't know about that option: " + in[i]);
+			} else if (in[i].startsWith("-")) {
+				for (char c : in[i].substring(1).toCharArray()) {
+					ParseItem item = shorthands.get(c);
+					if (item == null) throw new InvalidCommandLineException(String.format(
+							"invalid command line argument - %s is not a known option: %s", c, in[i]));
+					if (item.isParameterized()) {
+						String value;
+						if (i < in.length - 1 && !in[i+1].startsWith("-")) value = in[++i];
+						else throw new InvalidCommandLineException(String.format(
+								"invalid command line argument - %s requires a parameter but there is none.", c));
+						state.handle(item, value);
+					} else state.handle(item, null);
+				}
+			} else {
+				seqArgs.add(in[i]);
+			}
+		}
+		
+		if (collectionSeqIndex == -1 && seqArgs.size() > seqList.size()) {
+			throw new InvalidCommandLineException(String.format(
+					"invalid command line argument - you've provided too many free-standing arguments: %s", seqArgs.get(seqList.size())));
+		}
+		
+		if (collectionSeqIndex == -1) {
+			for (int i = 0; i < seqArgs.size(); i++) {
+				ParseItem item = seqList.get(i);
+				state.handle(item, seqArgs.get(i));
+			}
+		} else {
+			int totalCollectionSize = seqArgs.size() - seqList.size() + 1;
+			
+			int argsIdx = 0;
+			int optIdx = 0;
+			int colIdx = 0;
+			while (argsIdx < seqArgs.size()) {
+				if (optIdx < collectionSeqIndex) {
+					ParseItem item = seqList.get(optIdx);
+					state.handle(item, seqArgs.get(argsIdx));
+					optIdx++;
+					argsIdx++;
+				} else if (optIdx == collectionSeqIndex) {
+					ParseItem item = seqList.get(optIdx);
+					while (colIdx < totalCollectionSize) {
+						state.handle(item, seqArgs.get(argsIdx));
+						colIdx++;
+						argsIdx++;
+					}
+					optIdx++;
+				} else {
+					ParseItem item = seqList.get(optIdx);
+					state.handle(item, seqArgs.get(argsIdx));
+					optIdx++;
+					argsIdx++;
+				}
+			}
+		}
+		
+		state.finish();
+		
+		return obj;
+	}
+	
+	private T construct() {
+		try {
+			Constructor<T> constructor = settingsDescriptor.getDeclaredConstructor();
+			constructor.setAccessible(true);
+			return constructor.newInstance();
+		} catch (NoSuchMethodException e) {
+			throw new IllegalArgumentException(String.format(
+					"A CmdReader class must have a no-args constructor: %s", settingsDescriptor));
+		} catch (InstantiationException e) {
+			throw new IllegalArgumentException(String.format(
+					"A CmdReader class must not be an interface or abstract: %s", settingsDescriptor));
+		} catch (IllegalAccessException e) {
+			throw new IllegalArgumentException("Huh?");
+		} catch (InvocationTargetException e) {
+			throw new IllegalArgumentException(
+					"Exception occurred when constructing CmdReader class " + settingsDescriptor, e.getCause());
+		}
+	}
+	
+	/**
+	 * Turns a list of strings, such as "Hello", "World!" into a single string, each element separated by a space.
+	 * Use it if you want to grab the rest of the command line as a single string, spaces and all; include a @Sequential
+	 * List of Strings and run squash on it to do this.
+	 */
+	public static String squash(Collection<String> collection) {
+		Iterator<String> i = collection.iterator();
+		StringBuilder out = new StringBuilder();
+		
+		while (i.hasNext()) {
+			out.append(i.next());
+			if (i.hasNext()) out.append(' ');
+		}
+		
+		return out.toString();
+	}
+}
diff --git a/src/com/zwitserloot/cmdreader/Description.java b/src/com/zwitserloot/cmdreader/Description.java
new file mode 100644
index 0000000..7191112
--- /dev/null
+++ b/src/com/zwitserloot/cmdreader/Description.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright © 2010-2011 Reinier Zwitserloot.
+ * 
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ * 
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ * 
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.zwitserloot.cmdreader;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * A human readable description of the field. Used to auto-generate command line help.
+ */
+ at Retention(value=RetentionPolicy.RUNTIME)
+ at Target(value=ElementType.FIELD)
+ at Documented
+public @interface Description {
+	String value();
+}
diff --git a/src/com/zwitserloot/cmdreader/Excludes.java b/src/com/zwitserloot/cmdreader/Excludes.java
new file mode 100644
index 0000000..dda66ef
--- /dev/null
+++ b/src/com/zwitserloot/cmdreader/Excludes.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright © 2010-2011 Reinier Zwitserloot.
+ * 
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ * 
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ * 
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.zwitserloot.cmdreader;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * A list of names (either the {@code @FullName}, or if not present, the field name of another option) that cannot co-exist together with this option.
+ * If this option is present as well as one of the excluded ones, a {@link InvalidCommandLineException} will be thrown.
+ */
+ at Retention(value=RetentionPolicy.RUNTIME)
+ at Target(value=ElementType.FIELD)
+ at Documented
+public @interface Excludes {
+	String[] value();
+}
diff --git a/src/com/zwitserloot/cmdreader/ExcludesGroup.java b/src/com/zwitserloot/cmdreader/ExcludesGroup.java
new file mode 100644
index 0000000..ae81cf7
--- /dev/null
+++ b/src/com/zwitserloot/cmdreader/ExcludesGroup.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright © 2010-2011 Reinier Zwitserloot.
+ * 
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ * 
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ * 
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.zwitserloot.cmdreader;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * A list of one or more keywords. An {@link InvalidCommandLineException} is thrown if two or more options that share an {@code @ExcludesGroup} keyword are present.
+ * 
+ * This feature is useful for selecting various mutually exclusive modes of operation, such as 'pack, unpack, test' for a compression tool.
+ */
+ at Retention(value=RetentionPolicy.RUNTIME)
+ at Target(value=ElementType.FIELD)
+ at Documented
+public @interface ExcludesGroup {
+	String[] value() default {"default"};
+}
diff --git a/src/com/zwitserloot/cmdreader/FullName.java b/src/com/zwitserloot/cmdreader/FullName.java
new file mode 100644
index 0000000..d6aa66d
--- /dev/null
+++ b/src/com/zwitserloot/cmdreader/FullName.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright © 2010-2011 Reinier Zwitserloot.
+ * 
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ * 
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ * 
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.zwitserloot.cmdreader;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * The name of the option (optional - defaults to field name).
+ * Used in the Excludes and Mandatory annotations, and also allowed on the command line itself as {@code --fullname(=value)}
+ */
+ at Retention(value=RetentionPolicy.RUNTIME)
+ at Target(value=ElementType.FIELD)
+ at Documented
+public @interface FullName {
+	String value() default "";
+}
diff --git a/src/com/zwitserloot/cmdreader/InvalidCommandLineException.java b/src/com/zwitserloot/cmdreader/InvalidCommandLineException.java
new file mode 100644
index 0000000..bd7eba2
--- /dev/null
+++ b/src/com/zwitserloot/cmdreader/InvalidCommandLineException.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright © 2010-2011 Reinier Zwitserloot.
+ * 
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ * 
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ * 
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.zwitserloot.cmdreader;
+
+/**
+ * This exception is thrown if the command line is not valid, for example because it contains unrecognized options,
+ * or one of the constraints (such as one created with {@code @Excludes}) is broken.
+ * 
+ * The {@link #getMessage()} method will contain an english, human readable explanation of what's wrong with the command line.
+ */
+public class InvalidCommandLineException extends Exception {
+	private static final long serialVersionUID = 20080509L;
+	
+	public InvalidCommandLineException(String message) {
+		super(message);
+	}
+}
diff --git a/src/com/zwitserloot/cmdreader/Mandatory.java b/src/com/zwitserloot/cmdreader/Mandatory.java
new file mode 100644
index 0000000..49a0009
--- /dev/null
+++ b/src/com/zwitserloot/cmdreader/Mandatory.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright © 2010-2011 Reinier Zwitserloot.
+ * 
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ * 
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ * 
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.zwitserloot.cmdreader;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Indicates that the option must be present. You may optionally specify {@code onlyIf} or {@code onlyIfNot}, which are lists of
+ * option names.
+ */
+ at Retention(value=RetentionPolicy.RUNTIME)
+ at Target(value=ElementType.FIELD)
+ at Documented
+public @interface Mandatory {
+	/** If present, this option is mandatory only if at least one of these options are also present. */
+	String[] onlyIf() default {};
+	
+	/** If present, this option is mandatory if all options listed here are <em>not</em> present. */
+	String[] onlyIfNot() default {};
+}
diff --git a/src/com/zwitserloot/cmdreader/ParseItem.java b/src/com/zwitserloot/cmdreader/ParseItem.java
new file mode 100644
index 0000000..b1e44cf
--- /dev/null
+++ b/src/com/zwitserloot/cmdreader/ParseItem.java
@@ -0,0 +1,393 @@
+/*
+ * Copyright © 2010-2011 Reinier Zwitserloot.
+ * 
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ * 
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ * 
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.zwitserloot.cmdreader;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.util.AbstractCollection;
+import java.util.AbstractList;
+import java.util.AbstractSequentialList;
+import java.util.AbstractSet;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Queue;
+import java.util.Set;
+
+class ParseItem {
+	private final List<Class<?>> LEGAL_CLASSES = Collections.unmodifiableList(Arrays.<Class<?>>asList(
+			Integer.class, Long.class, Short.class, Byte.class, Float.class, Double.class, Boolean.class, Character.class,
+			String.class, Enum.class
+			));
+	
+	private final Field field;
+	private final boolean isCollection;
+	private final Class<?> type;
+	private final String fullName;
+	private final boolean isSeq;
+	private final int seqOrder;
+	private final boolean isParameterized;
+	private final boolean isMandatory;
+	private final String shorthand;
+	private final String description;
+	private final List<String> excludes;
+	private final List<String> excludesGroup;
+	private final List<String> mandatoryIf;
+	private final List<String> mandatoryIfNot;
+	private final List<String> requires;
+	
+	ParseItem(Field field) {
+		this.field = field;
+		field.setAccessible(true);
+		
+		Class<?> rawType;
+		if (Collection.class.isAssignableFrom(field.getType())) {
+			isCollection = true;
+			Type genericType = field.getGenericType();
+			Type[] typeArgs = null;
+			if (genericType instanceof ParameterizedType)
+				typeArgs = ((ParameterizedType)genericType).getActualTypeArguments();
+			if (typeArgs != null && typeArgs.length == 1 && typeArgs[0] instanceof Class<?>)
+				rawType = (Class<?>)typeArgs[0];
+			else throw new IllegalArgumentException(String.format(
+					"Only primitives, Strings, Enums, and Collections of those are allowed (for type: %s)", field.getGenericType()));
+		} else {
+			isCollection = false;
+			rawType = field.getType();
+		}
+		
+		if (rawType == int.class) this.type = Integer.class;
+		else if (rawType == long.class) this.type = Long.class;
+		else if (rawType == short.class) this.type = Short.class;
+		else if (rawType == byte.class) this.type = Byte.class;
+		else if (rawType == double.class) this.type = Double.class;
+		else if (rawType == float.class) this.type = Float.class;
+		else if (rawType == char.class) this.type = Character.class;
+		else if (rawType == boolean.class) this.type = Boolean.class;
+		else this.type = rawType;
+		
+		if (!LEGAL_CLASSES.contains(type)) throw new IllegalArgumentException("Not a valid class for command line parsing: " + field.getGenericType());
+		
+		this.fullName = setupFullName(field);
+		Sequential seq = field.getAnnotation(Sequential.class);
+		this.isSeq = seq != null;
+		this.seqOrder = seq == null ? 0 : seq.value();
+		this.isParameterized = field.getType() != boolean.class && field.getType() != Boolean.class;
+		this.shorthand = setupShorthand(field);
+		this.description = setupDescription(field);
+		this.isMandatory = setupMandatory(field);
+		this.mandatoryIf = setupMandatoryIf(field);
+		this.mandatoryIfNot = setupMandatoryIfNot(field);
+		this.requires = setupRequires(field);
+		this.excludes = setupExcludes(field);
+		this.excludesGroup = setupExcludesGroup(field);
+		
+		try {
+			sanityChecks();
+		} catch (IllegalArgumentException e) {
+			throw new IllegalArgumentException(String.format("%s (at %s)", e.getMessage(), fullName));
+		}
+	}
+	
+	private void sanityChecks() {
+		if (!isParameterized && Boolean.class != type) throw new IllegalArgumentException("Non-parameterized parameters must have type boolean. - it's there (true), or not (false).");
+		if (!isParameterized && isMandatory) throw new IllegalArgumentException("Non-parameterized parameters must not be mandatory - what's the point of having it?");
+		if (isSeq && !"".equals(shorthand)) throw new IllegalArgumentException("sequential parameters must not have any shorthands.");
+		if (isSeq && !isParameterized) throw new IllegalArgumentException("sequential parameters must always be parameterized.");
+	}
+	
+	static void multiSanityChecks(List<ParseItem> items) {
+		int len = items.size();
+		
+		// No two ParseItems must have the same full name.
+		for (int i = 0; i < len; i++) for (int j = i+1; j < len; j++) {
+			if (items.get(i).fullName.equalsIgnoreCase(items.get(j).fullName))
+				throw new IllegalArgumentException(String.format(
+						"Duplicate full names for fields %s and %s.",
+						items.get(i).field.getName(), items.get(j).field.getName()));
+		}
+		
+		// Only one isSeq may be a collection.
+		ParseItem isCollectionIsSeq = null;
+		for (ParseItem item : items) {
+			if (!item.isSeq) continue;
+			if (item.isSeq && item.isCollection) {
+				if (isCollectionIsSeq != null) throw new IllegalArgumentException(String.format(
+						"More than one @Sequential item is a collection (only one is allowed): %s %s",
+						isCollectionIsSeq.getFullName(), item.getFullName()));
+				isCollectionIsSeq = item;
+			}
+		}
+		
+		// No two sequential items share the same order number.
+		for (int i = 0; i < items.size(); i++) {
+			for (int j = i + 1; j < items.size(); j++) {
+				if (!items.get(i).isSeq() || !items.get(j).isSeq()) continue;
+				if (items.get(i).getSeqOrder() == items.get(j).getSeqOrder()) throw new IllegalArgumentException(String.format(
+						"Two @Sequential items have the same value; use @Sequential(10) to specify the ordering: %s %s",
+						items.get(i).getFullName(), items.get(j).getFullName()));
+			}
+		}
+	}
+	
+	static void multiSeqSanityChecks(List<ParseItem> seqItems) {
+		// If the Xth isSeq is mandatory, every isSeq below X must also be mandatory, unless that isSeq is a collection.
+		ParseItem firstNonMandatoryIsSeq = null;
+		for (ParseItem item : seqItems) {
+			if (firstNonMandatoryIsSeq == null && !item.isMandatory() && !item.isCollection()) firstNonMandatoryIsSeq = item;
+			if (item.isMandatory() && firstNonMandatoryIsSeq != null) throw new IllegalArgumentException(String.format(
+					"Sequential item %s is non-mandatory, so %s which is a later sequential item must also be non-mandatory",
+					firstNonMandatoryIsSeq.fullName, item.fullName));
+		}
+		
+		// If there is a collection sequential entry, then all sequential entries after it must be mandatory, or
+		// its not possible to tell the difference between supplying more to the collection or supplying an optional later sequential.
+		ParseItem collectionSeq = null;
+		for (ParseItem item : seqItems) {
+			if (collectionSeq == null) {
+				if (item.isCollection()) collectionSeq = item;
+			} else {
+				if (!item.isMandatory()) throw new IllegalArgumentException(String.format(
+						"Sequential item %s is non-mandatory, but earlier sequential item %s is a collection; this is ambiguous",
+						item, collectionSeq));
+			}
+		}
+	}
+	
+	static Map<Character, ParseItem> makeShortHandMap(List<ParseItem> items) {
+		Map<Character, ParseItem> out = new HashMap<Character, ParseItem>();
+		
+		for (ParseItem item : items) for (char c : item.shorthand.toCharArray()) {
+			if (out.containsKey(c)) throw new IllegalArgumentException(String.format(
+					"Both %s and %s contain the shorthand %s",
+					out.get(c).fullName, item.fullName, c));
+			else out.put(c, item);
+		}
+		
+		return out;
+	}
+	
+	String getFullName() {
+		return fullName;
+	}
+	
+	boolean isSeq() {
+		return isSeq;
+	}
+	
+	int getSeqOrder() {
+		return seqOrder;
+	}
+	
+	boolean isMandatory() {
+		return isMandatory;
+	}
+	
+	List<String> getMandatoryIf() {
+		return mandatoryIf;
+	}
+	
+	List<String> getMandatoryIfNot() {
+		return mandatoryIfNot;
+	}
+	
+	List<String> getRequires() {
+		return requires;
+	}
+	
+	List<String> getExcludes() {
+		return excludes;
+	}
+	
+	List<String> getExcludesGroup() {
+		return excludesGroup;
+	}
+	
+	boolean isParameterized() {
+		return isParameterized;
+	}
+	
+	boolean isCollection() {
+		return isCollection;
+	}
+	
+	String getFullDescription() {
+		return description;
+	}
+	
+	private static final List<Class<?>> ARRAY_LIST_COMPATIBLES = Collections.unmodifiableList(
+			Arrays.<Class<?>>asList(Collection.class, AbstractCollection.class, List.class, AbstractList.class, ArrayList.class));
+	private static final List<Class<?>> HASH_SET_COMPATIBLES = Collections.unmodifiableList(
+			Arrays.<Class<?>>asList(Set.class, AbstractSet.class, HashSet.class));
+	private static final List<Class<?>> LINKED_LIST_COMPATIBLES = Collections.unmodifiableList(
+			Arrays.<Class<?>>asList(AbstractSequentialList.class, Queue.class, LinkedList.class));
+	
+	@SuppressWarnings("unchecked")
+	void set(Object o, String value) {
+		Object v = stringToObject(value);
+		
+		try {
+			if (isCollection) {
+				Collection<Object> l = (Collection<Object>)field.get(o);
+				if (l == null) {
+					if (ARRAY_LIST_COMPATIBLES.contains(field.getType())) l = new ArrayList<Object>();
+					else if (LINKED_LIST_COMPATIBLES.contains(field.getType())) l = new LinkedList<Object>();
+					else if (HASH_SET_COMPATIBLES.contains(field.getType())) l = new HashSet<Object>();
+					else throw new IllegalArgumentException("Cannot construct a collection of type " + field.getType() + " -- try List, Set, Collection, or Queue.");
+					field.set(o, l);
+				}
+				l.add(v);
+			} else field.set(o, v);
+		} catch (IllegalAccessException e) {
+			throw new IllegalArgumentException("Huh?");
+		}
+	}
+	
+	@SuppressWarnings({"rawtypes", "unchecked"})
+	private Object stringToObject(String raw) {
+		if (String.class == type) return raw;
+		if (Integer.class == type) return Integer.parseInt(raw);
+		if (Long.class == type) return Long.parseLong(raw);
+		if (Short.class == type) return Short.parseShort(raw);
+		if (Byte.class == type) return Byte.parseByte(raw);
+		if (Float.class == type) return Float.parseFloat(raw);
+		if (Double.class == type) return Double.parseDouble(raw);
+		if (Boolean.class == type) return raw == null ? true : parseBoolean(raw);
+		if (Character.class == type) return raw.length() == 0 ? (char)0 : raw.charAt(0);
+		if (Enum.class == type) return Enum.valueOf((Class<? extends Enum>)type, raw);
+		
+		throw new IllegalArgumentException("Huh?");
+	}
+	
+	private String setupFullName(Field field) {
+		FullName ann = field.getAnnotation(FullName.class);
+		if (ann == null) return field.getName();
+		else {
+			if (ann.value().trim().equals("")) throw new IllegalArgumentException("Missing name for field: " + field.getName());
+			else return ann.value();
+		}
+	}
+	
+	private String setupShorthand(Field field) {
+		Shorthand ann = field.getAnnotation(Shorthand.class);
+		if (ann == null) return "";
+		String[] value = ann.value();
+		StringBuilder sb = new StringBuilder();
+		for (String v : value) {
+			char[] c = v.toCharArray();
+			if (c.length != 1) throw new IllegalArgumentException(String.format(
+					"Shorthands must be strings of 1 character long. (%s at %s)", v, fullName));
+			if (c[0] == '-') throw new IllegalArgumentException(String.format(
+					"The dash (-) is not a legal shorthand character. (at %s)", fullName));
+			if (sb.indexOf(v) > -1) throw new IllegalArgumentException(String.format(
+					"Duplicate shorthand: %s (at %s)", v, fullName));
+			sb.append(v);
+		}
+		
+		return sb.toString();
+	}
+	
+	private String setupDescription(Field field) {
+		StringBuilder out = new StringBuilder();
+		Description ann = field.getAnnotation(Description.class);
+		
+		if (ann != null) out.append(ann.value());
+		if (isCollection) out.append(out.length() > 0 ? "  " : "").append("This option may be used multiple times.");
+		if (isParameterized && type != String.class) {
+			if (out.length() > 0) out.append("  ");
+			if (type == Float.class || type == Double.class) out.append("value is a floating point number.");
+			if (type == Integer.class || type == Long.class || type == Short.class || type == Byte.class)
+				out.append("value is an integer.");
+			if (type == Boolean.class) out.append("value is 'true' or 'false'.");
+			if (type == Character.class) out.append("Value is a single character.");
+			if (type == Enum.class) {
+				out.append("value is one of: ");
+				boolean first = true;
+				
+				Enum<?>[] enumConstants = (Enum<?>[])type.getEnumConstants();
+				for (Enum<?> e : enumConstants) {
+					if (first) first = false;
+					else out.append(", ");
+					out.append(e.name());
+				}
+				out.append(".");
+			}
+		}
+		
+		return out.toString();
+	}
+	
+	private boolean setupMandatory(Field field) {
+		Mandatory mandatory = field.getAnnotation(Mandatory.class);
+		return mandatory != null && (mandatory.onlyIf().length == 0 && mandatory.onlyIfNot().length == 0);
+	}
+	
+	private List<String> setupMandatoryIf(Field field) {
+		Mandatory mandatory = field.getAnnotation(Mandatory.class);
+		if (mandatory == null || mandatory.onlyIf().length == 0) return Collections.emptyList();
+		return Collections.unmodifiableList(Arrays.asList(mandatory.onlyIf()));
+	}
+	
+	private List<String> setupMandatoryIfNot(Field field) {
+		Mandatory mandatory = field.getAnnotation(Mandatory.class);
+		if (mandatory == null || mandatory.onlyIfNot().length == 0) return Collections.emptyList();
+		return Collections.unmodifiableList(Arrays.asList(mandatory.onlyIfNot()));
+	}
+	
+	private List<String> setupRequires(Field feild) {
+		Requires requires = field.getAnnotation(Requires.class);
+		if (requires == null || requires.value().length == 0) return Collections.emptyList();
+		return Collections.unmodifiableList(Arrays.asList(requires.value()));
+	}
+	
+	private List<String> setupExcludes(Field field) {
+		Excludes excludes = field.getAnnotation(Excludes.class);
+		if (excludes == null || excludes.value().length == 0) return Collections.emptyList();
+		return Collections.unmodifiableList(Arrays.asList(excludes.value()));
+	}
+	
+	private List<String> setupExcludesGroup(Field field) {
+		ExcludesGroup excludesGroup = field.getAnnotation(ExcludesGroup.class);
+		if (excludesGroup == null || excludesGroup.value().length == 0) return Collections.emptyList();
+		return Collections.unmodifiableList(Arrays.asList(excludesGroup.value()));
+	}
+	
+	private List<String> TRUE_VALS = Collections.unmodifiableList(Arrays.asList("1", "true", "t", "y", "yes", "on"));
+	private List<String> FALSE_VALS = Collections.unmodifiableList(Arrays.asList("0", "false", "f", "n", "no", "off"));
+	
+	private boolean parseBoolean(String raw) {
+		for (String x : TRUE_VALS) if (x.equalsIgnoreCase(raw)) return true;
+		for (String x : FALSE_VALS) if (x.equalsIgnoreCase(raw)) return false;
+		throw new IllegalArgumentException("Not a boolean: " + raw);
+	}
+	
+	String getShorthand() {
+		return shorthand;
+	}
+}
diff --git a/src/com/zwitserloot/cmdreader/Requires.java b/src/com/zwitserloot/cmdreader/Requires.java
new file mode 100644
index 0000000..6cf1175
--- /dev/null
+++ b/src/com/zwitserloot/cmdreader/Requires.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright © 2010-2011 Reinier Zwitserloot.
+ * 
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ * 
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ * 
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.zwitserloot.cmdreader;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Indicates that the presence of this option also means 1 or more other options must also be present.
+ * option names.
+ */
+ at Retention(value=RetentionPolicy.RUNTIME)
+ at Target(value=ElementType.FIELD)
+ at Documented
+public @interface Requires {
+	/** The other option or options that must be present. */
+	String[] value();
+}
diff --git a/src/com/zwitserloot/cmdreader/Sequential.java b/src/com/zwitserloot/cmdreader/Sequential.java
new file mode 100644
index 0000000..57376c8
--- /dev/null
+++ b/src/com/zwitserloot/cmdreader/Sequential.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright © 2010-2011 Reinier Zwitserloot.
+ * 
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ * 
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ * 
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.zwitserloot.cmdreader;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * All options that are marked with {@code @Sequential} do not need an option name. Only one may be an array or list, and these options may not be of type {@code boolean}.
+ * For example:
+ * 
+ * <pre>
+ *     @Mandatory @Sequential(1) List<String> from;
+ *     @Mandatory @Sequential(2) String to;
+ * </pre>
+ * 
+ * could be part of the command line arguments structure for a copy command.
+ */
+ at Retention(value=RetentionPolicy.RUNTIME)
+ at Target(value=ElementType.FIELD)
+ at Documented
+public @interface Sequential {
+	/** Because fields in classes do not have a defined order, if you have multiple arguments marked {@code @Sequential} you need to specify the order. */
+	int value() default 0;
+}
diff --git a/src/com/zwitserloot/cmdreader/Shorthand.java b/src/com/zwitserloot/cmdreader/Shorthand.java
new file mode 100644
index 0000000..b4fd671
--- /dev/null
+++ b/src/com/zwitserloot/cmdreader/Shorthand.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright © 2010-2011 Reinier Zwitserloot.
+ * 
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ * 
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ * 
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.zwitserloot.cmdreader;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Allows a shorthand form, i.e. {@code -h} to be used instead of the full name, i.e. {@code --help}.
+ * You can have multiple shorthands for one command. These are usually single characters. Single character shorthands can be chained:
+ * <pre>
+ *     java -jar yourapp.jar -abc
+ * </pre>
+ * 
+ * is the same as:
+ * <pre>
+ *     java -jar yourapp.jar -a -b -c
+ * </pre>
+ * 
+ * assuming {@code a}, {@code b}, and {@code c} are all shorthands.
+ */
+ at Retention(value=RetentionPolicy.RUNTIME)
+ at Target(value=ElementType.FIELD)
+ at Documented
+public @interface Shorthand {
+	String[] value() default {};
+}
diff --git a/src/com/zwitserloot/cmdreader/package-info.java b/src/com/zwitserloot/cmdreader/package-info.java
new file mode 100644
index 0000000..dfc1856
--- /dev/null
+++ b/src/com/zwitserloot/cmdreader/package-info.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright © 2010 Reinier Zwitserloot.
+ * 
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ * 
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ * 
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+/**
+ * For an example program, check the project homepage at <a href="http://github.com/rzwitserloot/com.zwitserloot.cmdreader">http://github.com/rzwitserloot/com.zwitserloot.cmdreader</a>
+ * and for a full list of the annotations available, open the javadoc for {@link com.zwitserloot.cmdreader.CmdReader}.
+ */
+package com.zwitserloot.cmdreader;
diff --git a/test/com/zwitserloot/cmdreader/TestCmdReader.java b/test/com/zwitserloot/cmdreader/TestCmdReader.java
new file mode 100644
index 0000000..c7612d7
--- /dev/null
+++ b/test/com/zwitserloot/cmdreader/TestCmdReader.java
@@ -0,0 +1,333 @@
+/*
+ * Copyright © 2010 Reinier Zwitserloot.
+ * 
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ * 
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ * 
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package com.zwitserloot.cmdreader;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import org.junit.Before;
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+
+public class TestCmdReader {
+	private static class CmdArgs1 {
+		@Shorthand("a")
+		@Excludes("val2")
+		@FullName("foo-bar")
+		@Description("This is a description")
+		private String val1;
+		
+		private String val2;
+		
+		@Shorthand("v")
+		private String val3;
+		
+		@Shorthand("b")
+		private boolean bool;
+		
+		@Requires("foo2")
+		private String foo1;
+		
+		private String foo2;
+		
+		@Mandatory
+		private String foo3;
+	}
+	
+	private static class CmdArgs2 {
+		@Shorthand("a")
+		private int integer;
+		
+		@Shorthand("b")
+		private double real;
+		
+		@Sequential(1)
+		private String val1;
+		
+		@Sequential(2)
+		private List<String> val2;
+	}
+	
+	@SuppressWarnings("unused")
+	private static class CmdArgs3 {
+		@Mandatory(onlyIf="val2")
+		private String val1;
+		
+		private boolean val2;
+		
+		@Mandatory(onlyIfNot="val4")
+		private String val3;
+		
+		private boolean val4;
+	}
+	
+	private static class CmdArgs4 {
+		@ExcludesGroup
+		private boolean bar1;
+		
+		@ExcludesGroup
+		private boolean bar2;
+		
+		@ExcludesGroup({"default", "foobar"})
+		private boolean bar3;
+		
+		@ExcludesGroup("foobar")
+		private boolean bar4;
+		
+		@ExcludesGroup("foobar")
+		private boolean bar5;
+	}
+	
+	@SuppressWarnings("all")
+	private static class CmdArgsSeqFail1 {
+		@Sequential(1)
+		String foo;
+		
+		@Mandatory
+		@Sequential(2)
+		String bar;
+	}
+	
+	@SuppressWarnings("all")
+	private static class CmdArgsSeqFail2 {
+		@Mandatory
+		@Sequential(1)
+		List<String> col;
+		
+		@Mandatory
+		@Sequential(2)
+		String bar;
+		
+		@Sequential(3)
+		String baz;
+	}
+	
+	private static class CmdArgsSeq1 {
+		@Mandatory
+		@Sequential(1)
+		String arg1;
+		
+		@Sequential(2)
+		List<String> list;
+		
+		@Mandatory
+		@Sequential(3)
+		String arg2;
+		
+		@Mandatory
+		@Sequential(4)
+		String arg3;
+	}
+	
+	private static class CmdArgsSeq2 {
+		@Mandatory
+		@Sequential(1)
+		String arg1;
+		
+		@Mandatory
+		@Sequential(2)
+		List<String> list;
+	}
+	
+	private CmdReader<CmdArgs1> reader1;
+	private CmdReader<CmdArgs2> reader2;
+	private CmdReader<CmdArgs3> reader3;
+	private CmdReader<CmdArgs4> reader4;
+	private CmdReader<CmdArgsSeq1> readerSeq1;
+	private CmdReader<CmdArgsSeq2> readerSeq2;
+	
+	@Before
+	public void init() {
+		reader1 = CmdReader.of(CmdArgs1.class);
+		reader2 = CmdReader.of(CmdArgs2.class);
+		reader3 = CmdReader.of(CmdArgs3.class);
+		reader4 = CmdReader.of(CmdArgs4.class);
+		readerSeq1 = CmdReader.of(CmdArgsSeq1.class);
+		readerSeq2 = CmdReader.of(CmdArgsSeq2.class);
+	}
+	
+	@Test(expected=InvalidCommandLineException.class)
+	public void testMandatory1() throws InvalidCommandLineException {
+		reader1.make(new String[0]);
+	}
+	
+	@Test
+	public void testMandatory2() throws InvalidCommandLineException {
+		reader3.make("--val3 foo");
+		reader3.make("--val1 a --val2 --val3 foo");
+		reader3.make("--val4");
+	}
+	
+	@Test(expected=InvalidCommandLineException.class)
+	public void testMandatory3() throws InvalidCommandLineException {
+		reader3.make("");
+	}
+	
+	@Test(expected=InvalidCommandLineException.class)
+	public void testMandatory4() throws InvalidCommandLineException {
+		reader3.make("--val2");
+	}
+	
+	@Test(expected=InvalidCommandLineException.class)
+	public void testRequires1() throws InvalidCommandLineException {
+		reader1.make("--foo1 test --foo3=bar");
+	}
+	
+	@Test
+	public void testRequires2() throws InvalidCommandLineException {
+		CmdArgs1 args = reader1.make("--foo1=test --foo2 blabla --foo3=bar");
+		assertEquals("foo1 not set", "test", args.foo1);
+		assertEquals("foo2 not set", "blabla", args.foo2);
+		assertEquals("foo3 not set", "bar", args.foo3);
+		assertFalse(args.bool);
+		assertNull(args.val1);
+		assertNull(args.val2);
+		assertNull(args.val3);
+	}
+	
+	@Test
+	public void testExcludes1() throws InvalidCommandLineException {
+		CmdArgs1 args = reader1.make("--foo-bar test1 -vb test2 --foo3=bar");
+		assertEquals("foo3 not set", "bar", args.foo3);
+		assertNull(args.foo1);
+		assertNull(args.foo2);
+		assertEquals("val3 not set", "test2", args.val3);
+		assertEquals("val1 not set", "test1", args.val1);
+		assertTrue(args.bool);
+		assertNull(args.val2);
+	}
+	
+	@Test(expected=InvalidCommandLineException.class)
+	public void testExcludes2() throws InvalidCommandLineException {
+		reader1.make("--foo-bar test1 -b --val2 bla --foo3=bar");
+	}
+	
+	@Test(expected=InvalidCommandLineException.class)
+	public void testExcludesGroup1() throws InvalidCommandLineException {
+		reader4.make("--bar1 --bar3");
+	}
+	
+	@Test
+	public void testExcludesGroup2() throws InvalidCommandLineException {
+		reader4.make("--bar1 --bar4");
+	}
+	
+	@Test(expected=InvalidCommandLineException.class)
+	public void testExcludesGroup3() throws InvalidCommandLineException {
+		reader4.make("--bar3 --bar4 --bar5");
+	}
+	
+	@Test(expected=InvalidCommandLineException.class)
+	public void testExcludesGroup4() throws InvalidCommandLineException {
+		reader4.make("--bar3 --bar5");
+	}
+	
+	@Test(expected=InvalidCommandLineException.class)
+	public void testBadCommandLine1() throws InvalidCommandLineException {
+		reader1.make("--foo-bar");
+	}
+	
+	@Test(expected=InvalidCommandLineException.class)
+	public void testBadCommandLine2() throws InvalidCommandLineException {
+		reader1.make("-abv test1");
+	}
+	
+	@Test(expected=InvalidCommandLineException.class)
+	public void testBadCommandLine3() throws InvalidCommandLineException {
+		reader1.make("-abv test1 test2 test3");
+	}
+	
+	@Test
+	public void testSequential1() throws InvalidCommandLineException {
+		CmdArgs2 args = reader2.make("foo bar baz");
+		assertEquals("foo", args.val1);
+		assertEquals(Arrays.asList("bar", "baz"), args.val2);
+	}
+	
+	@Test
+	public void testSequential2() throws InvalidCommandLineException {
+		CmdArgs2 args = reader2.make("foo");
+		assertEquals("foo", args.val1);
+		assertNull(args.val2);
+	}
+	
+	@Test
+	public void testNumeric1() throws InvalidCommandLineException {
+		CmdArgs2 args = reader2.make("-ab 12 13.5");
+		assertEquals(12, args.integer);
+		assertEquals(13.5, args.real, 0.000001);
+	}
+	
+	@Test(expected=NumberFormatException.class)
+	public void testNumeric2() throws InvalidCommandLineException {
+		reader2.make("-a 12.5");
+	}
+	
+	public void testSequential3() throws InvalidCommandLineException {
+		CmdArgs2 args = reader2.make("--integer 10");
+		assertEquals(10, args.integer);
+		assertNull(args.val1);
+		assertNull(args.val2);
+	}
+	
+	public void testSequential4() throws InvalidCommandLineException {
+		CmdArgs2 args = reader2.make("--val2 test1 --val2=test2");
+		assertEquals(0, args.integer);
+		assertNull(args.val1);
+		assertEquals(Arrays.asList("test1", "test2"), args.val2);
+	}
+	
+	@Test(expected=IllegalArgumentException.class)
+	public void testSeqFail1() throws IllegalArgumentException {
+		CmdReader.of(CmdArgsSeqFail1.class);
+	}
+	
+	@Test(expected=IllegalArgumentException.class)
+	public void testSeqFail2() throws IllegalArgumentException {
+		CmdReader.of(CmdArgsSeqFail2.class);
+	}
+	
+	public void testSequential5() throws InvalidCommandLineException {
+		CmdArgsSeq1 args = readerSeq1.make("foo1 foo2 foo3 foo4 foo5 foo6 foo7");
+		assertEquals("foo1", args.arg1);
+		assertEquals(Arrays.asList("foo2", "foo3", "foo4", "foo5"), args.list);
+		assertEquals("foo6", args.arg2);
+		assertEquals("foo7", args.arg3);
+		
+		args = readerSeq1.make("foo1 foo2 foo3");
+		assertEquals("foo1", args.arg1);
+		assertEquals(Collections.emptyList(), args.list);
+		assertEquals("foo2", args.arg2);
+		assertEquals("foo3", args.arg3);
+		
+		CmdArgsSeq2 args2 = readerSeq2.make("foo1 foo2 foo3 foo4 foo5 foo6 foo7");
+		assertEquals("foo1", args2.arg1);
+		assertEquals(Arrays.asList("foo2", "foo3", "foo4", "foo5", "foo6", "foo7"), args2.list);
+	}
+	
+	@Test(expected=InvalidCommandLineException.class)
+	public void testSequential6() throws InvalidCommandLineException {
+		readerSeq2.make("foo1");
+	}
+}

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-java/cmdreader.git



More information about the pkg-java-commits mailing list