[med-svn] [libbroad-barclay-java] 01/02: Import Upstream version 1.2.1

Steffen Möller moeller at moszumanska.debian.org
Fri Sep 1 07:55:22 UTC 2017


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

moeller pushed a commit to branch master
in repository libbroad-barclay-java.

commit 4fffafe3b06e9660cf6c8c4922f9391a1bd9cf97
Author: Steffen Moeller <moeller at debian.org>
Date:   Thu Aug 31 18:30:09 2017 +0200

    Import Upstream version 1.2.1
---
 .gitignore                                         |    6 +
 .travis.yml                                        |   23 +
 AUTHORS                                            |   14 +
 LICENSE.txt                                        |   26 +
 README.md                                          |   11 +
 build.gradle                                       |  178 +++
 gradle/wrapper/gradle-wrapper.jar                  |  Bin 0 -> 52928 bytes
 gradle/wrapper/gradle-wrapper.properties           |    6 +
 gradlew                                            |  169 +++
 .../broadinstitute/barclay/argparser/Advanced.java |   13 +
 .../broadinstitute/barclay/argparser/Argument.java |  122 ++
 .../barclay/argparser/ArgumentCollection.java      |   16 +
 .../barclay/argparser/BetaFeature.java             |   13 +
 .../barclay/argparser/ClassFinder.java             |  188 +++
 .../argparser/CommandLineArgumentParser.java       | 1349 ++++++++++++++++++++
 .../barclay/argparser/CommandLineException.java    |  106 ++
 .../barclay/argparser/CommandLineParser.java       |  202 +++
 .../argparser/CommandLineParserOptions.java        |   21 +
 .../argparser/CommandLinePluginDescriptor.java     |  178 +++
 .../argparser/CommandLinePluginProvider.java       |   11 +
 .../barclay/argparser/CommandLineProgramGroup.java |   17 +
 .../argparser/CommandLineProgramProperties.java    |   31 +
 .../broadinstitute/barclay/argparser/Hidden.java   |   13 +
 .../argparser/LegacyCommandLineArgumentParser.java | 1029 +++++++++++++++
 .../barclay/argparser/PositionalArguments.java     |   36 +
 .../argparser/SpecialArgumentsCollection.java      |   30 +
 .../barclay/argparser/StrictBooleanConverter.java  |   27 +
 .../barclay/argparser/TaggedArgument.java          |   39 +
 .../barclay/argparser/TaggedArgumentParser.java    |  329 +++++
 .../barclay/help/BashTabCompletionDoclet.java      |  548 ++++++++
 .../barclay/help/DefaultDocWorkUnitHandler.java    |  812 ++++++++++++
 .../broadinstitute/barclay/help/DocException.java  |   17 +
 .../broadinstitute/barclay/help/DocWorkUnit.java   |  196 +++
 .../barclay/help/DocWorkUnitHandler.java           |   95 ++
 .../broadinstitute/barclay/help/DocletUtils.java   |   64 +
 .../barclay/help/DocumentedFeature.java            |   47 +
 .../broadinstitute/barclay/help/GSONArgument.java  |   54 +
 .../broadinstitute/barclay/help/GSONWorkUnit.java  |   27 +
 .../broadinstitute/barclay/help/HelpDoclet.java    |  635 +++++++++
 .../broadinstitute/barclay/help/package-info.java  |    4 +
 .../org/broadinstitute/barclay/utils/JVMUtils.java |   39 +
 .../org/broadinstitute/barclay/utils/Utils.java    |  139 ++
 .../barclay/helpTemplates/bash-completion.ftl      |  451 +++++++
 .../helpTemplates/bash-completion.macros.ftl       |  119 ++
 .../barclay/helpTemplates/common.html.ftl          |   47 +
 .../barclay/helpTemplates/generic.html.ftl         |  186 +++
 .../barclay/helpTemplates/generic.index.html.ftl   |   65 +
 .../argparser/CollectionArgumentUnitTests.java     |  298 +++++
 .../argparser/CommandLineArgumentParserTest.java   | 1193 +++++++++++++++++
 .../argparser/CommandLinePluginUnitTest.java       |  457 +++++++
 .../LegacyCommandLineArgumentParserTest.java       |  933 ++++++++++++++
 .../argparser/StrictBooleanConverterTest.java      |   25 +
 .../barclay/argparser/TaggedArgumentTest.java      |  441 +++++++
 .../barclay/argparser/TestProgramGroup.java        |   16 +
 .../DocumentationGenerationIntegrationTest.java    |  275 ++++
 .../barclay/help/TestArgumentCollection.java       |   36 +
 .../barclay/help/TestArgumentContainer.java        |  201 +++
 .../barclay/help/TestDocWorkUnitHandler.java       |   21 +
 .../broadinstitute/barclay/help/TestDoclet.java    |   50 +
 .../broadinstitute/barclay/help/TestExtraDocs.java |   17 +
 .../barclay/utils/UtilsUnitTest.java               |   33 +
 ...bashTabCompletionDocletTestLaunch-completion.sh |  479 +++++++
 ...etionDocletTestLaunchWithDefaults-completion.sh |  479 +++++++
 .../barclay/help/expected/HelpDoclet/index.html    |   76 ++
 ...stitute_barclay_help_TestArgumentContainer.html |  571 +++++++++
 ...stitute_barclay_help_TestArgumentContainer.json |  341 +++++
 ..._broadinstitute_barclay_help_TestExtraDocs.html |   77 ++
 ..._broadinstitute_barclay_help_TestExtraDocs.json |    6 +
 .../barclay/help/expected/TestDoclet/index.html    |   80 ++
 ...stitute_barclay_help_TestArgumentContainer.html |  583 +++++++++
 ...stitute_barclay_help_TestArgumentContainer.json |  341 +++++
 ..._broadinstitute_barclay_help_TestExtraDocs.html |   81 ++
 ..._broadinstitute_barclay_help_TestExtraDocs.json |    6 +
 .../help/templates/TestDoclet/common.html.ftl      |   53 +
 .../help/templates/TestDoclet/generic.html.ftl     |  205 +++
 .../templates/TestDoclet/generic.index.html.ftl    |   71 ++
 76 files changed, 15193 insertions(+)

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ca48ad7
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,6 @@
+.DS_Store
+.gradle
+*.iml
+build
+.idea
+gradlew.bat
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..84bc461
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,23 @@
+language: java
+dist: trusty
+sudo: true
+before_cache:
+- rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
+cache:
+  directories:
+  - $HOME/.gradle/caches/
+  - $HOME/.gradle/wrapper/
+  - $HOME/.m2
+jdk:
+- oraclejdk8
+- openjdk8
+script: ./gradlew jacocoTestReport ;
+env:
+  matrix:
+  - ARTIFACTORY_USERNAME=gatkci
+  global:
+    secure: rF9xJaWR1LRF6y0Ujq+zvfg2wO0DRgs1vqoUgS9BoHuXKRDNWyiEYbX0Mxef5LxXgqHSoIxirIBnjGxpDYaM+kRwNm1IqcW/C5Z5slKY12lbwFgTFdROfKS4lGMVo6U5/w+hyknrEVxEV4ULw7I2Z4sUWHU0X+uOhf7JP2sYTXcl0kyUPP4crSAMGQ+J/Epc4mvmxuNaSCbAq74+JW+GJ8KqbEmrDPRBpDFAoeISjmnmXGlvPECgIuPjFp3pJ3nOv3hDJqIb6jWs8Jt2w4xeByg4ENPI2z+oAzWM7QqqyybK706LMy/jppNqVa3AOUCCiQIjTnJYgapAthIatfSrJrtwKxmrxyq1v2XfPuXqeMi3PB8Yz/ikOWk2dWU/XQnqV9u1ZEuFwcs+0InsDVVYkLQ3A7RJ570CdDYsqejzdGDk25r+BIxob8TViCneG1UzWydKd3XFmtaxLORMWqu0vyoQ6OM+w7Yc [...]
+after_success:
+- echo "TRAVIS_BRANCH='$TRAVIS_BRANCH'"; echo "JAVA_HOME='$JAVA_HOME'";
+- bash <(curl -s https://codecov.io/bash); if [ "$TRAVIS_BRANCH" == "master" ]; then
+  ./gradlew uploadArchives; fi
diff --git a/AUTHORS b/AUTHORS
new file mode 100644
index 0000000..02f0599
--- /dev/null
+++ b/AUTHORS
@@ -0,0 +1,14 @@
+# This is the official list of Barclay authors for copyright purposes.
+#
+# The AUTHORS file lists the copyright holders.
+# For example, Broad Institute employees are not listed here, 
+# because Broad Institute holds the copyright.
+
+# Names should be added to this file as
+# Organization <webpage>
+# Individual Name <email address or website>
+
+# Please keep the list sorted alphabetically, companies/organizations first, individuals second.
+Broad Institute, Inc. <www.broadinstitute.org>
+Daniel Gómez-Sánchez <daniel.gomez.sanchez at hotmail.es>
+
diff --git a/LICENSE.txt b/LICENSE.txt
new file mode 100644
index 0000000..47f4c9d
--- /dev/null
+++ b/LICENSE.txt
@@ -0,0 +1,26 @@
+Copyright (c) 2009-2016, GATK Authors. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+  list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice,
+  this list of conditions and the following disclaimer in the documentation
+  and/or other materials provided with the distribution.
+
+* Neither the name Broad Institute, Inc. nor the names of its
+  contributors may be used to endorse or promote products derived from
+  this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..d7705f4
--- /dev/null
+++ b/README.md
@@ -0,0 +1,11 @@
+[![License (3-Clause BSD)](https://img.shields.io/badge/license-BSD%203--Clause-blue.svg)](https://opensource.org/licenses/BSD-3-Clause)
+
+
+# Barclay
+Barclay is a set of classes for annotating, parsing, validating, and generating documentation for command line options.
+
+##Requirements
+* Java 8
+* Gradle 3.1 or greater. We recommend using the `./gradlew` script which will
+    download and use an appropriate gradle version automatically.
+ 
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000..cad6973
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,178 @@
+import javax.tools.ToolProvider
+
+plugins {
+    id "java"
+    id 'maven'
+    id 'signing'
+    id 'jacoco'
+    id 'com.palantir.git-version' version '0.5.1' //version helper
+}
+
+sourceCompatibility = 1.8
+targetCompatibility = 1.8
+
+group = 'org.broadinstitute'
+
+final isRelease = Boolean.getBoolean("release")
+version = (isRelease ? gitVersion() : gitVersion() + "-SNAPSHOT").replaceAll(".dirty", "")
+
+repositories {
+    mavenCentral()
+    maven {
+        url "https://broadinstitute.jfrog.io/broadinstitute/libs-snapshot/" //for Broad snapshots
+    }
+
+    mavenLocal()
+}
+
+jacocoTestReport {
+    dependsOn test
+    group = "Reporting"
+    description = "Generate Jacoco coverage reports after running tests."
+    additionalSourceDirs = files(sourceSets.main.allJava.srcDirs)
+
+    reports {
+        xml.enabled = true // codecov plugin depends on xml format report
+        html.enabled = true
+    }
+}
+
+compileJava {
+    options.compilerArgs = ['-proc:none', '-Xlint:all','-Werror','-Xdiags:verbose']
+}
+compileTestJava {
+    options.compilerArgs = ['-proc:none', '-Xlint:all','-Werror','-Xdiags:verbose']
+}
+dependencies {
+    compile 'net.sf.jopt-simple:jopt-simple:5.0.3'
+    compile 'org.apache.commons:commons-lang3:3.4'
+    compile 'org.apache.logging.log4j:log4j-api:2.3'
+    compile 'org.apache.logging.log4j:log4j-core:2.3'
+
+    // Get the jdk files we need to run javaDoc. We need to use these during compile, testCompile,
+    // test execution, and gatkDoc generation, but we don't want them as part of the runtime
+    // classpath and we don't want to redistribute them in the uber jar.
+    final javadocJDKFiles = files(((URLClassLoader) ToolProvider.getSystemToolClassLoader()).getURLs())
+    compileOnly(javadocJDKFiles)
+    testCompile(javadocJDKFiles)
+    compile 'org.freemarker:freemarker:2.3.23'
+    compile 'com.google.code.gson:gson:2.2.2'
+
+    testCompile 'org.testng:testng:6.9.6'
+}
+
+test {
+    useTestNG()
+    outputs.upToDateWhen { false }  //tests will never be "up to date" so you can always rerun them
+
+    // show standard out and standard error of the test JVM(s) on the console
+    testLogging.showStandardStreams = true
+    beforeTest { descriptor ->
+        logger.lifecycle("Running Test: " + descriptor)
+    }
+
+    // listen to standard out and standard error of the test JVM(s)
+    onOutput { descriptor, event ->
+        logger.lifecycle("Test: " + descriptor + " produced standard out/err: " + event.message )
+    }
+
+    testLogging {
+        testLogging {
+            events "skipped", "failed"
+            exceptionFormat = "full"
+        }
+        afterSuite { desc, result ->
+            if (!desc.parent) { // will match the outermost suite
+                println "Results: ${result.resultType} (${result.testCount} tests, ${result.successfulTestCount} successes, ${result.failedTestCount} failures, ${result.skippedTestCount} skipped)"
+            }
+        }
+    }
+
+}
+
+task javadocJar(type: Jar, dependsOn: javadoc) {
+    classifier = 'javadoc'
+    from 'build/docs/javadoc'
+}
+
+task sourcesJar(type: Jar) {
+    from sourceSets.main.allSource
+    classifier = 'sources'
+}
+
+// This is a hack to disable the java 8 default javadoc lint until we fix the html formatting
+tasks.withType(Javadoc) {
+    options.addStringOption('Xdoclint:none', '-quiet')
+}
+
+/**
+ *This specifies what artifacts will be built and uploaded when performing a maven upload.
+ */
+artifacts {
+    archives jar
+    archives javadocJar
+    archives sourcesJar
+}
+
+/**
+ * Sign non-snapshot releases with our secret key.  This should never need to be invoked directly.
+ */
+signing {
+    required { isRelease && gradle.taskGraph.hasTask("uploadArchives") }
+    sign configurations.archives
+}
+
+/**
+ * Upload a release to sonatype.  You must be an authorized uploader and have your sonatype
+ * username and password information in your gradle properties file.  See the readme for more info.
+ *
+ * For releasing to your local maven repo, use gradle install
+ */
+uploadArchives {
+    doFirst {
+        println "Attempting to upload version:$version"
+    }
+    repositories {
+        mavenDeployer {
+            beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) }
+
+            repository(url: "https://oss.sonatype.org/service/local/staging/deploy/maven2/") {
+                authentication(userName: project.findProperty("sonatypeUsername"), password: project.findProperty("sonatypePassword"))
+            }
+
+            snapshotRepository(url: "https://broadinstitute.jfrog.io/broadinstitute/libs-snapshot-local/") {
+                authentication(userName: System.env.ARTIFACTORY_USERNAME, password: System.env.ARTIFACTORY_PASSWORD)
+            }
+
+            pom.project {
+                name 'Barclay'
+                packaging 'jar'
+                description 'Development on Barclay command line parsing and documentation utilities'
+                url 'http://github.com/broadinstitute/barclay'
+
+                scm {
+                    url 'scm:git at github.com:broadinstitute/barclay.git'
+                    connection 'scm:git at github.com:broadinstitute/barclay.git'
+                    developerConnection 'scm:git at github.com:broadinstitute/barclay.git'
+                }
+
+                developers {
+                    developer {
+                        id = "gatkdev"
+                        name = "GATK Development Team"
+                        email = "gatk-dev-public at broadinstitute.org"
+                    }
+                }
+
+                licenses {
+                    license {
+                        name 'BSD 3-Clause'
+                        url 'https://github.com/broadinstitute/barclay/blob/master/LICENSE.TXT'
+                        distribution 'repo'
+                    }
+                }
+            }
+        }
+    }
+}
+
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..6ffa237
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..ebaa8e3
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Mon Nov 07 10:14:42 EST 2016
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-3.1-bin.zip
diff --git a/gradlew b/gradlew
new file mode 100755
index 0000000..9aa616c
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,169 @@
+#!/usr/bin/env bash
+
+##############################################################################
+##
+##  Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+    ls=`ls -ld "$PRG"`
+    link=`expr "$ls" : '.*-> \(.*\)$'`
+    if expr "$link" : '/.*' > /dev/null; then
+        PRG="$link"
+    else
+        PRG=`dirname "$PRG"`"/$link"
+    fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn ( ) {
+    echo "$*"
+}
+
+die ( ) {
+    echo
+    echo "$*"
+    echo
+    exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+  CYGWIN* )
+    cygwin=true
+    ;;
+  Darwin* )
+    darwin=true
+    ;;
+  MINGW* )
+    msys=true
+    ;;
+  NONSTOP* )
+    nonstop=true
+    ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+        # IBM's JDK on AIX uses strange locations for the executables
+        JAVACMD="$JAVA_HOME/jre/sh/java"
+    else
+        JAVACMD="$JAVA_HOME/bin/java"
+    fi
+    if [ ! -x "$JAVACMD" ] ; then
+        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+    fi
+else
+    JAVACMD="java"
+    which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+    MAX_FD_LIMIT=`ulimit -H -n`
+    if [ $? -eq 0 ] ; then
+        if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+            MAX_FD="$MAX_FD_LIMIT"
+        fi
+        ulimit -n $MAX_FD
+        if [ $? -ne 0 ] ; then
+            warn "Could not set maximum file descriptor limit: $MAX_FD"
+        fi
+    else
+        warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+    fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+    GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+    APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+    CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+    JAVACMD=`cygpath --unix "$JAVACMD"`
+
+    # We build the pattern for arguments to be converted via cygpath
+    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+    SEP=""
+    for dir in $ROOTDIRSRAW ; do
+        ROOTDIRS="$ROOTDIRS$SEP$dir"
+        SEP="|"
+    done
+    OURCYGPATTERN="(^($ROOTDIRS))"
+    # Add a user-defined pattern to the cygpath arguments
+    if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+        OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+    fi
+    # Now convert the arguments - kludge to limit ourselves to /bin/sh
+    i=0
+    for arg in "$@" ; do
+        CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+        CHECK2=`echo "$arg"|egrep -c "^-"`                                 ### Determine if an option
+
+        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition
+            eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+        else
+            eval `echo args$i`="\"$arg\""
+        fi
+        i=$((i+1))
+    done
+    case $i in
+        (0) set -- ;;
+        (1) set -- "$args0" ;;
+        (2) set -- "$args0" "$args1" ;;
+        (3) set -- "$args0" "$args1" "$args2" ;;
+        (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+        (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+        (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+        (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+        (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+        (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+    esac
+fi
+
+# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
+function splitJvmOpts() {
+    JVM_OPTS=("$@")
+}
+eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
+JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
+
+# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
+if [[ "$(uname)" == "Darwin" ]] && [[ "$HOME" == "$PWD" ]]; then
+  cd "$(dirname "$0")"
+fi
+
+exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
diff --git a/src/main/java/org/broadinstitute/barclay/argparser/Advanced.java b/src/main/java/org/broadinstitute/barclay/argparser/Advanced.java
new file mode 100644
index 0000000..0ac9393
--- /dev/null
+++ b/src/main/java/org/broadinstitute/barclay/argparser/Advanced.java
@@ -0,0 +1,13 @@
+package org.broadinstitute.barclay.argparser;
+
+import java.lang.annotation.*;
+
+/**
+ * Indicates that an argument is considered an advanced option.
+ */
+ at Documented
+ at Inherited
+ at Retention(RetentionPolicy.RUNTIME)
+ at Target({ElementType.FIELD})
+public @interface Advanced {
+}
diff --git a/src/main/java/org/broadinstitute/barclay/argparser/Argument.java b/src/main/java/org/broadinstitute/barclay/argparser/Argument.java
new file mode 100644
index 0000000..5ef053d
--- /dev/null
+++ b/src/main/java/org/broadinstitute/barclay/argparser/Argument.java
@@ -0,0 +1,122 @@
+package org.broadinstitute.barclay.argparser;
+
+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;
+
+/**
+ * Used to annotate which fields of a CommandLineProgram are options given at the command line.
+ * If a command line call looks like "cmd -option foo -x y bar baz" the CommandLineProgram
+ * would have annotations on fields to handle the values of option and x. The java type of the option
+ * will be inferred from the type of the field or from the generic type of the collection
+ * if this option is allowed more than once. The type must be an enum or
+ * have a constructor with a single String parameter.
+ */
+ at Retention(RetentionPolicy.RUNTIME)
+ at Target(ElementType.FIELD)
+ at Documented
+public @interface Argument {
+
+    /**
+     * The full name of the command-line argument.  Full names should be
+     * prefixed on the command-line with a double dash (--).
+     * @return Selected full name, or "" to use the default.
+     */
+    String fullName() default "";
+
+    /**
+     * Specified short name of the command.  Short names should be prefixed
+     * with a single dash.  Argument values can directly abut single-char
+     * short names or be separated from them by a space.
+     * @return Selected short name, or "" for none.
+     */
+    String shortName() default "";
+
+    /**
+     * Documentation for the command-line argument.  Should appear when the
+     * --help argument is specified.
+     * @return Doc string associated with this command-line argument.
+     */
+    String doc() default "Undocumented option";
+
+    /**
+     * If set to false, a {@link org.broadinstitute.barclay.argparser.CommandLineException.MissingArgument} will be thrown
+     * if the option is not specified.
+     * If 2 options are mutually exclusive and both are required it will be interpreted as one or the other is required
+     * and an exception will only be thrown if neither are specified.
+     * An argument with a non-null default value specified will ignore this flag and always be treated as optional
+     */
+    boolean optional() default false;
+
+    /**
+     * Array of option names that cannot be used in conjunction with this one.
+     * If 2 options are mutually exclusive and both have optional=false it will be
+     * interpreted as one OR the other is required and an exception will only be thrown if
+     * neither are specified.
+     */
+    String[] mutex() default {};
+
+    /**
+     * Is this an Option common to all command line programs.  If it is then it will only
+     * be displayed in usage info when H or STDHELP is used to display usage.
+     */
+    boolean common() default false;
+
+    /**
+     * Does this option have special treatment in the argument parsing system.
+     * Some examples are arguments_file and help, which have special behavior in the parser.
+     * This is intended for documenting these options.
+     */
+    boolean special() default false;
+
+    /**
+     * Are the contents of this argument private and should be kept out of logs.
+     * Examples of sensitive arguments are encryption and api keys.
+     */
+    boolean sensitive() default false;
+
+    /** The minimum number of times that this option is required. */
+    int minElements() default 0;
+
+    /** The maximum number of times this option is allowed. */
+    int maxElements() default Integer.MAX_VALUE;
+    
+    /**
+     * Hard lower bound on the allowed value for the annotated argument -- generates an exception if violated.
+     * Enforced only for numeric types whose values are explicitly specified on the command line.
+     *
+     * @return Hard lower bound on the allowed value for the annotated argument, or Double.NEGATIVE_INFINITY
+     *         if there is none.
+     */
+    double minValue() default Double.NEGATIVE_INFINITY;
+
+    /**
+     * Hard upper bound on the allowed value for the annotated argument -- generates an exception if violated.
+     * Enforced only for numeric types whose values are explicitly specified on the command line.
+     *
+     * @return Hard upper bound on the allowed value for the annotated argument, or Double.POSITIVE_INFINITY
+     *         if there is none.
+     */
+    double maxValue() default Double.POSITIVE_INFINITY;
+
+    /**
+     * Soft lower bound on the allowed value for the annotated argument -- generates a warning if violated.
+     * Enforced only for numeric types whose values are explicitly specified on the command line.
+     *
+     * @return Soft lower bound on the allowed value for the annotated argument, or Double.NEGATIVE_INFINITY
+     *         if there is none.
+     */
+    double minRecommendedValue() default Double.NEGATIVE_INFINITY;
+
+    /**
+     * Soft upper bound on the allowed value for the annotated argument -- generates a warning if violated.
+     * Enforced only for numeric types whose values are explicitly specified on the command line.
+     *
+     * @return Soft upper bound on the allowed value for the annotated argument, or Double.POSITIVE_INFINITY
+     *         if there is none.
+     */
+    double maxRecommendedValue() default Double.POSITIVE_INFINITY;
+
+}
diff --git a/src/main/java/org/broadinstitute/barclay/argparser/ArgumentCollection.java b/src/main/java/org/broadinstitute/barclay/argparser/ArgumentCollection.java
new file mode 100644
index 0000000..bf1576b
--- /dev/null
+++ b/src/main/java/org/broadinstitute/barclay/argparser/ArgumentCollection.java
@@ -0,0 +1,16 @@
+package org.broadinstitute.barclay.argparser;
+
+import java.lang.annotation.*;
+
+/**
+ * Used to annotate a field in a CommandLineProgram that holds an instance containing @Argument-annotated
+ * fields.
+ */
+ at Retention(RetentionPolicy.RUNTIME)
+ at Target(ElementType.FIELD)
+ at Documented
+ at Inherited
+public @interface ArgumentCollection {
+    /** Text that appears for this group of options in text describing usage of the command line program. */
+    String doc() default "";
+}
diff --git a/src/main/java/org/broadinstitute/barclay/argparser/BetaFeature.java b/src/main/java/org/broadinstitute/barclay/argparser/BetaFeature.java
new file mode 100644
index 0000000..7999bf3
--- /dev/null
+++ b/src/main/java/org/broadinstitute/barclay/argparser/BetaFeature.java
@@ -0,0 +1,13 @@
+package org.broadinstitute.barclay.argparser;
+
+import java.lang.annotation.*;
+
+/**
+ * Marker interface for features that are under development and not ready for production use.
+ */
+ at Documented
+ at Inherited
+ at Retention(RetentionPolicy.RUNTIME)
+ at Target({ElementType.TYPE})
+public @interface BetaFeature {
+}
diff --git a/src/main/java/org/broadinstitute/barclay/argparser/ClassFinder.java b/src/main/java/org/broadinstitute/barclay/argparser/ClassFinder.java
new file mode 100644
index 0000000..ef38626
--- /dev/null
+++ b/src/main/java/org/broadinstitute/barclay/argparser/ClassFinder.java
@@ -0,0 +1,188 @@
+package org.broadinstitute.barclay.argparser;
+
+import org.apache.logging.log4j.Logger;
+import org.apache.logging.log4j.LogManager;
+
+import java.io.File;
+import java.io.IOException;
+import java.lang.reflect.Modifier;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.net.URLDecoder;
+import java.util.*;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+
+/**
+ * Utility class that can scan for classes in the classpath and find all the ones
+ * annotated with a particular annotation.
+ *
+ * @author Tim Fennell
+ */
+public final class ClassFinder {
+    private final Set<Class<?>> classes = new LinkedHashSet<>();
+    private final ClassLoader loader;
+    private Class<?> parentType;
+    // If not null, only look for classes in this jar
+    private String jarPath = null;
+
+    private static final Logger log = LogManager.getLogger();
+
+    public ClassFinder() {
+        loader = Thread.currentThread().getContextClassLoader();
+    }
+
+    public ClassFinder(final ClassLoader loader) {
+        this.loader = loader;
+    }
+
+    public ClassFinder(final File jarFile) throws IOException {
+        // The class loader must have the context in order to load dependent classes when loading a class,
+        // but the jarPath is remembered so that the iteration over the classpath skips anything other than
+        // the jarPath.
+        jarPath = jarFile.getCanonicalPath();
+        final URL[] urls = {new URL("file", "", jarPath)};
+        loader = new URLClassLoader(urls, Thread.currentThread().getContextClassLoader());
+    }
+
+    /** Convert a filename to a class name by removing '.class' and converting '/'s to '.'s. */
+    public String toClassName(final String filename) {
+        return filename.substring(0, filename.lastIndexOf(".class"))
+                .replace('/', '.').replace('\\', '.');
+    }
+
+    /**
+     * Scans the classpath for classes within the specified package and sub-packages that
+     * extend the parentType. This method can be called repeatedly
+     * with different packages. Classes are accumulated internally and
+     * can be accessed by calling {@link #getClasses()}.
+     */
+    public void find(String packageName, final Class<?> parentType) {
+        this.parentType = parentType;
+        packageName = packageName.replace('.', '/');
+        final Enumeration<URL> urls;
+
+        try {
+            urls = loader.getResources(packageName);
+        }
+        catch (IOException ioe) {
+            log.warn("Could not read package: " + packageName, ioe);
+            return;
+        }
+
+        while (urls.hasMoreElements()) {
+            try {
+                String urlPath = urls.nextElement().getFile();
+                urlPath = URLDecoder.decode(urlPath, "UTF-8");
+                if ( urlPath.startsWith("file:") ) {
+                    urlPath = urlPath.substring(5);
+                }
+                if (urlPath.indexOf('!') > 0) {
+                    urlPath = urlPath.substring(0, urlPath.indexOf('!'));
+                }
+                if (jarPath != null && !jarPath.equals(urlPath)) {
+                    continue;
+                }
+
+                //Log.info("Looking for classes in location: " + urlPath);
+                final File file = new File(urlPath);
+                if ( file.isDirectory() ) {
+                    scanDir(file, packageName);
+                }
+                else {
+                    scanJar(file, packageName);
+                }
+            }
+            catch (IOException ioe) {
+                log.warn("could not read entries", ioe);
+            }
+        }
+    }
+
+    /**
+     * Scans the entries in a ZIP/JAR file for classes under the parent package.
+     * @param file the jar file to be scanned
+     * @param packagePath the top level package to start from
+     */
+    protected void scanJar(final File file, final String packagePath) throws IOException {
+        final ZipFile zip = new ZipFile(file);
+        final Enumeration<? extends ZipEntry> entries = zip.entries();
+        while ( entries.hasMoreElements() ) {
+            final ZipEntry entry = entries.nextElement();
+            final String name = entry.getName();
+            if (name.startsWith(packagePath)) {
+                handleItem(name);
+            }
+        }
+    }
+
+    /**
+     * Scans a directory on the filesystem for classes.
+     * @param file the directory or file to examine
+     * @param path the package path acculmulated so far (e.g. edu/mit/broad)
+     */
+    protected void scanDir(final File file, final String path) {
+        for ( final File child: file.listFiles() ) {
+            final String newPath = (path==null ? child.getName() : path + '/' + child.getName() );
+            if ( child.isDirectory() ) {
+                scanDir(child, newPath);
+            }
+            else {
+                handleItem(newPath);
+            }
+        }
+    }
+
+    /**
+     * Checks an item to see if it is a class and is annotated with the specified
+     * annotation.  If so, adds it to the set, otherwise ignores it.
+     * @param name the path equivelant to the package + class/item name
+     */
+    protected void handleItem(final String name) {
+        if (name.endsWith(".class")) {
+            final String classname = toClassName(name);
+
+            try {
+                final Class<?> type = loader.loadClass(classname);
+                if (parentType.isAssignableFrom(type)) {
+                    this.classes.add(type);
+                }
+            }
+            catch (Throwable t) {
+                log.debug("could not load class: " + classname, t);
+            }
+        }
+    }
+
+    /** Fetches the set of classes discovered so far. */
+    public Set<Class<?>> getClasses() {
+        return this.classes;
+    }
+
+    /**
+     * Fetches the set of classes discovered so far, subsetted down to concrete (non-abstract/interface) classes only
+     *
+     * @return subset of classes discovered so far including only concrete (non-abstract/interface) classes
+     */
+    public Set<Class<?>> getConcreteClasses() {
+        Set<Class<?>> concreteClassSet = new LinkedHashSet<>();
+
+        for ( Class<?> clazz : classes ) {
+            if ( isConcrete(clazz) ) {
+                concreteClassSet.add(clazz);
+            }
+        }
+
+        return concreteClassSet;
+    }
+
+    /**
+     * Determines whether or not the specified class is concrete (ie., non-abstract and non-interface)
+     *
+     * @param clazz class to check
+     * @return true if the class is neither abstract nor an interface, otherwise false
+     */
+    public static boolean isConcrete( final Class<?> clazz ) {
+        return ! Modifier.isAbstract(clazz.getModifiers()) && ! Modifier.isInterface(clazz.getModifiers());
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/org/broadinstitute/barclay/argparser/CommandLineArgumentParser.java b/src/main/java/org/broadinstitute/barclay/argparser/CommandLineArgumentParser.java
new file mode 100644
index 0000000..9d4ae32
--- /dev/null
+++ b/src/main/java/org/broadinstitute/barclay/argparser/CommandLineArgumentParser.java
@@ -0,0 +1,1349 @@
+package org.broadinstitute.barclay.argparser;
+
+import joptsimple.OptionException;
+import joptsimple.OptionParser;
+import joptsimple.OptionSet;
+import joptsimple.OptionSpec;
+import joptsimple.OptionSpecBuilder;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.text.WordUtils;
+
+import org.apache.commons.lang3.tuple.Pair;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.broadinstitute.barclay.utils.Utils;
+
+import java.io.BufferedReader;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.PrintStream;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.util.*;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * Annotation-driven utility for parsing command-line arguments, checking for errors, and producing usage message.
+ * <p/>
+ * This class supports arguments of the form -KEY VALUE, plus positional arguments.
+ * <p/>
+ * The caller must supply an object that both defines the command line and has the parsed arguments set into it.
+ * For each possible "-KEY VALUE" argument, there must be a public data member annotated with @Argument.  The KEY name is
+ * the name of the fullName attribute of @Argument.  An abbreviated name may also be specified with the shortName attribute
+ * of @Argument.
+ * If the data member is a List<T>, then the argument may be specified multiple times.  The type of the data member,
+ * or the type of the List element must either have a ctor T(String), or must be an Enum.  List arguments must
+ * be initialized by the caller with some kind of list.  Any other argument that is non-null is assumed to have the given
+ * value as a default.  If an argument has no default value, and does not have the optional attribute of @Argument set,
+ * is required.  For List arguments, minimum and maximum number of elements may be specified in the @Argument annotation.
+ * <p/>
+ * A single List data member may be annotated with the @PositionalArguments.  This behaves similarly to a Argument
+ * with List data member: the caller must initialize the data member, the type must be constructable from String, and
+ * min and max number of elements may be specified.  If no @PositionalArguments annotation appears in the object,
+ * then it is an error for the command line to contain positional arguments.
+ * <p/>
+ */
+public final class CommandLineArgumentParser implements CommandLineParser {
+    // For formatting argument section of usage message.
+    private static final int ARGUMENT_COLUMN_WIDTH = 30;
+    private static final int DESCRIPTION_COLUMN_WIDTH = 90;
+
+    private static final String ENUM_OPTION_DOC_PREFIX = "Possible values: {";
+    private static final String ENUM_OPTION_DOC_SUFFIX = "} ";
+
+    private static final String defaultUsagePreamble = "Usage: program [arguments...]\n";
+    private static final String defaultUsagePreambleWithPositionalArguments =
+            "Usage: program [arguments...] [positional-arguments...]\n";
+    protected static final String BETA_PREFIX = "\n\n**BETA FEATURE - FOR EVALUATION ONLY**\n\n";
+
+    private static final String NULL_STRING = "null";
+    public static final String COMMENT = "#";
+    public static final String POSITIONAL_ARGUMENTS_NAME = "Positional Argument";
+
+    // Extension for collection argument list files
+    private static final String COLLECTION_LIST_FILE_EXTENSION = ".list";
+
+    private static final Logger logger = LogManager.getLogger();
+
+    // Map from (full class) name of each CommandLinePluginDescriptor requested and
+    // found to the actual descriptor instance
+    private Map<String, CommandLinePluginDescriptor<?>> pluginDescriptors = new HashMap<>();
+
+    // Keeps a map of tagged arguments for just-in-time retrieval at field population time
+    private TaggedArgumentParser tagParser = new TaggedArgumentParser();
+
+    // Return the plugin instance corresponding to the targetDescriptor class
+    @Override
+    public <T> T getPluginDescriptor(Class<T> targetDescriptor) {
+        return targetDescriptor.cast(pluginDescriptors.get(targetDescriptor.getName()));
+    }
+
+    private final Set<String> argumentsFilesLoadedAlready = new LinkedHashSet<>();
+
+    /**
+     * A typical command line program will call this to get the beginning of the usage message,
+     * and then append a description of the program, like this:
+     * commandLineParser.getStandardUsagePreamble(getClass()) + "Frobnicates the freebozzle."
+     */
+    @Override
+    public String getStandardUsagePreamble(final Class<?> mainClass) {
+        if (mainClass.getAnnotation(BetaFeature.class) != null) {
+            return BETA_PREFIX + "USAGE: " + mainClass.getSimpleName() + " [arguments]\n\n";
+        } else {
+            return "USAGE: " + mainClass.getSimpleName() + " [arguments]\n\n";
+        }
+    }
+
+    private void putInArgumentMap(ArgumentDefinition arg){
+        for (String key: arg.getNames()){
+            argumentMap.put(key, arg);
+        }
+    }
+
+    private boolean inArgumentMap(ArgumentDefinition arg){
+        for (String key: arg.getNames()){
+            if(argumentMap.containsKey(key)){
+                return true;
+            }
+        }
+        return false;
+    }
+
+    // This is the object that the caller has provided that contains annotations,
+    // and into which the values will be assigned.
+    private final Object callerArguments;
+
+    private final Set<CommandLineParserOptions> parserOptions;
+
+    // null if no @PositionalArguments annotation
+    private Field positionalArguments;
+    private int minPositionalArguments;
+    private int maxPositionalArguments;
+    private Object positionalArgumentsParent;
+
+    // List of all the data members with @Argument annotation
+    private List<ArgumentDefinition> argumentDefinitions = new ArrayList<>();
+
+    // Maps long name, and short name, if present, to an argument definition that is
+    // also in the argumentDefinitions list.
+    private final Map<String, ArgumentDefinition> argumentMap = new LinkedHashMap<>();
+
+    // The associated program properties using the CommandLineProgramProperties annotation
+    private final CommandLineProgramProperties programProperties;
+
+    private String getUsagePreamble() {
+        String usagePreamble = "";
+        if (null != programProperties) {
+            usagePreamble += programProperties.summary();
+        } else if (positionalArguments == null) {
+            usagePreamble += defaultUsagePreamble;
+        } else {
+            usagePreamble += defaultUsagePreambleWithPositionalArguments;
+        }
+        return usagePreamble;
+    }
+
+    /**
+     * @param callerArguments The object containing the command line arguments to be populated by
+     *                        this command line parser.
+     */
+    public CommandLineArgumentParser(final Object callerArguments) {
+        this(
+                callerArguments,
+                Collections.<CommandLinePluginDescriptor<?>>emptyList(),
+                Collections.<CommandLineParserOptions>emptySet()
+        );
+    }
+
+    /**
+     * @param callerArguments The object containing the command line arguments to be populated by
+     *                        this command line parser.
+     * @param pluginDescriptors A list of {@link CommandLinePluginDescriptor} objects that
+     *                          should be used by this command line parser to extend the list of
+     *                          command line arguments with dynamically discovered plugins. If
+     *                          null, no descriptors are loaded.
+     */
+    public CommandLineArgumentParser(
+            final Object callerArguments,
+            final List<? extends CommandLinePluginDescriptor<?>> pluginDescriptors,
+            final Set<CommandLineParserOptions> parserOptions) {
+        Utils.nonNull(callerArguments, "The object with command line arguments cannot be null");
+        Utils.nonNull(pluginDescriptors, "The list of pluginDescriptors cannot be null");
+        Utils.nonNull(parserOptions, "The set of parser options cannot be null");
+
+        this.callerArguments = callerArguments;
+        this.parserOptions = parserOptions;
+
+        createArgumentDefinitions(callerArguments, null);
+        createCommandLinePluginArgumentDefinitions(pluginDescriptors);
+
+        this.programProperties = this.callerArguments.getClass().getAnnotation(CommandLineProgramProperties.class);
+    }
+
+    private void createArgumentDefinitions(
+            final Object callerArguments,
+            final CommandLinePluginDescriptor<?> controllingDescriptor) {
+        for (final Field field : CommandLineParser.getAllFields(callerArguments.getClass())) {
+            if (field.getAnnotation(Argument.class) != null && field.getAnnotation(ArgumentCollection.class) != null){
+                throw new CommandLineException.CommandLineParserInternalException("An Argument cannot be an argument collection: "
+                        +field.getName() + " in " + callerArguments.toString() + " is annotated as both.");
+            }
+            if (field.getAnnotation(PositionalArguments.class) != null) {
+                handlePositionalArgumentAnnotation(field, callerArguments);
+            }
+            if (field.getAnnotation(Argument.class) != null) {
+                handleArgumentAnnotation(field, callerArguments, controllingDescriptor);
+            }
+            if (field.getAnnotation(ArgumentCollection.class) != null) {
+                try {
+                    field.setAccessible(true);
+                    createArgumentDefinitions(field.get(callerArguments), controllingDescriptor);
+                } catch (final IllegalAccessException e) {
+                    throw new CommandLineException.ShouldNeverReachHereException("should never reach here because we setAccessible(true)", e);
+                }
+            }
+        }
+    }
+
+    // Find all the instances of plugins specified by the provided plugin descriptors
+    private void createCommandLinePluginArgumentDefinitions(
+            final List<? extends CommandLinePluginDescriptor<?>> requestedPluginDescriptors) {
+        // For each descriptor, create the argument definitions for the descriptor object itself,
+        // then process it's plugin classes
+        requestedPluginDescriptors.forEach(
+                descriptor -> {
+                    pluginDescriptors.put(descriptor.getClass().getName(), descriptor);
+                    createArgumentDefinitions(descriptor, null);
+                    findPluginsForDescriptor(descriptor);
+                }
+        );
+    }
+
+    // Find all of the classes that derive from the class specified by the descriptor, obtain an
+    // instance each and add its ArgumentDefinitions
+    private void findPluginsForDescriptor(
+            final CommandLinePluginDescriptor<?> pluginDescriptor) {
+        final ClassFinder classFinder = new ClassFinder();
+        pluginDescriptor.getPackageNames().forEach(
+                pkg -> classFinder.find(pkg, pluginDescriptor.getPluginClass()));
+        final Set<Class<?>> pluginClasses = classFinder.getClasses();
+
+        final List<Object> plugins = new ArrayList<>(pluginClasses.size());
+        for (Class<?> c : pluginClasses) {
+            if (pluginDescriptor.getClassFilter().test(c)) {
+                try {
+                    final Object plugin = pluginDescriptor.getInstance(c);
+                    plugins.add(plugin);
+                    createArgumentDefinitions(plugin, pluginDescriptor);
+                } catch (InstantiationException | IllegalAccessException e) {
+                    throw new CommandLineException.CommandLineParserInternalException("Problem making an instance of plugin " + c +
+                            " Do check that the class has a non-arg constructor", e);
+                }
+            }
+        }
+    }
+
+    /**
+     * @return the list of ArgumentDefinitions seen by the parser
+     */
+    public List<ArgumentDefinition> getArgumentDefinitions() { return argumentDefinitions; }
+
+    /**
+     * @return the Field representing positional any argument definition found by the parser
+     */
+    public Field getPositionalArguments() { return positionalArguments; }
+
+    @Override
+    public String getVersion() {
+        return "Version:" + this.callerArguments.getClass().getPackage().getImplementationVersion();
+    }
+
+    // helper
+    private final void printArgumentUsageBlock(final StringBuilder sb, final String preamble, final List<ArgumentDefinition> args) {
+        if (args != null && !args.isEmpty()) {
+            sb.append(preamble);
+            args.stream().sorted(ArgumentDefinition.sortByLongName)
+                    .forEach(argumentDefinition -> printArgumentUsage(sb, argumentDefinition));
+        }
+    }
+
+    /**
+     * Print a usage message based on the arguments object passed to the ctor.
+     *
+     * @param printCommon True if common args should be included in the usage message.
+     * @param printHidden True if hidden args should be included in the usage message.
+     * @return Usage string generated by the command line parser.
+     */
+    @Override
+    public String usage(final boolean printCommon, final boolean printHidden) {
+        final StringBuilder sb = new StringBuilder();
+
+        sb.append(getStandardUsagePreamble(callerArguments.getClass()) + getUsagePreamble());
+        sb.append("\n" + getVersion() + "\n");
+
+        // filter on common and partition on plugin-controlled
+        final Map<Boolean, List<ArgumentDefinition>> allArgsMap = argumentDefinitions.stream()
+                .filter(argumentDefinition -> printCommon || !argumentDefinition.isCommon)
+                .filter(argumentDefinition -> printHidden || !argumentDefinition.isHidden)
+                .collect(Collectors.partitioningBy(a -> a.controllingDescriptor == null));
+
+        final List<ArgumentDefinition> nonPluginArgs = allArgsMap.get(true);
+        if (null != nonPluginArgs && !nonPluginArgs.isEmpty()) {
+            // partition the non-plugin args on optional
+            final Map<Boolean, List<ArgumentDefinition>> unconditionalArgsMap = nonPluginArgs.stream()
+                    .collect(Collectors.partitioningBy(a -> a.optional));
+
+            // required args
+            printArgumentUsageBlock(sb, "\n\nRequired Arguments:\n\n", unconditionalArgsMap.get(false));
+
+            // optional args split by advanced
+            final List<ArgumentDefinition> optArgs = unconditionalArgsMap.get(true);
+            if (null != optArgs && !optArgs.isEmpty()) {
+                final Map<Boolean, List<ArgumentDefinition>> byAdvanced = optArgs.stream()
+                        .collect(Collectors.partitioningBy(a -> a.isAdvanced));
+                printArgumentUsageBlock(sb, "\nOptional Arguments:\n\n", byAdvanced.get(false));
+                printArgumentUsageBlock(sb, "\nAdvanced Arguments:\n\n", byAdvanced.get(true));
+            }
+
+        }
+
+        // now the conditional/dependent args (those controlled by a plugin descriptor)
+        List<ArgumentDefinition> conditionalArgs = allArgsMap.get(false);
+        if (null != conditionalArgs && !conditionalArgs.isEmpty()) {
+            // group all of the conditional argdefs by the name of their controlling pluginDescriptor class
+            final Map<CommandLinePluginDescriptor<?>, List<ArgumentDefinition>> argsByControllingDescriptor =
+                    conditionalArgs
+                            .stream()
+                            .collect(Collectors.groupingBy(argDef -> argDef.controllingDescriptor));
+
+            // sort the list of controlling pluginDescriptors by display name and iterate through them
+            final List<CommandLinePluginDescriptor<?>> pluginDescriptorSortedByName =
+                    new ArrayList<>(argsByControllingDescriptor.keySet());
+            pluginDescriptorSortedByName.sort(
+                    (a, b) -> String.CASE_INSENSITIVE_ORDER.compare(a.getDisplayName(), b.getDisplayName())
+            );
+            for (final CommandLinePluginDescriptor<?> descriptor: pluginDescriptorSortedByName) {
+                sb.append("Conditional Arguments for " + descriptor.getDisplayName() + ":\n\n");
+                // get all the argument definitions controlled by this pluginDescriptor's plugins, group
+                // those by plugin, and get the sorted list of names of the owning plugins
+                final Map<String, List<ArgumentDefinition>> byPlugin =
+                        argsByControllingDescriptor.get(descriptor)
+                                .stream()
+                                .collect(Collectors.groupingBy(argDef -> argDef.parent.getClass().getSimpleName()));
+                final List<String> sortedPluginNames = new ArrayList<>(byPlugin.keySet());
+                sortedPluginNames.sort(String.CASE_INSENSITIVE_ORDER);
+
+                // iterate over the owning plugins in sorted order, get each one's argdefs in sorted order,
+                // and print their usage
+                for (final String pluginName: sortedPluginNames) {
+                    printArgumentUsageBlock(sb, "Valid only if \"" + pluginName + "\" is specified:\n", byPlugin.get(pluginName));
+                }
+            }
+        }
+
+        return sb.toString();
+    }
+
+    /**
+     * Parse command-line arguments, and store values in callerArguments object passed to ctor.
+     * @param messageStream Where to write error messages.
+     * @param args          Command line tokens.
+     * @return true if command line is valid and the program should run, false if help or version was requested
+     * @throws CommandLineException if there is an invalid command line
+     */
+    @SuppressWarnings("unchecked")
+    @Override
+    public boolean parseArguments(final PrintStream messageStream, String[] args) {
+
+        // Preprocess the arguments before the parser sees them, replacing any tagged options
+        // and their values with raw option names and surrogate key values, so that tagged
+        // options can be recognized by the parser. The actual values will be retrieved using
+        // the key when the fields's values are set.
+        args = tagParser.preprocessTaggedOptions(args);
+
+        OptionParser parser = new OptionParser(false);
+
+        for (ArgumentDefinition arg : argumentDefinitions){
+            OptionSpecBuilder bld = parser.acceptsAll(arg.getNames(), arg.doc);
+            if (arg.isFlag()) {
+                bld.withOptionalArg().withValuesConvertedBy(new StrictBooleanConverter());
+            } else {
+                bld.withRequiredArg();
+            }
+        }
+        if(positionalArguments != null){
+            parser.nonOptions();
+        }
+
+        OptionSet parsedArguments;
+        try {
+            parsedArguments = parser.parse(args);
+        } catch (final OptionException e) {
+            throw new CommandLineException(e.getMessage());
+        }
+        //Check for the special arguments file flag
+        //if it's seen, read arguments from that file and recursively call parseArguments()
+        if (parsedArguments.has(SpecialArgumentsCollection.ARGUMENTS_FILE_FULLNAME)) {
+            List<String> argfiles = parsedArguments.valuesOf(SpecialArgumentsCollection.ARGUMENTS_FILE_FULLNAME).stream()
+                    .map(f -> (String)f)
+                    .collect(Collectors.toList());
+
+            List<String> newargs = argfiles.stream()
+                    .distinct()
+                    .filter(file -> !argumentsFilesLoadedAlready.contains(file))
+                    .flatMap(file -> loadArgumentsFile(file).stream())
+                    .collect(Collectors.toList());
+            argumentsFilesLoadedAlready.addAll(argfiles);
+
+            if (!newargs.isEmpty()) {
+                newargs.addAll(Arrays.asList(args));
+                return parseArguments(messageStream, newargs.toArray(new String[newargs.size()]));
+            }
+        }
+
+        //check if special short circuiting arguments are set
+        if (isSpecialFlagSet(parsedArguments, SpecialArgumentsCollection.HELP_FULLNAME)) {
+            messageStream.print(usage(true, isSpecialFlagSet(parsedArguments, SpecialArgumentsCollection.SHOW_HIDDEN_FULLNAME)));
+            return false;
+        } else if (isSpecialFlagSet(parsedArguments, SpecialArgumentsCollection.VERSION_FULLNAME)) {
+            messageStream.println(getVersion());
+            return false;
+        }
+
+        for (OptionSpec<?> optSpec : parsedArguments.asMap().keySet()) {
+            if (parsedArguments.has(optSpec)) {
+                ArgumentDefinition argDef = argumentMap.get(optSpec.options().get(0));
+                setArgument(argDef, (List<String>) optSpec.values(parsedArguments));
+            }
+        }
+
+        for (Object arg : parsedArguments.nonOptionArguments()) {
+            setPositionalArgument((String) arg);
+        }
+
+        assertArgumentsAreValid();
+
+        return true;
+    }
+
+    /**
+     *  helper to deal with the case of special flags that are evaluated before the options are properly set
+     */
+    private boolean isSpecialFlagSet(OptionSet parsedArguments, String flagName){
+        if (parsedArguments.has(flagName)){
+            Object value = parsedArguments.valueOf(flagName);
+            return  (value == null || !value.equals("false"));
+        } else{
+            return false;
+        }
+
+    }
+
+    /**
+     * After command line has been parsed, make sure that all required arguments have values, and that
+     * lists with minimum # of elements have sufficient values.
+     *
+     * @throws CommandLineException if arguments requirements are not satisfied.
+     */
+    private void assertArgumentsAreValid()  {
+        validatePluginArguments(); // trim the list of plugin-derived argument definitions before validation
+        try {
+            for (final ArgumentDefinition argumentDefinition : argumentDefinitions) {
+                final String fullName = argumentDefinition.getLongName();
+                final StringBuilder mutextArgumentNames = new StringBuilder();
+                for (final String mutexArgument : argumentDefinition.mutuallyExclusive) {
+                    final ArgumentDefinition mutextArgumentDef = argumentMap.get(mutexArgument);
+                    if (mutextArgumentDef != null && mutextArgumentDef.hasBeenSet) {
+                        mutextArgumentNames.append(" ").append(mutextArgumentDef.getLongName());
+                    }
+                }
+                if (argumentDefinition.hasBeenSet && mutextArgumentNames.length() > 0) {
+                    throw new CommandLineException("Argument '" + fullName +
+                            "' cannot be used in conjunction with argument(s)" +
+                            mutextArgumentNames.toString());
+                }
+                if (argumentDefinition.isCollection && !argumentDefinition.optional) {
+                    @SuppressWarnings("rawtypes")
+                    final Collection c = (Collection) argumentDefinition.getFieldValue();
+                    if (c.isEmpty() && mutextArgumentNames.length() == 0) {
+                       throw new CommandLineException.MissingArgument(fullName, getOneOfMutexRequiredErrorMessage(argumentDefinition));
+                    }
+                } else if (!argumentDefinition.optional && !argumentDefinition.hasBeenSet && mutextArgumentNames.length() == 0) {
+                    throw new CommandLineException.MissingArgument(fullName, getOneOfMutexRequiredErrorMessage(argumentDefinition));
+                }
+            }
+            if (positionalArguments != null) {
+                @SuppressWarnings("rawtypes")
+                final Collection c = (Collection) positionalArguments.get(positionalArgumentsParent);
+                if (c.size() < minPositionalArguments) {
+                    throw new CommandLineException.MissingArgument(POSITIONAL_ARGUMENTS_NAME,"At least " + minPositionalArguments +
+                            " positional arguments must be specified.");
+                }
+            }
+        } catch (final IllegalAccessException e) {
+            throw new CommandLineException.ShouldNeverReachHereException("Should never happen",e);
+        }
+
+    }
+
+    // Error message for when mutex args are mutually required (meaning one of them must be specified) but none was
+    private String getOneOfMutexRequiredErrorMessage(ArgumentDefinition argumentDefinition) {
+        return "Argument '" + argumentDefinition.getLongName() + "' is required" +
+                (argumentDefinition.mutuallyExclusive.isEmpty() ?
+                        "." :
+                        " unless any of " + argumentDefinition.mutuallyExclusive) + " are specified.";
+    }
+
+    // Once all command line args have been processed, go through the argument definitions and
+    // validate any that are plugin class arguments against the controlling descriptor, trimming
+    // the list of argument definitions along the way by removing any that have not been set
+    // (so validation doesn't complain about missing required arguments for plugins that weren't
+    // specified) and throwing for any that have been set but are not allowed. Note that we don't trim
+    // the list of plugins themselves (just the argument definitions), since the plugin may contain
+    // other arguments that require validation.
+    private void validatePluginArguments() {
+        final List<ArgumentDefinition> actualArgumentDefinitions = new ArrayList<>();
+        for (final ArgumentDefinition argumentDefinition : argumentDefinitions) {
+            if (!argumentDefinition.isControlledByPlugin()) {
+                actualArgumentDefinitions.add(argumentDefinition);
+            } else {
+                final boolean isAllowed = argumentDefinition.controllingDescriptor.isDependentArgumentAllowed(
+                        argumentDefinition.parent.getClass());
+                if (argumentDefinition.hasBeenSet) {
+                    if (!isAllowed) {
+                        // dangling dependent argument; a value was specified but it's containing
+                        // (predecessor) plugin argument wasn't specified
+                        throw new CommandLineException(
+                                String.format(
+                                        "Argument \"%s/%s\" is only valid when the argument \"%s\" is specified",
+                                        argumentDefinition.shortName,
+                                        argumentDefinition.getLongName(),
+                                        argumentDefinition.parent.getClass().getSimpleName()));
+                    }
+                    actualArgumentDefinitions.add(argumentDefinition);
+                } else if (isAllowed) {
+                    // the predecessor argument was seen, so this value is allowed but hasn't been set; keep the
+                    // argument definition to allow validation to check for missing required args
+                    actualArgumentDefinitions.add(argumentDefinition);
+                }
+            }
+        }
+
+        // update the list of argument definitions with the new list
+        argumentDefinitions = actualArgumentDefinitions;
+
+        // finally, give each plugin a chance to trim down any unseen instances from it's own list
+        pluginDescriptors.entrySet().forEach(e -> e.getValue().validateArguments());
+    }
+    /**
+     * Check the provided value against any range constraints specified in the Argument annotation
+     * for the corresponding field. Throw an exception if limits are violated.
+     *
+     * - Only checks numeric types (int, double, etc.)
+     */
+    private void checkArgumentRange(final ArgumentDefinition argumentDefinition, final Object argumentValue) {
+        // Only validate numeric types because we have already ensured at constructor time that only numeric types have bounds
+        if (!Number.class.isAssignableFrom(argumentDefinition.type)) {
+            return;
+        }
+
+        final Double argumentDoubleValue = (argumentValue == null) ? null : ((Number)argumentValue).doubleValue();
+
+        // Check hard limits first, if specified
+        if (argumentDefinition.hasBoundedRange() && isOutOfRange(argumentDefinition.minValue, argumentDefinition.maxValue, argumentDoubleValue)) {
+            throw new CommandLineException.OutOfRangeArgumentValue(argumentDefinition.getLongName(), argumentDefinition.minValue, argumentDefinition.maxValue, argumentValue);
+        }
+        // Check recommended values
+        if (argumentDefinition.hasRecommendedRange() && isOutOfRange(argumentDefinition.minRecommendedValue, argumentDefinition.maxRecommendedValue, argumentDoubleValue)) {
+            final boolean outMinValue = argumentDefinition.minRecommendedValue != Double.NEGATIVE_INFINITY;
+            final boolean outMaxValue = argumentDefinition.maxRecommendedValue != Double.POSITIVE_INFINITY;
+            if (outMinValue && outMaxValue) {
+                logger.warn("Argument --{} has value {}, but recommended within range ({},{})",
+                        argumentDefinition.getLongName(), argumentDoubleValue, argumentDefinition.minRecommendedValue, argumentDefinition.maxRecommendedValue);
+            } else if (outMinValue) {
+                logger.warn("Argument --{} has value {}, but minimum recommended is {}",
+                        argumentDefinition.getLongName(), argumentDoubleValue, argumentDefinition.minRecommendedValue);
+            } else if (outMaxValue) {
+                logger.warn("Argument --{} has value {}, but maximum recommended is {}",
+                        argumentDefinition.getLongName(), argumentDoubleValue, argumentDefinition.maxRecommendedValue);
+            }
+            // if there is no recommended value, do not log anything
+        }
+    }
+
+    // null values are always out of range
+    private static boolean isOutOfRange(final double minValue, final double maxValue, final Double value) {
+        return value == null || minValue != Double.NEGATIVE_INFINITY && value < minValue
+                || maxValue != Double.POSITIVE_INFINITY && value > maxValue;
+    }
+
+    // check if the value is infinity or a mathematical integer
+    private static boolean isInfinityOrMathematicalInteger(final double value) {
+        return Double.isInfinite(value) || value == Math.rint(value);
+    }
+
+
+    @SuppressWarnings("unchecked")
+    private void setPositionalArgument(final String stringValue) {
+        if (positionalArguments == null) {
+            throw new CommandLineException("Invalid argument '" + stringValue + "'.");
+        }
+        final Object value = constructFromString(CommandLineParser.getUnderlyingType(positionalArguments), stringValue, POSITIONAL_ARGUMENTS_NAME);
+        @SuppressWarnings("rawtypes")
+        final Collection c;
+        try {
+            c = (Collection) positionalArguments.get(callerArguments);
+        } catch (final IllegalAccessException e) {
+            throw new CommandLineException.ShouldNeverReachHereException(e);
+        }
+        if (c.size() >= maxPositionalArguments) {  //we're checking if there is space to add another argument
+            throw new CommandLineException("No more than " + maxPositionalArguments +
+                    " positional arguments may be specified on the command line.");
+        }
+        c.add(value);
+    }
+
+    @SuppressWarnings("unchecked")
+    private void setArgument(ArgumentDefinition argumentDefinition, List<String> values) {
+        //special treatment for flags
+        if (argumentDefinition.isFlag() && values.isEmpty()){
+            argumentDefinition.hasBeenSet = true;
+            argumentDefinition.setFieldValue(true);
+            return;
+        }
+
+        if (!argumentDefinition.isCollection && (argumentDefinition.hasBeenSet || values.size() > 1)) {
+            throw new CommandLineException("Argument '" + argumentDefinition.getNames() + "' cannot be specified more than once.");
+        }
+        if (argumentDefinition.isCollection) {
+            if (!parserOptions.contains(CommandLineParserOptions.APPEND_TO_COLLECTIONS)) {
+                // if this is a collection then we only want to clear it once at the beginning, before we process
+                // any of the values, unless we're in APPEND_TO_COLLECTIONS mode, in which case we leave the initial
+                // and append to it
+                @SuppressWarnings("rawtypes")
+                final Collection c = (Collection) argumentDefinition.getFieldValue();
+                c.clear();
+            }
+            values = expandListFile(values);
+        }
+
+        for (int i = 0; i < values.size(); i++) {
+            String stringValue = values.get(i);
+            final Object value;
+            if (stringValue.equals(NULL_STRING)) {
+                if (argumentDefinition.isCollection && i != 0) {
+                    // If a "null" is included, and its not the first value for this option, honor it, but warn,
+                    // since it will clobber any values that were previously set for this option, and may indicate
+                    // an unintentional error on the user's part
+                    logger.warn("A \"null\" value was detected for an option after values for that option were already set. " +
+                            "Clobbering previously set values for this option: " + argumentDefinition.getNames() + ".");
+                }
+                //"null" is a special value that allows the user to override any default
+                //value set for this arg
+                if (argumentDefinition.optional) {
+                    value = null;
+                } else {
+                    throw new CommandLineException("Non \"null\" value must be provided for '" + argumentDefinition.getNames() + "'.");
+                }
+            } else {
+                // See if the value is a surrogate key in the tag parser's map that was placed there during preprocessing,
+                // and if so, unpack the values retrieved via the key and use those to populate the field
+                Pair<String, String> taggedOptionPair = tagParser.getTaggedOptionForSurrogate(stringValue);
+                if (TaggedArgument.class.isAssignableFrom(argumentDefinition.type)) {
+                    value = constructFromString(
+                            CommandLineParser.getUnderlyingType(argumentDefinition.field),
+                            taggedOptionPair == null ?
+                                    stringValue :
+                                    taggedOptionPair.getRight(),        // argument value
+                            argumentDefinition.getLongName());
+                    // NOTE: this propagates the tag name/attributes to the field BEFORE the value is set
+                    TaggedArgument taggedArgument = (TaggedArgument) value;
+                    tagParser.populateArgumentTags(
+                            taggedArgument,
+                            argumentDefinition.getLongName(),
+                            taggedOptionPair == null ?
+                                    null :
+                                    taggedOptionPair.getLeft());
+                }
+                else {
+                    if (taggedOptionPair == null) {
+                        value = constructFromString(
+                                CommandLineParser.getUnderlyingType(argumentDefinition.field),
+                                stringValue,
+                                argumentDefinition.getLongName());
+                    } else {
+                        // a tag was found for a non-taggable argument
+                        throw new CommandLineException(
+                                String.format("The argument: \"%s/%s\" does not accept tags: \"%s\"",
+                                        argumentDefinition.shortName,
+                                        argumentDefinition.fullName,
+                                        taggedOptionPair.getLeft()));
+                    }
+                }
+            }
+
+            // check the argument range
+            checkArgumentRange(argumentDefinition, value);
+
+            if (argumentDefinition.isCollection) {
+                @SuppressWarnings("rawtypes")
+                final Collection c = (Collection) argumentDefinition.getFieldValue();
+                if (value == null) {
+                    //user specified this arg=null which is interpreted as empty list
+                    c.clear();
+                } else {
+                    c.add(value);
+                }
+                argumentDefinition.hasBeenSet = true;
+            } else {
+                argumentDefinition.setFieldValue(value);
+                argumentDefinition.hasBeenSet = true;
+            }
+        }
+    }
+
+    /**
+     * Expand any collection values that are ".list" argument files, and add them
+     * to the list of values for that argument.
+     * @param originalValues
+     * @return a list containing the original entries in {@code originalValues}, with any
+     * values from list files expanded in place, preserving both the original list order and
+     * the file order
+     */
+    private List<String> expandListFile(final List<String> originalValues) {
+        List<String> expandedValues = new ArrayList<>(originalValues.size());
+        for (String stringValue: originalValues) {
+            if (stringValue.endsWith(COLLECTION_LIST_FILE_EXTENSION)) {
+                expandedValues.addAll(loadCollectionListFile(stringValue));
+            }
+            else {
+                expandedValues.add(stringValue);
+            }
+        }
+        return expandedValues;
+    }
+
+    /**
+     * Read a list file and return a list of the collection values contained in it
+     * A line that starts with {@link #COMMENT}  is ignored.
+     *
+     * @param collectionListFile a text file containing list values
+     * @return false if a fatal error occurred
+     */
+    private List<String> loadCollectionListFile(final String collectionListFile) {
+        try (BufferedReader reader = new BufferedReader(new FileReader(collectionListFile))){
+            return reader.lines()
+                    .map(String::trim)
+                    .filter(line -> !line.isEmpty())
+                    .filter(line -> !line.startsWith(COMMENT))
+                    .collect(Collectors.toList());
+        } catch (final IOException e) {
+            throw new CommandLineException("I/O error loading list file:" + collectionListFile, e);
+        }
+    }
+
+    /**
+     * Read an argument file and return a list of the args contained in it
+     * A line that starts with {@link #COMMENT}  is ignored.
+     *
+     * @param argumentsFile a text file containing args
+     * @return false if a fatal error occurred
+     */
+    private List<String> loadArgumentsFile(final String argumentsFile) {
+        List<String> args = new ArrayList<>();
+        try (BufferedReader reader = new BufferedReader(new FileReader(argumentsFile))){
+            String line;
+            while ((line = reader.readLine()) != null) {
+                if (!line.startsWith(COMMENT) && !line.trim().isEmpty()) {
+                    args.addAll(Arrays.asList(StringUtils.split(line)));
+                }
+            }
+        } catch (final IOException e) {
+            throw new CommandLineException("I/O error loading arguments file:" + argumentsFile, e);
+        }
+        return args;
+    }
+
+    private void printArgumentUsage(final StringBuilder sb, final ArgumentDefinition argumentDefinition) {
+        printArgumentParamUsage(sb, argumentDefinition.getLongName(), argumentDefinition.shortName,
+                CommandLineParser.getUnderlyingType(argumentDefinition.field).getSimpleName(),
+                makeArgumentDescription(argumentDefinition));
+    }
+
+
+    private void printArgumentParamUsage(final StringBuilder sb, final String name, final String shortName,
+                                         final String type, final String argumentDescription) {
+        String argumentLabel = name;
+        if (type != null) argumentLabel = "--"+ argumentLabel;
+
+        if (!shortName.isEmpty()) {
+            argumentLabel+=",-" + shortName;
+        }
+        argumentLabel += ":" + type;
+        sb.append(argumentLabel);
+
+        int numSpaces = ARGUMENT_COLUMN_WIDTH - argumentLabel.length();
+        if (argumentLabel.length() > ARGUMENT_COLUMN_WIDTH) {
+            sb.append("\n");
+            numSpaces = ARGUMENT_COLUMN_WIDTH;
+        }
+        printSpaces(sb, numSpaces);
+        final String wrappedDescription = WordUtils.wrap(argumentDescription, DESCRIPTION_COLUMN_WIDTH);
+        final String[] descriptionLines = wrappedDescription.split("\n");
+        for (int i = 0; i < descriptionLines.length; ++i) {
+            if (i > 0) {
+                printSpaces(sb, ARGUMENT_COLUMN_WIDTH);
+            }
+            sb.append(descriptionLines[i]);
+            sb.append("\n");
+        }
+        sb.append("\n");
+    }
+
+    private String makeArgumentDescription(final ArgumentDefinition argumentDefinition) {
+        final StringBuilder sb = new StringBuilder();
+        if (!argumentDefinition.doc.isEmpty()) {
+            sb.append(argumentDefinition.doc);
+            sb.append("  ");
+        }
+        if (argumentDefinition.isCollection) {
+            if (argumentDefinition.optional) {
+                sb.append("This argument may be specified 0 or more times. ");
+            } else {
+                sb.append("This argument must be specified at least once. ");
+            }
+        }
+        if (argumentDefinition.optional) {
+            sb.append("Default value: ");
+            sb.append(argumentDefinition.defaultValue);
+            sb.append(". ");
+        } else {
+            sb.append("Required. ");
+        }
+        // if this argument definition is a string field contained within a plugin descriptor (i.e.,
+        // it holds the names of plugins specified by the user on the command line, such as read filter names),
+        // then we need to delegate to the plugin descriptor to generate the list of allowed values
+        if (CommandLinePluginDescriptor.class.isAssignableFrom(argumentDefinition.parent.getClass()) &&
+                CommandLineParser.getUnderlyingType(argumentDefinition.field).equals(String.class)) {
+            usageForPluginDescriptorArgument(argumentDefinition, sb);
+        } else {
+            sb.append(getOptions(CommandLineParser.getUnderlyingType(argumentDefinition.field)));
+        }
+        if (!argumentDefinition.mutuallyExclusive.isEmpty()) {
+            sb.append(" Cannot be used in conjuction with argument(s)");
+            for (final String argument : argumentDefinition.mutuallyExclusive) {
+                final ArgumentDefinition mutextArgumentDefinition = argumentMap.get(argument);
+
+                if (mutextArgumentDefinition == null) {
+                    throw new CommandLineException("Invalid argument definition in source code.  " + argument +
+                            " doesn't match any known argument.");
+                }
+                sb.append(" ").append(mutextArgumentDefinition.fieldName);
+                if (!mutextArgumentDefinition.shortName.isEmpty()) {
+                    sb.append(" (").append(mutextArgumentDefinition.shortName).append(")");
+                }
+            }
+        }
+        return sb.toString();
+    }
+
+    private void usageForPluginDescriptorArgument(final ArgumentDefinition argDef, final StringBuilder sb) {
+        final CommandLinePluginDescriptor<?> descriptor = (CommandLinePluginDescriptor<?>) argDef.parent;
+        // this argument came from a plugin descriptor; delegate to get the list of allowed values
+        final List<String> allowedValues = new ArrayList<>(descriptor.getAllowedValuesForDescriptorArgument(argDef.getLongName()));
+        if (allowedValues.isEmpty()) {
+            sb.append("Any value allowed");
+        } else {
+            allowedValues.sort(String.CASE_INSENSITIVE_ORDER);
+            sb.append("Possible Values: {");
+            sb.append(String.join(", ", allowedValues));
+            sb.append("}");
+        }
+    }
+
+    /**
+     * Generates the option help string for a {@code boolean} or {@link Boolean} typed argument.
+     * @return never {@code null}.
+     */
+    private String getBooleanOptions() {
+        return String.format("%s%s, %s%s", ENUM_OPTION_DOC_PREFIX, Boolean.TRUE, Boolean.FALSE, ENUM_OPTION_DOC_SUFFIX);
+    }
+
+    /**
+     * Composes the help string on the possible options an {@link Enum} typed argument can take.
+     *
+     * @param clazz target enum class. Assumed no to be {@code null}.
+     * @param <T> enum class type.
+     * @param <U> ClpEnum implementing version of <code><T&gt</code>;.
+     * @throws CommandLineException if {@code <T>} has no constants.
+     * @return never {@code null}.
+     */
+    private <T extends Enum<T>,U extends Enum<U> & ClpEnum> String getEnumOptions(final Class<T> clazz) {
+        // We assume that clazz is guaranteed to be a Class<? extends Enum>, thus
+        // getEnumConstants() won't ever return a null.
+        final T[] enumConstants = clazz.getEnumConstants();
+        if (enumConstants.length == 0) {
+            throw new CommandLineException(String.format("Bad argument enum type '%s' with no options", clazz.getName()));
+        }
+
+        if (ClpEnum.class.isAssignableFrom(clazz)) {
+            @SuppressWarnings("unchecked")
+            final U[] clpEnumCastedConstants = (U[]) enumConstants;
+            return getEnumOptionsWithDescription(clpEnumCastedConstants);
+        } else {
+            return getEnumOptionsWithoutDescription(enumConstants);
+        }
+    }
+
+    /**
+     * Composes the help string for enum options that do not provide additional help documentation.
+     * @param enumConstants the enum constants. Assumed non-null.
+     * @param <T> the enum type.
+     * @return never {@code null}.
+     */
+    private <T extends Enum<T>> String getEnumOptionsWithoutDescription(final T[] enumConstants) {
+        return Stream.of(enumConstants)
+                .map(T::name)
+                .collect(Collectors.joining(", ",ENUM_OPTION_DOC_PREFIX,ENUM_OPTION_DOC_SUFFIX));
+    }
+
+    /**
+     * Composes the help string for enum options that provide additional documentation.
+     * @param enumConstants the enum constants. Assumed non-null.
+     * @param <T> the enum type.
+     * @return never {@code null}.
+     */
+    private <T extends Enum<T> & ClpEnum> String getEnumOptionsWithDescription(final T[] enumConstants) {
+        final String optionsString = Stream.of(enumConstants)
+                .map(c -> String.format("%s (%s)",c.name(),c.getHelpDoc()))
+                .collect(Collectors.joining("\n"));
+        return String.join("\n",ENUM_OPTION_DOC_PREFIX,optionsString,ENUM_OPTION_DOC_SUFFIX);
+    }
+
+    /**
+     * Returns the help string with details about valid options for the given argument class.
+     *
+     * <p>
+     *     Currently this only make sense with {@link Boolean} and {@link Enum}. Any other class
+     *     will result in an empty string.
+     * </p>
+     *
+     * @param clazz the target argument's class.
+     * @return never {@code null}.
+     */
+    @SuppressWarnings({"unchecked","rawtypes"})
+    private String getOptions(final Class<?> clazz) {
+        if (clazz == Boolean.class) {
+            return getBooleanOptions();
+        } else if (clazz.isEnum()) {
+            final Class<? extends Enum> enumClass = (Class<? extends Enum>)clazz;
+            return getEnumOptions(enumClass);
+        } else {
+            return "";
+        }
+    }
+
+    private void printSpaces(final StringBuilder sb, final int numSpaces) {
+        for (int i = 0; i < numSpaces; ++i) {
+            sb.append(" ");
+        }
+    }
+
+    private void handleArgumentAnnotation(
+            final Field field, final Object parent, final CommandLinePluginDescriptor<?> controllingDescriptor) {
+        try {
+            field.setAccessible(true);
+            final Argument argumentAnnotation = field.getAnnotation(Argument.class);
+            final boolean isCollection = isCollectionField(field);
+            if (isCollection) {
+                field.setAccessible(true);
+                if (field.get(parent) == null) {
+                    createCollection(field, parent, "@Argument");
+                }
+            }
+            if (!canBeMadeFromString(CommandLineParser.getUnderlyingType(field))) {
+                throw new CommandLineException.CommandLineParserInternalException("@Argument member \"" + field.getName() +
+                        "\" must have a String constructor or be an enum");
+            }
+
+            final ArgumentDefinition argumentDefinition = new ArgumentDefinition(field, argumentAnnotation, parent, controllingDescriptor);
+
+            for (final String argument : argumentAnnotation.mutex()) {
+                final ArgumentDefinition mutextArgumentDef = argumentMap.get(argument);
+                if (mutextArgumentDef != null) {
+                    mutextArgumentDef.mutuallyExclusive.add(getArgumentNameForMutex(field, argumentAnnotation));
+                }
+            }
+            if (inArgumentMap(argumentDefinition)) {
+                throw new CommandLineException.CommandLineParserInternalException(argumentDefinition.getNames() + " has already been used.");
+            } else {
+                putInArgumentMap(argumentDefinition);
+                argumentDefinitions.add(argumentDefinition);
+            }
+        } catch (final IllegalAccessException e) {
+            throw new CommandLineException.ShouldNeverReachHereException("We should not have reached here because we set accessible to true", e);
+        }
+    }
+
+    private String getArgumentNameForMutex(final Field field, final Argument argumentAnnotation) {
+        if (!argumentAnnotation.fullName().isEmpty()) {
+            return argumentAnnotation.fullName();
+        } else if (!argumentAnnotation.shortName().isEmpty()) {
+            return argumentAnnotation.shortName();
+        } else {
+            return field.getName();
+        }
+    }
+
+    private void handlePositionalArgumentAnnotation(final Field field, Object parent) {
+        if (positionalArguments != null) {
+            throw new CommandLineException.CommandLineParserInternalException
+                    ("@PositionalArguments cannot be used more than once in an argument class.");
+        }
+        field.setAccessible(true);
+        positionalArguments = field;
+        positionalArgumentsParent = parent;
+        if (!isCollectionField(field)) {
+            throw new CommandLineException.CommandLineParserInternalException("@PositionalArguments must be applied to a Collection");
+        }
+
+        if (!canBeMadeFromString(CommandLineParser.getUnderlyingType(field))) {
+            throw new CommandLineException.CommandLineParserInternalException("@PositionalParameters member " + field.getName() +
+                    "does not have a String ctor");
+        }
+
+        final PositionalArguments positionalArgumentsAnnotation = field.getAnnotation(PositionalArguments.class);
+        minPositionalArguments = positionalArgumentsAnnotation.minElements();
+        maxPositionalArguments = positionalArgumentsAnnotation.maxElements();
+        if (minPositionalArguments > maxPositionalArguments) {
+            throw new CommandLineException.CommandLineParserInternalException("In @PositionalArguments, minElements cannot be > maxElements");
+        }
+        try {
+            field.setAccessible(true);
+            if (field.get(parent) == null) {
+                createCollection(field, parent, "@PositionalParameters");
+            }
+        } catch (final IllegalAccessException e) {
+            throw new CommandLineException.ShouldNeverReachHereException("We should not have reached here because we set accessible to true", e);
+
+        }
+    }
+
+
+    private static boolean isCollectionField(final Field field) {
+        try {
+            field.getType().asSubclass(Collection.class);
+            return true;
+        } catch (final ClassCastException e) {
+            return false;
+        }
+    }
+
+    private void createCollection(final Field field, final Object callerArguments, final String annotationType)
+            throws IllegalAccessException {
+        try {
+            field.set(callerArguments, field.getType().newInstance());
+        } catch (final Exception ex) {
+            try {
+                field.set(callerArguments, new ArrayList<>());
+            } catch (final IllegalArgumentException e) {
+                throw new CommandLineException.CommandLineParserInternalException("In collection " + annotationType +
+                        " member " + field.getName() +
+                        " cannot be constructed or auto-initialized with ArrayList, so collection must be initialized explicitly.");
+            }
+
+        }
+
+    }
+
+    // True if clazz is an enum, or if it has a ctor that takes a single String argument.
+    private boolean canBeMadeFromString(final Class<?> clazz) {
+        if (clazz.isEnum()) {
+            return true;
+        }
+        try {
+            // Need to use getDeclaredConstructor() instead of getConstructor() in case the constructor
+            // is non-public
+            clazz.getDeclaredConstructor(String.class);
+            return true;
+        } catch (final NoSuchMethodException e) {
+            return false;
+        }
+    }
+
+    @SuppressWarnings({"unchecked", "rawtypes"})
+    private Object constructFromString(final Class clazz, final String s, final String argumentName) {
+        try {
+            if (clazz.isEnum()) {
+                try {
+                    return Enum.valueOf(clazz, s);
+                } catch (final IllegalArgumentException e) {
+                    throw new CommandLineException.BadArgumentValue(argumentName, s, "'" + s + "' is not a valid value for " +
+                            clazz.getSimpleName() + ". "+ getEnumOptions(clazz) );
+                }
+            }
+            // Need to use getDeclaredConstructor() instead of getConstructor() in case the constructor
+            // is non-public. Set it to be accessible if it isn't already.
+            final Constructor<?> ctor = clazz.getDeclaredConstructor(String.class);
+            ctor.setAccessible(true);
+            return ctor.newInstance(s);
+        } catch (final NoSuchMethodException e) {
+            // Shouldn't happen because we've checked for presence of ctor
+            throw new CommandLineException.ShouldNeverReachHereException("Cannot find string ctor for " + clazz.getName(), e);
+        } catch (final InstantiationException e) {
+            throw new CommandLineException.CommandLineParserInternalException("Abstract class '" + clazz.getSimpleName() +
+                    "'cannot be used for an argument value type.", e);
+        } catch (final IllegalAccessException e) {
+            throw new CommandLineException.CommandLineParserInternalException("String constructor for argument value type '" + clazz.getSimpleName() +
+                    "' must be public.", e);
+        } catch (final InvocationTargetException e) {
+            throw new CommandLineException.BadArgumentValue(argumentName, s, "Problem constructing " + clazz.getSimpleName() +
+                    " from the string '" + s + "'.");
+        }
+    }
+
+    public static class ArgumentDefinition {
+        public final Field field;
+        public final Class<?> type;
+        final String fieldName;
+        public final String fullName;
+        public final String shortName;
+        public final String doc;
+        public final boolean optional;
+        final boolean isCollection;
+        public final String defaultValue;
+        public final boolean isCommon;
+        boolean hasBeenSet = false;
+        public final Set<String> mutuallyExclusive;
+        public final Object parent;
+        final boolean isSpecial;
+        final boolean isSensitive;
+        public final CommandLinePluginDescriptor<?> controllingDescriptor;
+        final Double maxValue;
+        final Double minValue;
+        final Double maxRecommendedValue;
+        final Double minRecommendedValue;
+        final boolean isHidden;
+        final boolean isAdvanced;
+
+        public ArgumentDefinition(
+                final Field field,
+                final Argument annotation,
+                final Object parent,
+                final CommandLinePluginDescriptor<?> controllingDescriptor) {
+            this.field = field;
+            this.fieldName = field.getName();
+            this.parent = parent;
+            this.fullName = annotation.fullName();
+            this.shortName = annotation.shortName();
+            this.doc = annotation.doc();
+            this.isCollection = isCollectionField(field);
+
+            this.isCommon = annotation.common();
+            this.isSpecial = annotation.special();
+            this.isSensitive = annotation.sensitive();
+
+            this.mutuallyExclusive = new LinkedHashSet<>(Arrays.asList(annotation.mutex()));
+            this.controllingDescriptor = controllingDescriptor;
+
+            Object tmpDefault = getFieldValue();
+            if (tmpDefault != null) {
+                if (isCollection && ((Collection) tmpDefault).isEmpty()) {
+                    //treat empty collections the same as uninitialized primitive types
+                    this.defaultValue = NULL_STRING;
+                } else {
+                    //this is an initialized primitive type or a non-empty collection
+                    this.defaultValue = tmpDefault.toString();
+                }
+            } else {
+                this.defaultValue = NULL_STRING;
+            }
+
+            //null collections have been initialized by createCollection which is called in handleArgumentAnnotation
+            //this is optional if it's specified as being optional or if there is a default value specified
+            this.optional = annotation.optional() || ! this.defaultValue.equals(NULL_STRING);
+            this.maxValue = annotation.maxValue();
+            this.minValue = annotation.minValue();
+            this.maxRecommendedValue = annotation.maxRecommendedValue();
+            this.minRecommendedValue = annotation.minRecommendedValue();
+            // bounds should be only set for numeric arguments and if the type is integer it should
+            // be set to an integer
+            this.type = CommandLineParser.getUnderlyingType(this.field);
+            if (! Number.class.isAssignableFrom(this.type)) {
+                if (hasBoundedRange() || hasRecommendedRange()) {
+                    throw new CommandLineException.CommandLineParserInternalException(String.format("Min/max value ranges can only be set for numeric arguments. Argument --%s has a minimum or maximum value but has a non-numeric type.", this.getLongName()));
+                }
+            }
+            if (Integer.class.isAssignableFrom(this.type)) {
+                if (!isInfinityOrMathematicalInteger(this.maxValue)
+                        || !isInfinityOrMathematicalInteger(this.minValue)
+                        || !isInfinityOrMathematicalInteger(this.maxRecommendedValue)
+                        || !isInfinityOrMathematicalInteger(this.minRecommendedValue)) {
+                    throw new CommandLineException.CommandLineParserInternalException(String.format("Integer argument --%s has a minimum or maximum attribute with a non-integral value.", this.getLongName()));
+                }
+            }
+            this.isHidden = field.getAnnotation(Hidden.class) != null;
+            if (this.isHidden && !this.optional) {
+                // required arguments cannot be hidden, because they should be provided in the command line
+                throw new CommandLineException.CommandLineParserInternalException(String.format("A required argument cannot be annotated with @Hidden: %s", this.getLongName()));
+            }
+            this.isAdvanced = field.getAnnotation(Advanced.class) != null;
+            if (this.isAdvanced && !this.optional) {
+                // required arguments cannot be advanced, because they represent options that should be changed carefully
+                throw new CommandLineException.CommandLineParserInternalException(String.format("A required argument cannot be annotated with @Advanced: %s", this.getLongName()));
+            }
+        }
+
+        public Object getFieldValue(){
+            try {
+                field.setAccessible(true);
+                return field.get(parent);
+            } catch (IllegalAccessException e) {
+                throw new CommandLineException.ShouldNeverReachHereException("This shouldn't happen since we setAccessible(true).", e);
+            }
+        }
+
+        public void setFieldValue(final Object value){
+            try {
+                field.setAccessible(true);
+                field.set(parent, value);
+            } catch (IllegalAccessException e) {
+                throw new CommandLineException.ShouldNeverReachHereException("BUG: couldn't set field value. For "
+                        + fieldName +" in " + parent.toString() + " with value " + value.toString()
+                        + " This shouldn't happen since we setAccessible(true)", e);
+            }
+        }
+
+        public boolean isFlag(){
+            return field.getType().equals(boolean.class) || field.getType().equals(Boolean.class);
+        }
+
+        /** Returns {@code true} if the argument has a bounded range; {@code false} otherwise. */
+        private boolean hasBoundedRange() {
+            return this.minValue != Double.NEGATIVE_INFINITY || this.maxValue != Double.POSITIVE_INFINITY;
+        }
+
+        private boolean hasRecommendedRange() {
+            return this.maxRecommendedValue != Double.POSITIVE_INFINITY || this.minRecommendedValue != Double.NEGATIVE_INFINITY;
+        }
+
+        /**
+         * Determine if this argument definition is controlled by a plugin (and thus subject to
+         * descriptor dependency validation).
+         * @return
+         */
+        public boolean isControlledByPlugin() { return controllingDescriptor != null; }
+
+        public List<String> getNames(){
+            List<String> names = new ArrayList<>();
+            if (!shortName.isEmpty()){
+                names.add(shortName);
+            }
+            if (!fullName.isEmpty()){
+                names.add(fullName);
+            } else {
+                names.add(fieldName);
+            }
+            return names;
+        }
+
+        public String getLongName(){
+            return !fullName.isEmpty() ? fullName : fieldName;
+        }
+
+        /**
+         * Comparator for sorting ArgumentDefinitions in alphabetical order b y longName
+         */
+        public static Comparator<ArgumentDefinition> sortByLongName = new Comparator<ArgumentDefinition>() {
+            public int compare(ArgumentDefinition argDef1, ArgumentDefinition argDef2) {
+                return String.CASE_INSENSITIVE_ORDER.compare(argDef1.getLongName(), argDef2.getLongName());
+            }
+        };
+
+        /**
+         * Helper for pretty printing this option.
+         * @param value A value this argument was given
+         * @return a string
+         *
+         */
+        private String prettyNameValue(Object value) {
+            if(value != null){
+                if (isSensitive){
+                    return String.format("--%s ***********", getLongName());
+                } else {
+                    if (value instanceof TaggedArgument) {
+                        TaggedArgument taggedArg = (TaggedArgument) value;
+                        return String.format("--%s %s", TaggedArgumentParser.getDisplayString(getLongName(), taggedArg), value);
+                    } else {
+                        return String.format("--%s %s", getLongName(), value);
+                    }
+                }
+            }
+            return "";
+        }
+
+        /**
+         * @return A string representation of this argument and it's value(s) which would be valid if copied and pasted
+         * back as a command line argument
+         */
+        public String toCommandLineString(){
+            Object value = getFieldValue();
+            if (this.isCollection){
+                Collection<?> collect = (Collection<?>)value;
+                return collect.stream()
+                        .map(this::prettyNameValue)
+                        .collect(Collectors.joining(" "));
+
+            } else {
+                return prettyNameValue(value);
+            }
+        }
+
+    }
+
+    /**
+     * The commandline used to run this program, including any default args that
+     * weren't necessarily specified. This is used for logging and debugging.
+     * <p/>
+     * NOTE: {@link #parseArguments(PrintStream, String[])} must be called before
+     * calling this method.
+     *
+     * @return The commandline, or null if {@link #parseArguments(PrintStream, String[])}
+     * hasn't yet been called, or didn't complete successfully.
+     */
+    @SuppressWarnings("unchecked")
+    @Override
+    public String getCommandLine() {
+        final String toolName = callerArguments.getClass().getSimpleName();
+        final StringBuilder commandLineString = new StringBuilder();
+
+        final List<Object> positionalArgs;
+        if( positionalArguments != null) {
+            try {
+                positionalArguments.setAccessible(true);
+                positionalArgs = (List<Object>) positionalArguments.get(positionalArgumentsParent);
+            } catch (IllegalAccessException e) {
+                throw new CommandLineException.ShouldNeverReachHereException("Should never reach here because we setAccessible(true)", e);
+            }
+            for (final Object posArg : positionalArgs) {
+                commandLineString.append(" ").append(posArg.toString());
+            }
+        }
+
+        //first, append args that were explicitly set
+        commandLineString.append(argumentDefinitions.stream()
+                .filter(argumentDefinition -> argumentDefinition.hasBeenSet)
+                .map(ArgumentDefinition::toCommandLineString)
+                .collect(Collectors.joining(" ", " ", "  ")))
+                //next, append args that weren't explicitly set, but have a default value
+                .append(argumentDefinitions.stream()
+                        .filter(argumentDefinition -> !argumentDefinition.hasBeenSet && !argumentDefinition.defaultValue.equals(NULL_STRING))
+                        .map(ArgumentDefinition::toCommandLineString)
+                        .collect(Collectors.joining(" ")));
+
+        return toolName + " " + commandLineString.toString();
+    }
+
+}
diff --git a/src/main/java/org/broadinstitute/barclay/argparser/CommandLineException.java b/src/main/java/org/broadinstitute/barclay/argparser/CommandLineException.java
new file mode 100644
index 0000000..4241811
--- /dev/null
+++ b/src/main/java/org/broadinstitute/barclay/argparser/CommandLineException.java
@@ -0,0 +1,106 @@
+package org.broadinstitute.barclay.argparser;
+
+import java.util.function.DoubleFunction;
+
+/**
+ * Exceptions thrown by CommandLineParser implementations.
+ */
+public class CommandLineException extends RuntimeException {
+    private static final long serialVersionUID = 1L;
+
+    public CommandLineException( String msg ) {
+        super(msg);
+    }
+
+    public CommandLineException( String message, Throwable throwable ) {
+        super(message, throwable);
+    }
+
+    // todo -- fix up exception cause passing
+    public static class MissingArgument extends CommandLineException {
+        private static final long serialVersionUID = 0L;
+
+        public MissingArgument(String arg, String message) {
+            super(String.format("Argument %s was missing: %s", arg, message));
+        }
+    }
+
+    public static class BadArgumentValue extends CommandLineException {
+        private static final long serialVersionUID = 0L;
+
+        public BadArgumentValue(String arg, String value) {
+            super(String.format("Argument %s has a bad value: %s", arg, value));
+        }
+
+        public BadArgumentValue(String arg, String value, String message){
+            super(String.format("Argument %s has a bad value: %s. %s", arg, value,message));
+        }
+
+        public BadArgumentValue(String message) {
+            super(String.format("Illegal argument value: %s", message));
+        }
+    }
+
+    public static class OutOfRangeArgumentValue extends BadArgumentValue {
+        private static final long serialVersionUID = 0L;
+
+        public OutOfRangeArgumentValue(final String argName, final double minValue, final double maxValue, final Object value) {
+            super(argName, getValueString(value), getMessage(minValue, maxValue, value instanceof Integer));
+        }
+
+        // to handle null values
+        private static String getValueString(final Object value) {
+            return (value == null) ? "null" : value.toString();
+        }
+
+        // get the message for the values, correctly formatted
+        private static String getMessage(final double minValue, final double maxValue, final boolean asInt) {
+            final boolean outMinValue = minValue != Double.NEGATIVE_INFINITY;
+            final boolean outMaxValue = maxValue != Double.POSITIVE_INFINITY;
+            final DoubleFunction<String> toString = (asInt) ? Double::toString : v -> Integer.toString((int) Math.rint(v));
+            if (outMinValue && outMaxValue) {
+                return String.format("allowed range [%s, %s].", toString.apply(minValue), toString.apply(maxValue));
+            } else if (outMinValue) {
+                return String.format("minimum allowed value %s", toString.apply(minValue));
+            } else if (outMaxValue) {
+                return String.format("maximum allowed value %s", toString.apply(maxValue));
+            }
+            // this should never be reached
+            throw new IllegalArgumentException("Unbounded range should not thrown this exception");
+        }
+
+    }
+
+    /**
+     * <p/>
+     * Class CommandLineParserInternalException
+     * <p/>
+     * For internal errors in the command line parser not related to syntax errors in the command line itself.
+     */
+    public static class CommandLineParserInternalException extends RuntimeException {
+        private static final long serialVersionUID = 0L;
+        public CommandLineParserInternalException( final String s ) {
+            super(s);
+        }
+
+        public CommandLineParserInternalException( final String s, final Throwable throwable ) {
+            super(s, throwable);
+        }
+    }
+
+    /**
+     * For wrapping errors that are believed to never be reachable
+     * Package protected to restrict usage to the arg parsers.
+     */
+    static class ShouldNeverReachHereException extends CommandLineParserInternalException {
+        private static final long serialVersionUID = 0L;
+        public ShouldNeverReachHereException( final String s ) {
+            super(s);
+        }
+        public ShouldNeverReachHereException( final String s, final Throwable throwable ) {
+            super(s, throwable);
+        }
+        public ShouldNeverReachHereException( final Throwable throwable) {this("Barclay command line parser; should never reach here.", throwable);}
+    }
+
+}
diff --git a/src/main/java/org/broadinstitute/barclay/argparser/CommandLineParser.java b/src/main/java/org/broadinstitute/barclay/argparser/CommandLineParser.java
new file mode 100644
index 0000000..4a40f03
--- /dev/null
+++ b/src/main/java/org/broadinstitute/barclay/argparser/CommandLineParser.java
@@ -0,0 +1,202 @@
+package org.broadinstitute.barclay.argparser;
+
+import org.apache.commons.lang3.tuple.Pair;
+
+import java.io.PrintStream;
+import java.lang.reflect.Field;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Collection;
+
+/**
+ * Interface implemented by Barclay command line argument parsers.
+ */
+public interface CommandLineParser {
+
+    /**
+     * Parse command-line arguments in an object passed to the implementing class ctor.
+     *
+     * @param messageStream Where to write error messages.
+     * @param args          Command line tokens.
+     * @return true if command line is valid and the program should run, false if help or version was requested
+     * @throws CommandLineException if there is an invalid command line
+     */
+    public boolean parseArguments(final PrintStream messageStream, final String[] args);
+
+    /**
+     * The commandline used to run this program, including any default args that
+     * weren't necessarily specified. This is used for logging and debugging.
+     * <p/>
+     * NOTE: {@link #parseArguments(PrintStream, String[])} must be called before
+     * calling this method.
+     *
+     * @return The commandline, or null if {@link #parseArguments(PrintStream, String[])}
+     * hasn't yet been called, or didn't complete successfully.
+     */
+    public String getCommandLine();
+
+    /**
+     * A typical command line program will call this to get the beginning of the usage message,
+     * and then append a description of the program, like this:
+     *
+     * commandLineParser.getStandardUsagePreamble(getClass()) + "Frobnicates the freebozzle."
+     */
+    public abstract String getStandardUsagePreamble(final Class<?> mainClass);
+
+    public abstract String getVersion();
+
+    /**
+     * Return the plugin instance corresponding to the targetDescriptor class
+     */
+    public default <T> T getPluginDescriptor(Class<T> targetDescriptor) {
+        // Throw unless overridden - the legacy command line parser doesn't implement plugins
+        throw new CommandLineException.CommandLineParserInternalException(
+                "Command line plugins are not implemented by this command line parser"
+        );
+    }
+
+    /**
+     * Print a usage message based on the arguments object passed to the ctor.
+
+     * @param printCommon True if common args should be included in the usage message.
+     * @param printHidden True if hidden args should be included in the usage message.
+     * @return Usage string generated by the command line parser.
+     */
+    public abstract String usage(final boolean printCommon, final boolean printHidden);
+
+    /**
+     * Interface for @Argument annotated enums that have user documentation.
+     */
+    public interface ClpEnum {
+        String getHelpDoc();
+    }
+
+    /**
+     * Locates and returns the VALUES of all Argument-annotated fields of a specified type in a given object,
+     * pairing each field value with its corresponding Field object.
+     *
+     * Must be called AFTER argument parsing and value injection into argumentSource is complete (otherwise there
+     * will be no values to gather!). As a result, this is implemented as a static utility method into which
+     * the fully-initialized tool instance must be passed.
+     *
+     * Locates Argument-annotated fields of the target type, subtypes of the target type, and Collections of
+     * the target type or one of its subtypes. Unpacks Collection fields, returning a separate Pair for each
+     * value in each Collection.
+     *
+     * Searches argumentSource itself, as well as ancestor classes, and also recurses into any ArgumentCollections
+     * found.
+     *
+     * Will return Pairs containing a null second element for fields having no value, including empty Collection fields
+     * (these represent arguments of the target type that were not specified on the command line and so never initialized).
+     *
+     * @param type Target type. Search for Argument-annotated fields that are either of this type, subtypes of this type, or Collections of this type or one of its subtypes.
+     * @param argumentSource Object whose fields to search. Must have already undergone argument parsing and argument value injection.
+     * @param <T> Type parameter representing the type to search for and return
+     * @return A List of Pairs containing all Argument-annotated field values found of the target type. First element in each Pair
+     *         is the Field object itself, and the second element is the actual value of the argument field. The second
+     *         element will be null for uninitialized fields.
+     */
+    public static <T> List<Pair<Field, T>> gatherArgumentValuesOfType( final Class<T> type, final Object argumentSource ) {
+        List<Pair<Field, T>> argumentValues = new ArrayList<>();
+
+        // Examine all fields in argumentSource (including superclasses)
+        for ( Field field : getAllFields(argumentSource.getClass()) ) {
+            field.setAccessible(true);
+
+            try {
+                // Consider only fields that have Argument annotations and are either of the target type,
+                // subtypes of the target type, or Collections of the target type or one of its subtypes:
+                if ( field.getAnnotation(Argument.class) != null && type.isAssignableFrom(getUnderlyingType(field)) ) {
+
+                    if ( isCollectionField(field) ) {
+                        // Collection arguments are guaranteed by the parsing system to be non-null (at worst, empty)
+                        Collection<?> argumentContainer = (Collection<?>)field.get(argumentSource);
+
+                        // Emit a Pair with an explicit null value for empty Collection arguments
+                        if ( argumentContainer.isEmpty() ) {
+                            argumentValues.add(Pair.of(field, null));
+                        }
+                        // Unpack non-empty Collections of the target type into individual values,
+                        // each paired with the same Field object.
+                        else {
+                            for ( Object argumentValue : argumentContainer ) {
+                                argumentValues.add(Pair.of(field, type.cast(argumentValue)));
+                            }
+                        }
+                    }
+                    else {
+                        // Add values for non-Collection arguments of the target type directly
+                        argumentValues.add(Pair.of(field, type.cast(field.get(argumentSource))));
+                    }
+                }
+                else if ( field.getAnnotation(ArgumentCollection.class) != null ) {
+                    // Recurse into ArgumentCollections for more potential matches.
+                    argumentValues.addAll(gatherArgumentValuesOfType(type, field.get(argumentSource)));
+                }
+            }
+            catch ( IllegalAccessException e ) {
+                throw new CommandLineException.ShouldNeverReachHereException("field access failed after setAccessible(true)");
+            }
+        }
+
+        return argumentValues;
+    }
+
+    /**
+     * Returns the type that each instance of the argument needs to be converted to. In
+     * the case of primitive fields it will return the wrapper type so that String
+     * constructors can be found.
+     */
+    static Class<?> getUnderlyingType(final Field field) {
+        if (isCollectionField(field)) {
+            final ParameterizedType clazz = (ParameterizedType) (field.getGenericType());
+            final Type[] genericTypes = clazz.getActualTypeArguments();
+            if (genericTypes.length != 1) {
+                throw new CommandLineException.CommandLineParserInternalException("Strange collection type for field " +
+                        field.getName());
+            }
+
+            // If the Collection's parametrized type is itself parametrized (eg., List<Foo<Bar>>),
+            // return the raw type of the outer parameter (Foo.class, in this example) to avoid a
+            // ClassCastException. Otherwise, return the Collection's type parameter directly as a Class.
+            return (Class<?>) (genericTypes[0] instanceof ParameterizedType ?
+                    ((ParameterizedType)genericTypes[0]).getRawType() :
+                    genericTypes[0]);
+
+        } else {
+            final Class<?> type = field.getType();
+            if (type == Byte.TYPE) return Byte.class;
+            if (type == Short.TYPE) return Short.class;
+            if (type == Integer.TYPE) return Integer.class;
+            if (type == Long.TYPE) return Long.class;
+            if (type == Float.TYPE) return Float.class;
+            if (type == Double.TYPE) return Double.class;
+            if (type == Boolean.TYPE) return Boolean.class;
+
+            return type;
+        }
+    }
+
+    static List<Field> getAllFields(Class<?> clazz) {
+        final List<Field> ret = new ArrayList<>();
+        do {
+            ret.addAll(Arrays.asList(clazz.getDeclaredFields()));
+            clazz = clazz.getSuperclass();
+        } while (clazz != null);
+        return ret;
+    }
+
+    public static boolean isCollectionField(final Field field) {
+        try {
+            field.getType().asSubclass(Collection.class);
+            return true;
+        } catch (final ClassCastException e) {
+            return false;
+        }
+    }
+
+
+}
diff --git a/src/main/java/org/broadinstitute/barclay/argparser/CommandLineParserOptions.java b/src/main/java/org/broadinstitute/barclay/argparser/CommandLineParserOptions.java
new file mode 100644
index 0000000..57e6c3b
--- /dev/null
+++ b/src/main/java/org/broadinstitute/barclay/argparser/CommandLineParserOptions.java
@@ -0,0 +1,21 @@
+package org.broadinstitute.barclay.argparser;
+
+/**
+ * Options used to control command line parser behavior.
+ */
+public enum CommandLineParserOptions  {
+
+    /**
+     * The default behavior for the parser is to:
+     *
+     * <p><ul>
+     *     <li>Replace the contents of a collection argument with any values from the command line</li>
+     *     <li>Optionally allow the special singleton value of "null" to clear the contents of the collection.</li>
+     * </ul></p>
+     *
+     * Specifying "APPEND_TO_COLLECTIONS" changes the behavior so that any collection arguments are ADDED to the
+     * initial values of the collection, and allows the special value "null" to be used first to clear the initial
+     * values.
+     */
+    APPEND_TO_COLLECTIONS    // default behavior is "replace"
+}
diff --git a/src/main/java/org/broadinstitute/barclay/argparser/CommandLinePluginDescriptor.java b/src/main/java/org/broadinstitute/barclay/argparser/CommandLinePluginDescriptor.java
new file mode 100644
index 0000000..f2bd72a
--- /dev/null
+++ b/src/main/java/org/broadinstitute/barclay/argparser/CommandLinePluginDescriptor.java
@@ -0,0 +1,178 @@
+package org.broadinstitute.barclay.argparser;
+
+import java.util.List;
+import java.util.Set;
+import java.util.function.Predicate;
+
+/**
+ * A base class for descriptors for plugins that can be dynamically discovered by the
+ * command line parser and specified as command line arguments. An instance of each
+ * plugin descriptor to be used should be passed to the command line parser, and will
+ * be queried to find the class and package names to search for all plugin classes
+ * that should be discovered dynamically. The command line parser will find all such
+ * classes, and delegate to the descriptor to obtain the corresponding plugin instance;
+ * the object returned to the parser is then added to the parser's list of argument sources.
+ *
+ * Descriptors (sub)classes should have at least one @Argument used to accumulate the
+ * user-specified instances of the plugin seen on the command line. Allowed values for
+ * this argument are the simple class names of the discovered plugin subclasses.
+ *
+ * Plugin (sub)classes:
+ *
+ * - should subclass a common base class (the name of which is returned by the descriptor)
+ * - may live in any one of the packages returned by the descriptor {@Link #getPackageNames},
+ *   but must have a unique simple name to avoid command line name collisions.
+ * - should contain @Arguments for any values they wish to collect. @Arguments may be
+ *   optional or required. If required, the arguments are in effect "provisionally
+ *   required" in that they are contingent on the specific plugin being specified on
+ *   the command line; they will only be marked by the command line parser as missing
+ *   if the they have not been specified on the command line, and the plugin class
+ *   containing the plugin argument *has* been specified on the command line (as
+ *   determined by the command line parser via a call to isDependentArgumentAllowed).
+ *
+ * NOTE: plugin class @Arguments that are marked "optional=false" should be not have a primitive
+ * type, and should not have an initial value, as the command line parser will interpret these as
+ * having been set even if they have not been specified on the command line. Conversely, @Arguments
+ * that are optional=true should have an initial value, since they parser will not require them
+ * to be set in the command line.
+ *
+ * The methods for each descriptor are called in the following order:
+ *
+ *  getPluginClass()/getPackageNames() - once when argument parsing begins (if the descriptor
+ *  has been passed to the command line parser as a target descriptor)
+ *
+ *  getClassFilter() - once for each plugin subclass found
+ *  getInstance() - once for each plugin subclass that isn't filtered out by getClassFilter
+ *  validateDependentArgumentAllowed  - once for each plugin argument value that has been
+ *  specified on the command line for a plugin that is controlled by this descriptor
+ *
+ *  validateArguments() - once when argument parsing is complete
+ *  getAllInstances() - whenever the pluggable class consumer wants the resulting plugin instances
+ *
+ *  getAllowedValuesForDescriptorArgument is only called when the command line parser is constructing
+ *  a help/usage message.
+ */
+public abstract class CommandLinePluginDescriptor<T> {
+
+    /**
+     * Return a display name to identify this plugin to the user
+     * @return A short user-friendly name for this plugin.
+     */
+    public String getDisplayName() { return getPluginClass().getSimpleName(); }
+
+    /**
+     * Base class for all command line plugin classes managed by this descriptor. Subclasses of
+     * this class in any of the packages returned by {@link #getPackageNames} will be command line
+     * accessible.
+     */
+    public abstract Class<?> getPluginClass();
+
+    /**
+     * List of package names from which to load command line plugin classes.
+     *
+     * Note that the simple name of each class must be unique, even across packages.
+     * @return List of package names.
+     */
+    public abstract List<String> getPackageNames();
+
+    /**
+     * Give this descriptor a chance to filter out any classes it doesn't want to be
+     * dynamically discoverable.
+     * @return false if the class shouldn't be used; otherwise true
+     */
+    public Predicate<Class<?>> getClassFilter() { return c -> true;}
+
+    /**
+     * Return an instance of the specified pluggable class. The descriptor should
+     * instantiate or otherwise obtain (possibly by having been provided an instance
+     * through the descriptor's constructor) an instance of this plugin class.
+     * The descriptor should maintain a list of these instances so they can later
+     * be retrieved by {@link #getAllInstances}.
+     *
+     * In addition, implementations should recognize and reject any attempt to instantiate
+     * a second instance of a plugin that has the same simple class name as another plugin
+     * controlled by this descriptor (which can happen if they have different qualified names
+     * within the base package used by the descriptor) since the user has no way to disambiguate
+     * these on the command line).
+     *
+     * @param pluggableClass a plugin class discovered by the command line parser that
+     *                       was not rejected by {@link #getClassFilter}
+     * @return the instantiated object that will be used by the command line parser
+     * as an argument source
+     * @throws IllegalAccessException
+     * @throws InstantiationException
+     */
+    public abstract Object getInstance(Class<?> pluggableClass)
+            throws IllegalAccessException, InstantiationException;
+
+    /**
+     * Return the allowable values for the String argument of this plugin descriptor
+     * that is specified by longArgName. Called by the command line parser to generate
+     * a usage string. If the value is unrecognized, the implementation should throw
+     * IllegalArgumentException.
+     *
+     * @param longArgName
+     * @return Set<String> of allowable values, or empty set if any value is allowed
+     */
+    public abstract Set<String> getAllowedValuesForDescriptorArgument(String longArgName);
+
+    /**
+     * Called by the command line parser when an argument value from the class specified
+     * by dependentClass has been seen on the command line.
+     *
+     * Return true if the argument is allowed (i.e., this name of this class was specified
+     * as a predecessor on the command line) otherwise false.
+     *
+     * This method can be used by both the command line parser and the descriptor class for
+     * determining when to issue error messages for "dangling" arguments (dependent arguments
+     * for which a value has been supplied on the command line, but for which the predecessor
+     * argument was not supplied).
+     *
+     * When this method returns "false", the parser will issue an error message if an argument
+     * value in this class has been set on the command line.
+     *
+     * @param dependentClass
+     * @return true if the plugin for this class was specified on the command line, or the
+     * values in this class may be set byt he user, otherwise false
+     */
+    public abstract boolean isDependentArgumentAllowed(Class<?> dependentClass);
+
+    /**
+     * This method is called after all command line arguments have been processed to allow
+     * the descriptor to validate the plugin arguments that have been specified.
+     *
+     * It is the descriptor's job to contain an argument list which will be populated
+     * by the command line parser with the name of each plugin specified on the command line,
+     * and for each such plugin, to maintain a list of the corresponding instance. This
+     * method gives the descriptor a chance to reduce that list to include only those
+     * instances actually seen on the command line.
+     *
+     * Implementations of this method should minimally validate that all of values that have
+     * been specified on the command line have a corresponding plugin instance (this will
+     * detect a user-specified value for which there is no corresponding plugin class).
+     *
+     * @throws CommandLineException if a plugin value has been specified that
+     * has no corresponding plugin instance (i.e., the plugin class corresponding to the name
+     * was not discovered)
+     */
+    public abstract void validateArguments() throws CommandLineException;
+
+    /**
+     * @return the default plugins used for this instance of this descriptor as a list of Object. Used for
+     * help/doc generation.
+     */
+    public abstract List<Object> getDefaultInstances();
+
+    /**
+     * @return an ordered List of actual plugin instances that have been specified on the command
+     * line, in the same order they were obtained/created by {@line #getInstance}).
+     */
+    public abstract List<T> getAllInstances();
+
+    /**
+     * @param pluginName Name of the plugin requested
+     * @return Class object for the plugin instance requested
+     */
+    public abstract Class<?> getClassForInstance(final String pluginName);
+
+}
diff --git a/src/main/java/org/broadinstitute/barclay/argparser/CommandLinePluginProvider.java b/src/main/java/org/broadinstitute/barclay/argparser/CommandLinePluginProvider.java
new file mode 100644
index 0000000..fc584a7
--- /dev/null
+++ b/src/main/java/org/broadinstitute/barclay/argparser/CommandLinePluginProvider.java
@@ -0,0 +1,11 @@
+package org.broadinstitute.barclay.argparser;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Interface implemented by command line programs that supply plugins to the command line parser.
+ */
+public interface CommandLinePluginProvider {
+    List<? extends CommandLinePluginDescriptor<?>> getPluginDescriptors();
+}
diff --git a/src/main/java/org/broadinstitute/barclay/argparser/CommandLineProgramGroup.java b/src/main/java/org/broadinstitute/barclay/argparser/CommandLineProgramGroup.java
new file mode 100644
index 0000000..312a702
--- /dev/null
+++ b/src/main/java/org/broadinstitute/barclay/argparser/CommandLineProgramGroup.java
@@ -0,0 +1,17 @@
+package org.broadinstitute.barclay.argparser;
+
+import java.util.Comparator;
+
+/**
+ * Interface for groups of CommandLinePrograms.
+ * @author Nils Homer
+ */
+public interface CommandLineProgramGroup {
+
+    /** Gets the name of this group. **/
+    public String getName();
+    /** Gets the description of this group. **/
+    public String getDescription();
+    /** Compares two program groups by name. **/
+    public static Comparator<CommandLineProgramGroup> comparator = (a, b) -> a.getName().compareTo(b.getName());
+}
diff --git a/src/main/java/org/broadinstitute/barclay/argparser/CommandLineProgramProperties.java b/src/main/java/org/broadinstitute/barclay/argparser/CommandLineProgramProperties.java
new file mode 100644
index 0000000..476a397
--- /dev/null
+++ b/src/main/java/org/broadinstitute/barclay/argparser/CommandLineProgramProperties.java
@@ -0,0 +1,31 @@
+package org.broadinstitute.barclay.argparser;
+
+import java.lang.annotation.*;
+
+/**
+ * Annotates a command line program with various properties, such as usage (short and long),
+ * as well as to which program group it belongs.
+ *
+ */
+ at Retention(RetentionPolicy.RUNTIME)
+ at Target(ElementType.TYPE)
+ at Inherited
+public @interface CommandLineProgramProperties {
+    /**
+     * @return a summary of what the program does
+     */
+    String summary();
+
+    /**
+     * @return a very short summary for the main menu list of all programs
+     */
+    String oneLineSummary();
+
+    /**
+     * @return an example command line for this program
+     */
+    String usageExample() default "The author of this program hasn't included any example usage, please complain to them.";
+    Class<? extends CommandLineProgramGroup> programGroup();
+
+    boolean omitFromCommandLine() default false;
+}
diff --git a/src/main/java/org/broadinstitute/barclay/argparser/Hidden.java b/src/main/java/org/broadinstitute/barclay/argparser/Hidden.java
new file mode 100644
index 0000000..4fbc84e
--- /dev/null
+++ b/src/main/java/org/broadinstitute/barclay/argparser/Hidden.java
@@ -0,0 +1,13 @@
+package org.broadinstitute.barclay.argparser;
+
+import java.lang.annotation.*;
+
+/**
+ * Indicates that an argument should not be presented in the help system.
+ */
+ at Documented
+ at Inherited
+ at Retention(RetentionPolicy.RUNTIME)
+ at Target({ElementType.FIELD})
+public @interface Hidden {
+}
diff --git a/src/main/java/org/broadinstitute/barclay/argparser/LegacyCommandLineArgumentParser.java b/src/main/java/org/broadinstitute/barclay/argparser/LegacyCommandLineArgumentParser.java
new file mode 100644
index 0000000..cbf70c6
--- /dev/null
+++ b/src/main/java/org/broadinstitute/barclay/argparser/LegacyCommandLineArgumentParser.java
@@ -0,0 +1,1029 @@
+/*
+ * The MIT License
+ *
+ * Copyright (c) 2009 The Broad Institute
+ *
+ * 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 org.broadinstitute.barclay.argparser;
+
+import org.apache.commons.lang3.text.WordUtils;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.PrintStream;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Pattern;
+
+/**
+ * Annotation-driven utility for parsing command-line arguments, checking for errors, and producing usage message.
+ * <p/>
+ * This class supports options of the form KEY=VALUE, plus positional arguments.  Positional arguments must not contain
+ * an equal sign lest they be mistaken for a KEY=VALUE pair.
+ * <p/>
+ * The caller must supply an object that both defines the command line and has the parsed options set into it.
+ * For each possible KEY=VALUE option, there must be a public data member annotated with @Argument.  The KEY name is
+ * the name of the data member.  An abbreviated name may also be specified with the shortName attribute of @Argument.
+ * If the data member is a List<T>, then the option may be specified multiple times.  The type of the data member,
+ * or the type of the List element must either have a ctor T(String), or must be an Enum.  List options must
+ * be initialized by the caller with some kind of list.  Any other option that is non-null is assumed to have the given
+ * value as a default.  If an option has no default value, and does not have the optional attribute of @Argument set,
+ * is required.  For List options, minimum and maximum number of elements may be specified in the @Argument annotation.
+ * <p/>
+ * A single List data member may be annotated with the @PositionalArguments.  This behaves similarly to a Option
+ * with List data member: the caller must initialize the data member, the type must be constructable from String, and
+ * min and max number of elements may be specified.  If no @PositionalArguments annotation appears in the object,
+ * then it is an error for the command line to contain positional arguments.
+ */
+public class LegacyCommandLineArgumentParser implements CommandLineParser {
+    // For formatting option section of usage message.
+    private static final int OPTION_COLUMN_WIDTH = 30;
+    private static final int DESCRIPTION_COLUMN_WIDTH = 90;
+
+    private static final Boolean[] TRUE_FALSE_VALUES = {Boolean.TRUE, Boolean.FALSE};
+
+    private static final String[] PACKAGES_WITH_WEB_DOCUMENTATION = {"picard"};
+
+    private static final String defaultUsagePreamble = "Usage: program [options...]\n";
+    private static final String defaultUsagePreambleWithPositionalArguments =
+            "Usage: program [options...] [positional-arguments...]\n";
+    private static final String OPTIONS_FILE = "OPTIONS_FILE";
+
+    /** name, shortName, description for options built in to framework */
+    private static final String[][] FRAMEWORK_OPTION_DOC = {
+            {"--help", "-h", "Displays options specific to this tool."},
+            {"--stdhelp", "-H", "Displays options specific to this tool AND " +
+                    "options common to all Picard command line tools."},
+            {"--version", null, "Displays program version."}
+    };
+
+    private final static Logger logger = LogManager.getLogger();
+
+    /**
+     * A typical command line program will call this to get the beginning of the usage message,
+     * and then append a description of the program, like this:
+     *
+     * getStandardUsagePreamble(getClass()) + "Frobnicates the freebozzle."
+     */
+    @Override
+    public String getStandardUsagePreamble(final Class<?> mainClass) {
+        return "USAGE: " + mainClass.getSimpleName() + " [options]\n\n" +
+                (hasWebDocumentation(mainClass) ?
+                        "Documentation: http://broadinstitute.github.io/picard/command-line-overview.html#" +
+                                mainClass.getSimpleName() + "\n\n"
+                        : "");
+    }
+
+    /**
+     * Determines if a class has web documentation based on its package name
+     *
+     * @param clazz
+     * @return true if the class has web documentation, false otherwise
+     */
+    public boolean hasWebDocumentation(final Class<?>  clazz) {
+        for (final String pkg : PACKAGES_WITH_WEB_DOCUMENTATION) {
+            if (clazz.getPackage().getName().startsWith(pkg)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * @return the link to a FAQ
+     */
+    public String getFaqLink() {
+        return "To get help, see http://broadinstitute.github.io/picard/index.html#GettingHelp";
+    }
+
+    // This is the object that the caller has provided that contains annotations,
+    // and into which the values will be assigned.
+    private final Object callerOptions;
+
+    // For child CommandLineParser, this contains the prefix for the option names, which is needed for generating
+    // the command line.  For non-nested, this is the empty string.
+    private final String prefix;
+    // For non-nested, empty string.  For nested, prefix + "."
+    private final String prefixDot;
+
+    // null if no @PositionalArguments annotation
+    private Field positionalArguments;
+    private int minPositionalArguments;
+    private int maxPositionalArguments;
+
+    // List of all the data members with @Argument annotation
+    private final List<OptionDefinition> optionDefinitions = new ArrayList<>();
+
+    // Maps long name, and short name, if present, to an option definition that is
+    // also in the optionDefinitions list.
+    private final Map<String, OptionDefinition> optionMap = new HashMap<>();
+
+    // For printing error messages when parsing command line.
+    private PrintStream messageStream;
+
+    // In case implementation wants to get at arg for some reason.
+    private String[] argv;
+
+    private String programVersion = null;
+
+    // The command line used to launch this program, including non-null default options that
+    // weren't explicitly specified. This is used for logging and debugging.
+    private String commandLine = "";
+
+    // The associated program properties using the CommandLineProgramProperties annotation
+    private final CommandLineProgramProperties programProperties;
+
+    /**
+     * Prepare for parsing command line arguments, by validating annotations.
+     *
+     * @param callerOptions This object contains annotations that define the acceptable command-line options,
+     *                      and ultimately will receive the settings when a command line is parsed.
+     */
+    public LegacyCommandLineArgumentParser(final Object callerOptions) {
+        this(callerOptions, "");
+    }
+
+    private String getUsagePreamble() {
+        String usagePreamble = "";
+        if (null != programProperties) {
+            usagePreamble += programProperties.summary();
+        } else if (positionalArguments == null) {
+            usagePreamble += defaultUsagePreamble;
+        } else {
+            usagePreamble += defaultUsagePreambleWithPositionalArguments;
+        }
+
+        if (null != this.programVersion && 0 < this.programVersion.length()) {
+            usagePreamble += "Version: " + getVersion() + "\n";
+        }
+        //checkForNonASCII(usagePreamble, "preamble");
+
+        return usagePreamble;
+    }
+
+    /**
+     * @param prefix Non-empty for child options object.
+     */
+    private LegacyCommandLineArgumentParser(final Object callerOptions, final String prefix) {
+        this.callerOptions = callerOptions;
+
+        this.prefix = prefix;
+        if (prefix.isEmpty()) {
+            prefixDot = "";
+        } else {
+            prefixDot = prefix + ".";
+        }
+
+        createArgumentDefinitions(callerOptions);
+
+        this.programProperties = this.callerOptions.getClass().getAnnotation(CommandLineProgramProperties.class);
+    }
+
+    private void createArgumentDefinitions(final Object callerArguments) {
+        for (final Field field : CommandLineParser.getAllFields(callerArguments.getClass())) {
+            if (field.getAnnotation(Argument.class) != null && field.getAnnotation(ArgumentCollection.class) != null){
+                throw new CommandLineException.CommandLineParserInternalException("An Argument cannot be an argument collection: "
+                        +field.getName() + " in " + callerArguments.toString() + " is annotated as both.");
+            }
+            if (field.getAnnotation(PositionalArguments.class) != null) {
+                handlePositionalArgumentAnnotation(field);
+            }
+            if (field.getAnnotation(Argument.class) != null) {
+                handleOptionAnnotation(field, callerArguments);
+            }
+            if (field.getAnnotation(ArgumentCollection.class) != null) {
+                try {
+                    field.setAccessible(true);
+                    createArgumentDefinitions(field.get(callerArguments));
+                } catch (final IllegalAccessException e) {
+                    throw new CommandLineException.ShouldNeverReachHereException("should never reach here because we setAccessible(true)", e);
+                }
+            }
+        }
+    }
+
+    @Override
+    public String getVersion() {
+        return this.callerOptions.getClass().getPackage().getImplementationVersion();
+    }
+
+    /**
+     * Print a usage message based on the options object passed to the ctor.
+     *
+     * @param printCommon True if common args should be included in the usage message.
+     * @param printHidden Ignored in legacy.
+     * @return Usage string generated by the command line parser.
+     */
+    @Override
+    public String usage(final boolean printCommon, final boolean printHidden) {
+        final StringBuilder sb = new StringBuilder();
+
+        if (!printHidden) {
+            logger.warn("Hidden arguments are always printed in LegacyCommandLineArgumentParser");
+        }
+
+        if (prefix.isEmpty()) {
+            final String preamble = htmlUnescape(convertFromHtml(getStandardUsagePreamble(callerOptions.getClass()) + getUsagePreamble()));
+            checkForNonASCII(preamble, "Tool description");
+            sb.append(preamble);
+            sb.append("\nVersion: " + getVersion());
+            sb.append("\n");
+            sb.append("\n\nOptions:\n\n");
+
+            for (final String[] optionDoc : FRAMEWORK_OPTION_DOC) {
+                printOptionParamUsage(sb, optionDoc[0], optionDoc[1], null, optionDoc[2]);
+            }
+        }
+
+        if (!optionDefinitions.isEmpty()) {
+            optionDefinitions.stream().filter(optionDefinition -> printCommon || !optionDefinition.isCommon).forEach(optionDefinition -> printOptionUsage(sb, optionDefinition));
+        }
+
+        if (printCommon) {
+            final Field fileField;
+            try {
+                //Temp class OPTIONS_FILE
+                class OptionFileContainerForUsage { public File optionFileContainer;}
+                fileField = OptionFileContainerForUsage.class.getField("optionFileContainer");
+            } catch (final NoSuchFieldException e) {
+                throw new CommandLineException("Should never happen", e);
+            }
+            final OptionDefinition optionsFileOptionDefinition =
+                    new OptionDefinition(fileField, null, OPTIONS_FILE, "",
+                            "File of OPTION_NAME=value pairs.  No positional parameters allowed.  Unlike command-line options, " +
+                                    "unrecognized options are ignored.  " + "A single-valued option set in an options file may be overridden " +
+                                    "by a subsequent command-line option.  " +
+                                    "A line starting with '#' is considered a comment.",
+                            false, false, 0, Integer.MAX_VALUE, null, true, new String[0]);
+            printOptionUsage(sb, optionsFileOptionDefinition);
+        }
+        return sb.toString();
+    }
+
+    static void checkForNonASCII(String documentationText, String location) {
+        if (documentationText.matches("[^\\p{ASCII}]")) {
+            throw new AssertionError("Non-ASCII character used in documentation ("+location+"). Only ASCII characters are allowed.");
+        }
+        //make sure that html-encoded non-ascii characters are found as well
+        if ( Pattern.compile(".*&[a-zA-Z]*?;.*",Pattern.MULTILINE).matcher(documentationText).find()) {
+            throw new AssertionError("Non-ASCII character used in documentation ("+location+"). Only ASCII characters are allowed.");
+        }
+    }
+    // package local for testing
+    static String convertFromHtml(final String textToConvert) {
+
+        //LinkedHashmap since the order matters
+        final Map<String, String> regexps = new LinkedHashMap<>();
+
+        regexps.put("< *a *href=[\'\"](.*?)[\'\"] *>(.*?)</ *a *>","$2 ($1)");
+        regexps.put("< *a *href=[\'\"](.*?)[\'\"] *>(.*?)< *a */>","$2 ($1)");
+        regexps.put("</ *(br|p|table|h[1-4]|pre|hr|li|ul) *>","\n");
+        regexps.put("< *(br|p|table|h[1-4]|pre|hr|li|ul) */>","\n");
+        regexps.put("< *(p|table|h[1-4]|ul|pre) *>","\n");
+        regexps.put("<li>", " - ");
+        regexps.put("</th>", "\t");
+        regexps.put("<\\w*?>", "");
+
+        return regexps.entrySet().stream().sequential()
+                .reduce(textToConvert, (string, entrySet) -> string.replaceAll(entrySet.getKey(), entrySet.getValue()), (a, b) -> b);
+    }
+
+    private static final Map<String, String> htmlToText = new LinkedHashMap<String, String>(){
+        private static final long serialVersionUID = 1L;
+        {
+            put("<","<");
+            put(">",">");
+            put("≥",">=");
+            put("≤","<=");
+
+            put("<p>","\n");
+        }
+    };
+
+    static String htmlUnescape(String str) {
+        // May need more here
+        return htmlToText.entrySet().stream().sequential()
+                .reduce(str, (string, entrySet) -> string.replace(entrySet.getKey(), entrySet.getValue()), (a, b) -> b);
+    }
+
+    /**
+     * Parse command-line options, and store values in callerOptions object passed to ctor.
+     *
+     * @param messageStream Where to write error messages.
+     * @param args          Command line tokens.
+     * @return true if command line is valid.
+     */
+    @Override
+    public boolean parseArguments(final PrintStream messageStream, final String[] args) {
+        this.argv = args;
+        this.messageStream = messageStream;
+        if (prefix.isEmpty()) {
+            commandLine = callerOptions.getClass().getSimpleName();
+        }
+        for (int i = 0; i < args.length; ++i) {
+            final String arg = args[i];
+            if (arg.equals("-h") || arg.equals("--help")) {
+                messageStream.append(usage(false, true));
+                return false;
+            }
+            if (arg.equals("-H") || arg.equals("--stdhelp")) {
+                messageStream.append(usage(true, true));
+                return false;
+            }
+
+            if (arg.equals("--version")) {
+                messageStream.println(getVersion());
+                return false;
+            }
+
+            final String[] pair = arg.split("=", 2);
+            if (pair.length == 2) {
+                if (pair[1].isEmpty() && i < args.length - 1) {
+                    pair[1] = args[++i];
+                }
+                if (!parseOption(pair[0], pair[1], false)) {
+                    messageStream.println();
+                    messageStream.append(usage(true, true));
+                    return false;
+                }
+            } else if (!parsePositionalArgument(arg)) {
+                messageStream.println();
+                messageStream.append(usage(false, true));
+                return false;
+            }
+        }
+        if (!checkNumArguments()) {
+            messageStream.println();
+            messageStream.append(usage(false, true));
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * After command line has been parsed, make sure that all required options have values, and that
+     * lists with minimum # of elements have sufficient.
+     *
+     * @return true if valid
+     */
+    private boolean checkNumArguments() {
+        //Also, since we're iterating over all options and args, use this opportunity to recreate the commandLineString
+        final StringBuilder commandLineString = new StringBuilder();
+        try {
+            for (final OptionDefinition optionDefinition : optionDefinitions) {
+                final String fullName = prefixDot + optionDefinition.name;
+                final StringBuilder mutextOptionNames = new StringBuilder();
+                for (final String mutexOption : optionDefinition.mutuallyExclusive) {
+                    final OptionDefinition mutextOptionDef = optionMap.get(mutexOption);
+                    if (mutextOptionDef != null && mutextOptionDef.hasBeenSet) {
+                        mutextOptionNames.append(' ').append(prefixDot).append(mutextOptionDef.name);
+                    }
+                }
+                if (optionDefinition.hasBeenSet && mutextOptionNames.length() > 0) {
+                    messageStream.println("ERROR: Option '" + fullName +
+                            "' cannot be used in conjunction with option(s)" +
+                            mutextOptionNames.toString());
+                    return false;
+                }
+                if (optionDefinition.isCollection) {
+                    final Collection<?> c = (Collection<?>) optionDefinition.field.get(optionDefinition.parent);
+                    if (c.size() < optionDefinition.minElements) {
+                        messageStream.println("ERROR: Option '" + fullName + "' must be specified at least " +
+                                optionDefinition.minElements + " times.");
+                        return false;
+                    }
+                } else if (!optionDefinition.optional && !optionDefinition.hasBeenSet &&
+                        !optionDefinition.hasBeenSetFromParent && mutextOptionNames.length() == 0) {
+                    messageStream.print("ERROR: Option '" + fullName + "' is required");
+                    if (optionDefinition.mutuallyExclusive.isEmpty()) {
+                        messageStream.println(".");
+                    } else {
+                        messageStream.println(" unless any of " + optionDefinition.mutuallyExclusive +
+                                " are specified.");
+                    }
+                    return false;
+                }
+            }
+
+            if (positionalArguments != null) {
+                final Collection<?> c = (Collection<?>) positionalArguments.get(callerOptions);
+                if (c.size() < minPositionalArguments) {
+                    messageStream.println("ERROR: At least " + minPositionalArguments +
+                            " positional arguments must be specified.");
+                    return false;
+                }
+                for (final Object posArg : c) {
+                    commandLineString.append(' ').append(posArg.toString());
+                }
+            }
+            //first, append args that were explicitly set
+            for (final OptionDefinition optionDefinition : optionDefinitions) {
+                if (optionDefinition.hasBeenSet) {
+                    commandLineString.append(' ').append(prefixDot).append(optionDefinition.name).append('=').append(
+                            optionDefinition.field.get(optionDefinition.parent));
+                }
+            }
+            commandLineString.append("   "); //separator to tell the 2 apart
+            //next, append args that weren't explicitly set, but have a default value
+            for (final OptionDefinition optionDefinition : optionDefinitions) {
+                if (!optionDefinition.hasBeenSet && !optionDefinition.defaultValue.equals("null")) {
+                    commandLineString.append(' ').append(prefixDot).append(optionDefinition.name).append('=').append(
+                            optionDefinition.defaultValue);
+                }
+            }
+            this.commandLine += commandLineString.toString();
+            return true;
+        } catch (final IllegalAccessException e) {
+            // Should never happen because lack of publicness has already been checked.
+            throw new RuntimeException(e);
+        }
+    }
+
+    @SuppressWarnings({"unchecked", "rawtypes"})
+    private boolean parsePositionalArgument(final String stringValue) {
+        if (positionalArguments == null) {
+            messageStream.println("ERROR: Invalid argument '" + stringValue + "'.");
+            return false;
+        }
+        final Object value;
+        try {
+            value = constructFromString(getUnderlyingType(positionalArguments), stringValue);
+        } catch (final CommandLineException e) {
+            messageStream.println("ERROR: " + e.getMessage());
+            return false;
+        }
+        final Collection c;
+        try {
+            c = (Collection) positionalArguments.get(callerOptions);
+        } catch (final IllegalAccessException e) {
+            throw new RuntimeException(e);
+        }
+        if (c.size() >= maxPositionalArguments) {
+            messageStream.println("ERROR: No more than " + maxPositionalArguments +
+                    " positional arguments may be specified on the command line.");
+            return false;
+        }
+        c.add(value);
+        return true;
+    }
+
+    @SuppressWarnings({"unchecked", "rawtypes"})
+    private boolean parseOption(String key, final String stringValue, final boolean optionsFile) {
+        key = key.toUpperCase();
+        if (key.equals(OPTIONS_FILE)) {
+            commandLine += " " + prefix + OPTIONS_FILE + "=" + stringValue;
+            return parseOptionsFile(stringValue);
+        }
+
+        final OptionDefinition optionDefinition = optionMap.get(key);
+        if (optionDefinition == null) {
+            if (optionsFile) {
+                // Silently ignore unrecognized option from options file
+                return true;
+            }
+            messageStream.println("ERROR: Unrecognized option: " + key);
+            return false;
+        }
+
+        if (!optionDefinition.isCollection && optionDefinition.hasBeenSet && !optionDefinition.hasBeenSetFromOptionsFile) {
+            messageStream.println("ERROR: Option '" + key + "' cannot be specified more than once.");
+            return false;
+        }
+        final Object value;
+        try {
+            if (stringValue.equals("null")) {
+                //"null" is a special value that allows the user to override any default
+                //value set for this arg. It can only be used for optional args. When
+                //used for a list arg, it will clear the list.
+                if (optionDefinition.optional) {
+                    value = null;
+                } else {
+                    messageStream.println("ERROR: non-null value must be provided for '" + key + "'.");
+                    return false;
+                }
+            } else {
+                value = constructFromString(getUnderlyingType(optionDefinition.field), stringValue);
+            }
+        } catch (final CommandLineException e) {
+            messageStream.println("ERROR: " + e.getMessage());
+            return false;
+        }
+        try {
+            if (optionDefinition.isCollection) {
+                final Collection c = (Collection) optionDefinition.field.get(optionDefinition.parent);
+                if (value == null) {
+                    //user specified this arg=null which is interpreted as empty list
+                    c.clear();
+                } else if (c.size() >= optionDefinition.maxElements) {
+                    messageStream.println("ERROR: Option '" + key + "' cannot be used more than " +
+                            optionDefinition.maxElements + " times.");
+                    return false;
+                } else {
+                    c.add(value);
+                }
+                optionDefinition.hasBeenSet = true;
+                optionDefinition.hasBeenSetFromOptionsFile = optionsFile;
+            } else {
+                optionDefinition.field.set(optionDefinition.parent, value);
+                optionDefinition.hasBeenSet = true;
+                optionDefinition.hasBeenSetFromOptionsFile = optionsFile;
+            }
+        } catch (final IllegalAccessException e) {
+            // Should never happen because we only iterate through public fields.
+            throw new RuntimeException(e);
+        }
+        return true;
+    }
+
+    /**
+     * Parsing of options from file is looser than normal.  Any unrecognized options are
+     * ignored, and a single-valued option that is set in a file may be overridden by a
+     * subsequent appearance of that option.
+     * A line that starts with '#' is ignored.
+     *
+     * @param optionsFile
+     * @return false if a fatal error occurred
+     */
+    private boolean parseOptionsFile(final String optionsFile) {
+        return parseOptionsFile(optionsFile, true);
+    }
+
+    /**
+     * @param optionFileStyleValidation true: unrecognized options are silently ignored; and a single-valued option may be overridden.
+     *                                  false: standard rules as if the options in the file were on the command line directly.
+     * @return
+     */
+    public boolean parseOptionsFile(final String optionsFile, final boolean optionFileStyleValidation) {
+        try (final BufferedReader reader = new BufferedReader(new FileReader(optionsFile))) {
+            String line;
+            while ((line = reader.readLine()) != null) {
+                if (line.startsWith("#") || line.trim().isEmpty()) {
+                    continue;
+                }
+                final String[] pair = line.split("=", 2);
+                if (pair.length == 2) {
+                    if (!parseOption(pair[0], pair[1], optionFileStyleValidation)) {
+                        messageStream.println();
+                        messageStream.append(usage(true, true));
+                        return false;
+                    }
+                } else {
+                    messageStream.println("Strange line in OPTIONS_FILE " + optionsFile + ": " + line);
+                    messageStream.append(usage(true, true));
+                    return false;
+                }
+            }
+            return true;
+
+        } catch (final IOException e) {
+            throw new CommandLineException("I/O error loading OPTIONS_FILE=" + optionsFile, e);
+        }
+    }
+
+    private void printHtmlOptionUsage(final PrintStream stream, final OptionDefinition optionDefinition) {
+        final String type = getUnderlyingType(optionDefinition.field).getSimpleName();
+        final String optionLabel = prefixDot + optionDefinition.name + " (" + type + ")";
+        stream.println("<tr><td>" + optionLabel + "</td><td>" + makeOptionDescription(optionDefinition) + "</td></tr>");
+    }
+
+    private void printOptionUsage(final StringBuilder sb, final OptionDefinition optionDefinition) {
+        printOptionParamUsage(sb, optionDefinition.name, optionDefinition.shortName,
+                getUnderlyingType(optionDefinition.field).getSimpleName(),
+                makeOptionDescription(optionDefinition));
+    }
+
+
+    private void printOptionParamUsage(final StringBuilder sb, final String name, final String shortName,
+                                       final String type, final String optionDescription) {
+        String optionLabel = prefixDot + name;
+        if (type != null) optionLabel += "=" + type;
+
+        sb.append(optionLabel);
+        if (shortName != null && !shortName.isEmpty()) {
+            sb.append("\n");
+            optionLabel = prefixDot + shortName;
+            if (type != null) optionLabel += "=" + type;
+            sb.append(optionLabel);
+        }
+
+        int numSpaces = OPTION_COLUMN_WIDTH - optionLabel.length();
+        if (optionLabel.length() > OPTION_COLUMN_WIDTH) {
+            sb.append("\n");
+            numSpaces = OPTION_COLUMN_WIDTH;
+        }
+        printSpaces(sb, numSpaces);
+        checkForNonASCII(optionDescription, name);
+        final String wrappedDescription = WordUtils.wrap(convertFromHtml(optionDescription), DESCRIPTION_COLUMN_WIDTH);
+        final String[] descriptionLines = wrappedDescription.split("\n");
+        for (int i = 0; i < descriptionLines.length; ++i) {
+            if (i > 0) {
+                printSpaces(sb, OPTION_COLUMN_WIDTH);
+            }
+            sb.append(descriptionLines[i]);
+            sb.append("\n");
+        }
+        sb.append("\n");
+    }
+
+    private String makeOptionDescription(final OptionDefinition optionDefinition) {
+        final StringBuilder sb = new StringBuilder();
+        if (!optionDefinition.doc.isEmpty()) {
+            sb.append(optionDefinition.doc);
+            sb.append("  ");
+        }
+        if (optionDefinition.optional) {
+            sb.append("Default value: ");
+            sb.append(optionDefinition.defaultValue);
+            sb.append(". ");
+            if (!optionDefinition.defaultValue.equals("null")) {
+                sb.append("This option can be set to 'null' to clear the default value. ");
+            }
+        } else if (!optionDefinition.isCollection) {
+            sb.append("Required. ");
+        }
+        Object[] enumConstants = getUnderlyingType(optionDefinition.field).getEnumConstants();
+        if (enumConstants == null && getUnderlyingType(optionDefinition.field) == Boolean.class) {
+            enumConstants = TRUE_FALSE_VALUES;
+        }
+
+        if (enumConstants != null) {
+            final Boolean isClpEnum = enumConstants.length > 0 && (enumConstants[0] instanceof ClpEnum);
+
+            sb.append("Possible values: {");
+            if (isClpEnum) sb.append('\n');
+
+            for (int i = 0; i < enumConstants.length; ++i) {
+                if (i > 0 && !isClpEnum) {
+                    sb.append(", ");
+                }
+                sb.append(enumConstants[i].toString());
+
+                if (isClpEnum) {
+                    sb.append(" (").append(((ClpEnum) enumConstants[i]).getHelpDoc()).append(")\n");
+                }
+            }
+            sb.append("} ");
+        }
+        if (optionDefinition.isCollection) {
+            if (optionDefinition.minElements == 0) {
+                if (optionDefinition.maxElements == Integer.MAX_VALUE) {
+                    sb.append("This option may be specified 0 or more times. ");
+                } else {
+                    sb.append("This option must be specified no more than ").append(optionDefinition.maxElements).append(
+                            " times. ");
+                }
+            } else if (optionDefinition.maxElements == Integer.MAX_VALUE) {
+                sb.append("This option must be specified at least ").append(optionDefinition.minElements).append(" times. ");
+            } else {
+                sb.append("This option may be specified between ").append(optionDefinition.minElements).append(
+                        " and ").append(optionDefinition.maxElements).append(" times. ");
+            }
+
+            if (!optionDefinition.defaultValue.equals("null")) {
+                sb.append("This option can be set to 'null' to clear the default list. ");
+            }
+
+        }
+        if (!optionDefinition.mutuallyExclusive.isEmpty()) {
+            sb.append(" Cannot be used in conjuction with option(s)");
+            for (final String option : optionDefinition.mutuallyExclusive) {
+                final OptionDefinition mutextOptionDefinition = optionMap.get(option);
+
+                if (mutextOptionDefinition == null) {
+                    throw new CommandLineException("Invalid option definition in source code.  " + option +
+                            " doesn't match any known option.");
+                }
+
+                sb.append(' ').append(mutextOptionDefinition.name);
+                if (!mutextOptionDefinition.shortName.isEmpty()) {
+                    sb.append(" (").append(mutextOptionDefinition.shortName).append(')');
+                }
+            }
+        }
+        return sb.toString();
+    }
+
+    private void printSpaces(final StringBuilder sb, final int numSpaces) {
+        for (int i = 0; i < numSpaces; ++i) {
+            sb.append(' ');
+        }
+    }
+
+    /**
+     * @param field the command line parameter as a {@link Field}
+     */
+    private void handleOptionAnnotation(final Field field, Object parent) {
+        try {
+            field.setAccessible(true);
+            final Argument optionAnnotation = field.getAnnotation(Argument.class);
+            final boolean isCollection = isCollectionField(field);
+            if (isCollection) {
+                if (optionAnnotation.maxElements() == 0) {
+                    throw new CommandLineException.CommandLineParserInternalException("@Argument member " + field.getName() +
+                            "has maxElements = 0");
+                }
+                if (optionAnnotation.minElements() > optionAnnotation.maxElements()) {
+                    throw new CommandLineException.CommandLineParserInternalException("In @Argument member " + field.getName() +
+                            ", minElements cannot be > maxElements");
+                }
+                if (field.get(parent) == null) {
+                    createCollection(field, parent, "@Argument");
+                }
+            }
+            if (!canBeMadeFromString(getUnderlyingType(field))) {
+                throw new CommandLineException.CommandLineParserInternalException("@Argument member " + field.getName() +
+                        " must have a String ctor or be an enum");
+            }
+
+            final OptionDefinition optionDefinition = new OptionDefinition(field,
+                    parent,
+                    field.getName(),
+                    optionAnnotation.shortName(),
+                    optionAnnotation.doc(), optionAnnotation.optional() || (field.get(parent) != null),
+                    isCollection, optionAnnotation.minElements(),
+                    optionAnnotation.maxElements(), field.get(parent), optionAnnotation.common(),
+                    optionAnnotation.mutex());
+
+            // log a warning if boundaries are set
+            if (optionAnnotation.maxValue() != Double.POSITIVE_INFINITY) {
+                logger.warn("Maximum allowed value for argument --{} is not enforced", optionDefinition.name);
+            }
+            if (optionAnnotation.minValue() != Double.NEGATIVE_INFINITY) {
+                logger.warn("Minimum allowed value for argument --{} is not enforced", optionDefinition.name);
+            }
+            if (optionAnnotation.maxRecommendedValue() != Double.POSITIVE_INFINITY) {
+                logger.warn("Maximum recommended value for argument --{} is not checked", optionDefinition.name);
+            }
+            if (optionAnnotation.minRecommendedValue() != Double.NEGATIVE_INFINITY) {
+                logger.warn("Minimum recommended value for argument --{} is not checked", optionDefinition.name);
+            }
+
+            for (final String option : optionAnnotation.mutex()) {
+                final OptionDefinition mutextOptionDef = optionMap.get(option);
+                if (mutextOptionDef != null) {
+                    mutextOptionDef.mutuallyExclusive.add(field.getName());
+                }
+            }
+            if (optionMap.containsKey(optionDefinition.name)) {
+                throw new CommandLineException.CommandLineParserInternalException(optionDefinition.name + " has already been used.");
+            }
+            if (!optionDefinition.shortName.isEmpty() && !optionDefinition.shortName.equals(optionDefinition.name)) {
+                if (optionMap.containsKey(optionDefinition.shortName)) {
+                        throw new CommandLineException.CommandLineParserInternalException(optionDefinition.shortName +
+                                " has already been used");
+                } else {
+                    optionMap.put(optionDefinition.shortName, optionDefinition);
+                }
+            }
+            if (!optionMap.containsKey(optionDefinition.name)) {
+                optionMap.put(optionDefinition.name, optionDefinition);
+            }
+            optionDefinitions.add(optionDefinition);
+        } catch (final IllegalAccessException e) {
+            throw new CommandLineException.CommandLineParserInternalException(field.getName() +
+                    " must have public visibility to have @Argument annotation");
+        }
+    }
+
+    private void handlePositionalArgumentAnnotation(final Field field) {
+        if (positionalArguments != null) {
+            throw new CommandLineException.CommandLineParserInternalException
+                    ("@PositionalArguments cannot be used more than once in an option class.");
+        }
+        field.setAccessible(true);
+        positionalArguments = field;
+        if (!isCollectionField(field)) {
+            throw new CommandLineException.CommandLineParserInternalException("@PositionalArguments must be applied to a Collection");
+        }
+
+        if (!canBeMadeFromString(getUnderlyingType(field))) {
+            throw new CommandLineException.CommandLineParserInternalException("@PositionalParameters member " + field.getName() +
+                    "does not have a String ctor");
+        }
+
+        final PositionalArguments positionalArgumentsAnnotation = field.getAnnotation(PositionalArguments.class);
+        minPositionalArguments = positionalArgumentsAnnotation.minElements();
+        maxPositionalArguments = positionalArgumentsAnnotation.maxElements();
+        if (minPositionalArguments > maxPositionalArguments) {
+            throw new CommandLineException.CommandLineParserInternalException("In @PositionalArguments, minElements cannot be > maxElements");
+        }
+        try {
+            if (field.get(callerOptions) == null) {
+                createCollection(field, callerOptions, "@PositionalParameters");
+            }
+        } catch (final IllegalAccessException e) {
+            throw new CommandLineException.CommandLineParserInternalException(field.getName() +
+                    " must have public visibility to have @PositionalParameters annotation");
+
+        }
+    }
+
+    private boolean isCollectionField(final Field field) {
+        try {
+            field.getType().asSubclass(Collection.class);
+            return true;
+        } catch (final ClassCastException e) {
+            return false;
+        }
+    }
+
+    private void createCollection(final Field field, final Object callerOptions, final String annotationType)
+            throws IllegalAccessException {
+        try {
+            field.set(callerOptions, field.getType().newInstance());
+        } catch (final Exception ex) {
+            try {
+                field.set(callerOptions, new ArrayList<>());
+            } catch (final IllegalArgumentException e) {
+                throw new CommandLineException.CommandLineParserInternalException("In collection " + annotationType +
+                        " member " + field.getName() +
+                        " cannot be constructed or auto-initialized with ArrayList, so collection must be initialized explicitly.");
+            }
+
+        }
+
+    }
+
+    /**
+     * Returns the type that each instance of the argument needs to be converted to. In
+     * the case of primitive fields it will return the wrapper type so that String
+     * constructors can be found.
+     */
+    private Class<?> getUnderlyingType(final Field field) {
+        if (isCollectionField(field)) {
+            final ParameterizedType clazz = (ParameterizedType) (field.getGenericType());
+            final Type[] genericTypes = clazz.getActualTypeArguments();
+            if (genericTypes.length != 1) {
+                throw new CommandLineException.CommandLineParserInternalException("Strange collection type for field " +
+                        field.getName());
+            }
+            return (Class) genericTypes[0];
+
+        } else {
+            final Class<?> type = field.getType();
+            if (type == Byte.TYPE) return Byte.class;
+            if (type == Short.TYPE) return Short.class;
+            if (type == Integer.TYPE) return Integer.class;
+            if (type == Long.TYPE) return Long.class;
+            if (type == Float.TYPE) return Float.class;
+            if (type == Double.TYPE) return Double.class;
+            if (type == Boolean.TYPE) return Boolean.class;
+
+            return type;
+        }
+    }
+
+    // True if clazz is an enum, or if it has a ctor that takes a single String argument.
+    private boolean canBeMadeFromString(final Class<?> clazz) {
+        if (clazz.isEnum()) {
+            return true;
+        }
+        try {
+            clazz.getConstructor(String.class);
+            return true;
+        } catch (final NoSuchMethodException e) {
+            return false;
+        }
+    }
+
+    @SuppressWarnings({"rawtypes", "unchecked"})
+    private Object constructFromString(final Class clazz, final String s) {
+        try {
+            if (clazz.isEnum()) {
+                try {
+                    return Enum.valueOf(clazz, s);
+                } catch (final IllegalArgumentException e) {
+                    throw new CommandLineException("'" + s + "' is not a valid value for " +
+                            clazz.getSimpleName() + ".", e);
+                }
+            }
+            final Constructor ctor = clazz.getConstructor(String.class);
+            return ctor.newInstance(s);
+        } catch (final NoSuchMethodException e) {
+            // Shouldn't happen because we've checked for presence of ctor
+            throw new CommandLineException("Cannot find string ctor for " + clazz.getName(), e);
+        } catch (final InstantiationException e) {
+            throw new CommandLineException("Abstract class '" + clazz.getSimpleName() +
+                    "'cannot be used for an option value type.", e);
+        } catch (final IllegalAccessException e) {
+            throw new CommandLineException("String constructor for option value type '" + clazz.getSimpleName() +
+                    "' must be public.", e);
+        } catch (final InvocationTargetException e) {
+            throw new CommandLineException("Problem constructing " + clazz.getSimpleName() +
+                    " from the string '" + s + "'.", e.getCause());
+        }
+    }
+
+    public String[] getArgv() {
+        return argv;
+    }
+
+    protected static final class OptionDefinition {
+        final Field field;
+        final Object parent;
+        final String name;
+        final String shortName;
+        final String doc;
+        final boolean optional;
+        final boolean isCollection;
+        final int minElements;
+        final int maxElements;
+        final String defaultValue;
+        final boolean isCommon;
+        boolean hasBeenSet = false;
+        boolean hasBeenSetFromOptionsFile = false;
+        boolean hasBeenSetFromParent = false;
+        final Set<String> mutuallyExclusive;
+
+        private OptionDefinition(final Field field, final Object parent,final String name, final String shortName, final String doc,
+                                 final boolean optional, boolean collection, final int minElements,
+                                 final int maxElements, final Object defaultValue, final boolean isCommon,
+                                 final String[] mutuallyExclusive) {
+            this.field = field;
+            this.parent = parent;
+            this.name = name.toUpperCase();
+            this.shortName = shortName.toUpperCase();
+            this.doc = doc;
+            this.optional = optional;
+            isCollection = collection;
+            this.minElements = minElements;
+            this.maxElements = maxElements;
+            if (defaultValue != null) {
+                if (isCollection && ((Collection) defaultValue).isEmpty()) {
+                    //treat empty collections the same as uninitialized primitive types
+                    this.defaultValue = "null";
+                } else {
+                    //this is an intialized primitive type or a non-empty collection
+                    this.defaultValue = defaultValue.toString();
+                }
+            } else {
+                this.defaultValue = "null";
+            }
+            this.isCommon = isCommon;
+            this.mutuallyExclusive = new HashSet<String>(Arrays.asList(mutuallyExclusive));
+            if (this.field.getAnnotation(Hidden.class) != null) {
+                logger.warn("Hidden annotation is not honored for --{}", this.name);
+            }
+        }
+    }
+
+    /**
+     * The commandline used to run this program, including any default args that
+     * weren't necessarily specified. This is used for logging and debugging.
+     * <p/>
+     * NOTE: {@link #parseArguments(PrintStream, String[])} must be called before
+     * calling this method.
+     *
+     * @return The commandline, or null if {@link #parseArguments(PrintStream, String[])}
+     * hasn't yet been called, or didn't complete successfully.
+     */
+    @Override
+    public String getCommandLine() { return commandLine; }
+
+    /**
+     * This method is only needed when calling one of the public methods that doesn't take a messageStream argument.
+     */
+    public void setMessageStream(final PrintStream messageStream) {
+        this.messageStream = messageStream;
+    }
+
+    public Object getCallerOptions() {
+        return callerOptions;
+    }
+}
diff --git a/src/main/java/org/broadinstitute/barclay/argparser/PositionalArguments.java b/src/main/java/org/broadinstitute/barclay/argparser/PositionalArguments.java
new file mode 100644
index 0000000..f94be0a
--- /dev/null
+++ b/src/main/java/org/broadinstitute/barclay/argparser/PositionalArguments.java
@@ -0,0 +1,36 @@
+package org.broadinstitute.barclay.argparser;
+
+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;
+
+/**
+ * Used to annotate which field of a CommandLineProgram should store parameters given at the 
+ * command line which are not options. Fields with this annotation must be a Collection
+ * (and probably should be a List if order is important).
+ * If a command line call looks like "cmd option=foo x=y bar baz" the values "bar" and "baz"
+ * would be added to the collection with this annotation. The java type of the arguments
+ * will be inferred from the generic type of the collection. The type must be an enum or
+ * have a constructor with a single String parameter.
+ *
+ * @author Alec Wysoker
+ */
+ at Retention(RetentionPolicy.RUNTIME)
+ at Target(ElementType.FIELD)
+ at Documented
+public @interface PositionalArguments {
+    /** The minimum number of arguments required. */
+    int minElements() default 0;
+    
+    /** The maximum number of arguments allowed. */
+    int maxElements() default Integer.MAX_VALUE;
+
+    /**
+     * Documentation for the command-line argument.  Should appear when the
+     * --help argument is specified.
+     * @return Doc string associated with this command-line argument.
+     */
+    String doc() default "Undocumented option";
+}
diff --git a/src/main/java/org/broadinstitute/barclay/argparser/SpecialArgumentsCollection.java b/src/main/java/org/broadinstitute/barclay/argparser/SpecialArgumentsCollection.java
new file mode 100644
index 0000000..dfff5a7
--- /dev/null
+++ b/src/main/java/org/broadinstitute/barclay/argparser/SpecialArgumentsCollection.java
@@ -0,0 +1,30 @@
+package org.broadinstitute.barclay.argparser;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * This collection is for arguments that require special treatment by the arguments parser itself.
+ * It should not grow beyond a very short list.
+ */
+public final class SpecialArgumentsCollection {
+    public static final String HELP_FULLNAME = "help";
+    public static final String SHOW_HIDDEN_FULLNAME = "showHidden";
+    public static final String VERSION_FULLNAME = "version";
+    public static final String ARGUMENTS_FILE_FULLNAME = "arguments_file";
+    private static final long serialVersionUID = 1L;
+
+    @Argument(shortName = "h", fullName = HELP_FULLNAME, doc= "display the help message", special = true)
+    public boolean HELP = false;
+
+    @Argument(fullName = VERSION_FULLNAME, doc="display the version number for this tool", special = true)
+    public boolean VERSION = false;
+
+    @Argument(fullName = ARGUMENTS_FILE_FULLNAME, doc="read one or more arguments files and add them to the command line", optional = true, special = true)
+    public List<File> ARGUMENTS_FILE = new ArrayList<>();
+
+    @Advanced
+    @Argument(fullName = SHOW_HIDDEN_FULLNAME, shortName = SHOW_HIDDEN_FULLNAME, doc = "display hidden arguments", special = true)
+    public boolean SHOW_HIDDEN = false;
+}
diff --git a/src/main/java/org/broadinstitute/barclay/argparser/StrictBooleanConverter.java b/src/main/java/org/broadinstitute/barclay/argparser/StrictBooleanConverter.java
new file mode 100644
index 0000000..c091ae8
--- /dev/null
+++ b/src/main/java/org/broadinstitute/barclay/argparser/StrictBooleanConverter.java
@@ -0,0 +1,27 @@
+package org.broadinstitute.barclay.argparser;
+
+import joptsimple.ValueConversionException;
+import joptsimple.ValueConverter;
+
+/**
+ * converts values case insensitively matching T, True, F, or False to true or false
+ * throws {@link ValueConversionException} otherwise
+ */
+public final class StrictBooleanConverter implements ValueConverter<String> {
+    public String convert( String value ) {
+        if ( value.equalsIgnoreCase("true") || value.equalsIgnoreCase("t")) {
+            return "true";
+        } else if (value.equalsIgnoreCase("false") || value.equalsIgnoreCase("f")) {
+            return "false";
+        } else {
+            throw new ValueConversionException(value + " does not match one of T|True|F|False");
+        }
+    }
+    public final Class<? extends String> valueType() {
+        return String.class;
+    }
+
+    public String valuePattern() {
+        return "[T|True|F|False]";
+    }
+}
diff --git a/src/main/java/org/broadinstitute/barclay/argparser/TaggedArgument.java b/src/main/java/org/broadinstitute/barclay/argparser/TaggedArgument.java
new file mode 100644
index 0000000..75d2eec
--- /dev/null
+++ b/src/main/java/org/broadinstitute/barclay/argparser/TaggedArgument.java
@@ -0,0 +1,39 @@
+package org.broadinstitute.barclay.argparser;
+
+import java.util.Map;
+
+/**
+ * Interface for arguments that can have tags and attributes. The command line argument parser
+ * looks for argument fields that implement this interface, and propagates tag names and
+ * attribute/value pairs to the field.
+ *
+ * NOTE: No check is done to prevent duplicate tag names from being used more than once
+ * in an arugment that is a Collection.
+ */
+public interface TaggedArgument {
+
+    /**
+     * Set the tag name (optional - required only if attributes are present) for this instance.
+     * @param tagName May be null to indicate no tag name.
+     */
+    void setTag(String tagName);
+
+    /**
+     * Retrieve the tag name for this instance.
+     * @return String representing the tagName. May be null if no tag name was set.
+     */
+    String getTag();
+
+    /**
+     * Set the attribute/value pairs for this instance.
+     * @param attributes Map of attribute names and values for this arguments. May be empty, should not be null.
+     */
+    void setTagAttributes(Map<String, String> attributes);
+
+    /**
+     * Get the attribute/value pair Map for this instance. May be empty.
+     * @return Map of attribute/value pairs for this instance. May be empty if no tags are present. May not be null.
+     */
+    Map<String, String> getTagAttributes();
+
+}
diff --git a/src/main/java/org/broadinstitute/barclay/argparser/TaggedArgumentParser.java b/src/main/java/org/broadinstitute/barclay/argparser/TaggedArgumentParser.java
new file mode 100644
index 0000000..053e15d
--- /dev/null
+++ b/src/main/java/org/broadinstitute/barclay/argparser/TaggedArgumentParser.java
@@ -0,0 +1,329 @@
+package org.broadinstitute.barclay.argparser;
+
+import org.apache.commons.lang3.tuple.Pair;
+import org.broadinstitute.barclay.utils.Utils;
+
+import java.util.*;
+
+/**
+ * The parser used by the {@link CommandLineArgumentParser} for handling tagged argument strings. Tagged arguments
+ * can optionally contain a logical name, and optional attribute/value pairs. They are are of the form:
+ *
+ *     --argument_name:logical_name argument_value
+ * or:
+ *     --argument_name:logical_name,key1=value1,key2=value2 argument_value
+ *
+ * The logical name is optional, but is required if key/value pairs are includede. @Argument annotated fields that
+ * require tagged values must implement {@link TaggedArgument}.
+ *
+ * The parsing occurs in two phases:
+ *
+ * 1) Phase 1 occurs before the option parser is presented with the arguments. In phase 1, the option name is
+ * peeled off from the raw option string (which includes that tag string), and the resulting tag string and accompanying
+ * raw argument value are stored as a pair in a hash map for later retrieval via a key. The key is a string that is
+ * constructed by concatenating the original option name (excluding the actual short/long hyphen prefix), the
+ * tag string, and the raw argument value together to ensure the key is unique:
+ *
+ *      Key: --argument_name:logical_name,key1=value1,key2=value2:argument_value
+ *
+ * The pair object stored in the map contains only the (previously peeled off) tag string, and the raw argument value that
+ * was provided by the user. The command line parser replaces the original arguments provided by the user with the option
+ * name and the key:
+ *
+ *          User provides:              "--argument_name:logical_name,key1=value1,key2=value2" "raw_argument_value"
+ *          Parser is presented with:   "--argument_name" "argument_name:logical_name,key1=value1,key2=value2:argument_value"
+ *          Map stores:                 Pair("logical_name,key1=value1,key2=value2", "argument_value")
+ *
+ * 2) In phase 2, which occurs when the underlying argument field is being populated with a value, the key is used to
+ * retrieve the original tag string and argument value. The tag string is parsed (logical name and attributes) and used
+ * to populate the underlying argument field.
+ */
+public final class TaggedArgumentParser {
+
+    /**
+     * Delimiter between key-value pairs in the "logical_name,key1=value1,key2=value2" syntax.
+     */
+    private static final String ARGUMENT_KEY_VALUE_PAIR_DELIMITER = ",";
+
+    /**
+     * Separator between keys and values in the "logical_name,key1=value1,key2=value2" syntax.
+     */
+    private static final String ARGUMENT_KEY_VALUE_SEPARATOR = "=";
+
+    /**
+     * Separator used between option name and logical name.
+     */
+    private static final char ARGUMENT_TAG_NAME_SEPARATOR = ':';
+
+    private static final String USAGE = "Tagged arguments must be of the form argument_name or argument_name:logical_name(,key=value)*";
+
+    // Map of surrogate keys to Pair(tag_string, raw_argument_value)
+    private Map<String, Pair<String, String>> tagSurrogates = new HashMap<>();
+
+    // Prefixes used by the opt parser for short/long prefixes
+    private static final String SHORT_OPTION_PREFIX = "-";
+    private static final String LONG_OPTION_PREFIX = "--";
+
+    /**
+     * Given an array of raw arguments provided by the user, return an array of args where tagged arguments
+     * have been replaced with curated arguments containing a key to be used by the parser to retrieve the actual
+     * values.
+     * @param argArray raw arguments as provided by the user
+     * @return curated string of arguments to be presented to the opt parser
+     */
+    public String[] preprocessTaggedOptions(final String[] argArray) {
+        List<String> finalArgs = new ArrayList<>(argArray.length);
+
+        Iterator<String> argIt = Arrays.asList(argArray).iterator();
+        while (argIt.hasNext()) {
+            final String arg = argIt.next();
+            if (isShortOptionToken(arg)) {
+                replaceTaggedOption(SHORT_OPTION_PREFIX, arg.substring(SHORT_OPTION_PREFIX.length()), argIt, finalArgs);
+            } else if (isLongOptionToken(arg)) {
+                replaceTaggedOption(LONG_OPTION_PREFIX, arg.substring(LONG_OPTION_PREFIX.length()), argIt, finalArgs);
+            } else { // Positional arg, etc., just add it
+                finalArgs.add(arg);
+            }
+
+        }
+        return finalArgs.toArray(new String[finalArgs.size()]);
+    };
+
+    /**
+     * Process a single option and add it to the curated args list. If the option is tagged, add the
+     * curated key and value. Otherwise just add the raw option.
+     *
+     * @param optionPrefix the actual option prefix used for this option, either "-" or "--"
+     * @param optionString the option string including everything but the prefix
+     * @param userArgIt iterator of raw arguments provided by the user, used to retrieve the value corresponding
+     *                    to this option
+     * @param finalArgList the curated argument list
+     */
+    private void replaceTaggedOption(
+            final String optionPrefix,          // the option prefix used (short or long)
+            final String optionString,          // the option string, minus prefix, including any tags/attributes
+            final Iterator<String> userArgIt,   // the original, raw, un-curated input argument list
+            final List<String> finalArgList     // final, curated argument list
+    )
+    {
+        final int separatorIndex = optionString.indexOf(TaggedArgumentParser.ARGUMENT_TAG_NAME_SEPARATOR);
+        if (separatorIndex == -1) { // no tags, consume one argument and get out
+            finalArgList.add(optionPrefix + optionString);
+        } else {
+            if (userArgIt.hasNext()) {
+
+                final String optionName = optionString.substring(0, separatorIndex);
+                if (optionName.isEmpty()) {
+                    throw new CommandLineException("Zero length argument name found in tagged argument: " + optionString);
+                }
+                final String tagNameAndValues = optionString.substring(separatorIndex+1, optionString.length());
+                if (tagNameAndValues.isEmpty()) {
+                    throw new CommandLineException("Zero length tag name found in tagged argument: " + optionString);
+                }
+                final String argValue = userArgIt.next();
+                if (isLongOptionToken(argValue) || isShortOptionToken(argValue)) {
+                    // An argument value is required, and there isn't one to consume
+                    throw new CommandLineException("No value found for tagged argument: " + optionString);
+                }
+
+                // Replace the original prefix/option/attribute string with the original prefix/option name, and
+                // replace it's companion argument value with the surrogate key to be used later to retrieve the
+                // actual values
+                final String pairKey = getSurrogateKeyForTaggedOption(optionString, argValue, tagNameAndValues);
+                finalArgList.add(optionPrefix + optionName);
+                finalArgList.add(pairKey);
+            } else {
+                // the option appears to be tagged, but we're already at the end of the argument list,
+                // and there is no companion value to use
+                throw new CommandLineException("No value found for tagged argument: " + optionString);
+            }
+        }
+    }
+
+    /**
+     * Attempt to retrieve an option pair from the map using a surrogate key.
+     * @param putativeSurrogateKey putative key to try to retrieve from the surrogate map
+     * @return tagged option pair for this surrogate, or null if no entry
+     */
+    public Pair<String, String> getTaggedOptionForSurrogate(final String putativeSurrogateKey) {
+        return tagSurrogates.get(putativeSurrogateKey);
+    }
+
+    // See if the opt parser would think this is a short option ("-")
+    private static boolean isShortOptionToken(final String argument) {
+        return argument.startsWith( SHORT_OPTION_PREFIX )
+                && !SHORT_OPTION_PREFIX.equals( argument )
+                && !isLongOptionToken( argument );
+    }
+
+    // See if the opt parser would think this is a long option ("--")
+    private static boolean isLongOptionToken(final String argument) {
+        return argument.startsWith( LONG_OPTION_PREFIX );
+    }
+
+    // Stores the option string and value in the tagSurrogates hash map and returns a surrogate key.
+    private String getSurrogateKeyForTaggedOption(
+            final String rawOptionString,   // the raw option string provided by the user, including the prefix and option name
+            final String rawArgumentValue,  // the raw argument value provided by the user
+            final String tagString          // the tag string that has been peeled off of the raw option string, including logical name and any attributes
+    )
+    {
+        final String surrogateKey = makeSurrogateKey(rawOptionString, rawArgumentValue);
+        if (null != tagSurrogates.put(surrogateKey, Pair.of(tagString, rawArgumentValue))) {
+            throw new CommandLineException.BadArgumentValue(
+                    String.format("The argument value: \"%s %s\" was duplicated on the command line", rawOptionString, rawArgumentValue));
+        }
+        return surrogateKey;
+    }
+
+    /**
+     * Construct a surrogate key from the option name/tag string (as provided by the user) and raw argument value (as
+     * provided by the user). In order to ensure uniqueness of the key, it needs to include all elements from the
+     * command line, including:
+     *
+     *  -the option name (short/long) used
+     *  -the tag string used
+     *  -the argument value used
+     *
+     * @param rawOptionString the raw option string as provided by the user, without the option prefix
+     * @param rawArgumentValue the entire raw argument value provided by the user
+     * @return a surrogate key that can be included as a substitute value in the command line args presented to
+     * command line the parser
+     */
+    private String makeSurrogateKey(
+            final String rawOptionString,
+            final String rawArgumentValue
+    ) {
+        // The keys strings are never parsed, but the separator is added to the
+        // middle to make visual key inspection easier
+        return rawOptionString + ARGUMENT_TAG_NAME_SEPARATOR + rawArgumentValue;
+    }
+
+    /**
+     * Parse a tag string and populate a TaggedArgument with values.
+     *
+     * @param taggedArg TaggedArgument to receive tags
+     * @param longArgName name of the argument being tagged
+     * @param tagString tag string (including logical name and attributes, no option name)
+     */
+    public void populateArgumentTags(final TaggedArgument taggedArg, final String longArgName, final String tagString) {
+        if (tagString == null) {
+            taggedArg.setTag(null);
+            taggedArg.setTagAttributes(Collections.emptyMap());
+        } else {
+            final ParsedArgument pa = ParsedArgument.of(longArgName, tagString);
+            taggedArg.setTag(pa.getName());
+            taggedArg.setTagAttributes(pa.keyValueMap());
+        }
+    }
+
+    /**
+     * Given a TaggedArgument implementation and along argument name, return a string representation of argument,
+     * including the tag and attributes, for display purposes.
+     *
+     * @param taggedArg implementation of TaggedArgument interface
+     * @return a display string representing the tag name and attributes. May be empty if no tag name/attributes are present.
+     */
+    public static String getDisplayString(final String longArgName, final TaggedArgument taggedArg) {
+        Utils.nonNull(longArgName);
+        Utils.nonNull(taggedArg);
+
+        StringBuilder sb = new StringBuilder();
+        sb.append(longArgName);
+        if (taggedArg.getTag() != null) {
+            sb.append(ARGUMENT_TAG_NAME_SEPARATOR);
+            sb.append(taggedArg.getTag());
+
+            if (taggedArg.getTagAttributes() != null) {
+                taggedArg.getTagAttributes().entrySet().stream()
+                        .forEach(
+                                entry -> {
+                                    sb.append(ARGUMENT_KEY_VALUE_PAIR_DELIMITER);
+                                    sb.append(entry.getKey().toString());
+                                    sb.append(ARGUMENT_KEY_VALUE_SEPARATOR);
+                                    sb.append(entry.getValue().toString());
+                                });
+            }
+        }
+
+        return sb.toString();
+    }
+
+    /**
+     * Represents a parsed, tagged argument.
+     *
+     * May have attributes.
+     */
+    private static final class ParsedArgument{
+        private final String name;
+        private final Map<String, String> keyValueMap;
+
+        /**
+         * Parses an argument value String of the forms:
+         *
+         * "logical_name(,key=value)*"
+         *
+         * into logical name and key=value pairs.
+         *
+         * @param rawTagValue tag string value from the command line (does not include the argument name) to parse
+         * @return The argument parsed from the provided string.
+         */
+        public static ParsedArgument of(final String longArgName, final String rawTagValue) {
+            final String[] tokens = rawTagValue.split(ARGUMENT_KEY_VALUE_PAIR_DELIMITER, -1);
+
+            if (tokens.length == 0) {
+                throw new CommandLineException.BadArgumentValue(longArgName, rawTagValue, USAGE);
+            }
+            // first token is required to be a name
+            if (tokens[0].contains(ARGUMENT_KEY_VALUE_SEPARATOR)) {
+                throw new CommandLineException.BadArgumentValue("Missing tag name for argument: " + rawTagValue);
+            }
+            if ( Arrays.stream(tokens).anyMatch(String::isEmpty)) {
+                throw new CommandLineException.BadArgumentValue(longArgName, rawTagValue, "Empty tag or attribute encountered. " + USAGE);
+            }
+
+            final ParsedArgument pa = new ParsedArgument(tokens[0]);
+            if (tokens.length == 1) {
+                return pa;
+            } else {
+                // User specified a logical name (and optional list of key-value pairs)
+                for (int i = 1; i < tokens.length; i++){
+                    final String[] kv = tokens[i].split(ARGUMENT_KEY_VALUE_SEPARATOR, -1);
+                    if (kv.length != 2 || kv[0].isEmpty() || kv[1].isEmpty()){
+                        throw new CommandLineException.BadArgumentValue("", rawTagValue, USAGE);
+                    }
+                    if (pa.containsKey(kv[0])){
+                        throw new CommandLineException.BadArgumentValue("", rawTagValue, "Duplicate key " + kv[0] + "\n" + USAGE);
+                    }
+                    pa.addKeyValue(kv[0], kv[1]);
+                }
+                return pa;
+            }
+        }
+
+        private ParsedArgument(final String name) {
+            this.name=name;
+            this.keyValueMap = new LinkedHashMap<>(2);
+        }
+
+        public String getName() {
+            return name;
+        }
+
+        /**
+         * Returns an immutable view of the key-value map.
+         */
+        public Map<String, String> keyValueMap() {
+            return Collections.unmodifiableMap(keyValueMap);
+        }
+
+        public void addKeyValue(final String k, final String v) {
+            keyValueMap.put(k, v);
+        }
+
+        private boolean containsKey(final String k) {
+            return keyValueMap.containsKey(k);
+        }
+    }
+
+}
diff --git a/src/main/java/org/broadinstitute/barclay/help/BashTabCompletionDoclet.java b/src/main/java/org/broadinstitute/barclay/help/BashTabCompletionDoclet.java
new file mode 100644
index 0000000..c31e1fc
--- /dev/null
+++ b/src/main/java/org/broadinstitute/barclay/help/BashTabCompletionDoclet.java
@@ -0,0 +1,548 @@
+package org.broadinstitute.barclay.help;
+
+import com.sun.javadoc.RootDoc;
+import freemarker.template.Configuration;
+import freemarker.template.Template;
+import freemarker.template.TemplateException;
+
+import java.io.*;
+import java.util.*;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * For testing of Bash tab completion generation.
+ *
+ * Using this to generate tab-completion files requires that there is a
+ * wrapper script around the call to java that acts as a user-interface.
+ *
+ * This is required because of how Bash handles tab completion - it keys off
+ * of the first word typed in a line.  When invoking directly from java, Bash
+ * will complete for the `java` command, but will not know how to complete for a
+ * jar incorporating Barclay-enabled arguments.
+ *
+ * This is a known issue and is being investigated for remedies in the future.
+ */
+public class BashTabCompletionDoclet extends HelpDoclet {
+
+    // Barclay BashTabCompletionDoclet custom Command-line Arguments:
+
+    // All these arguments are optional, but it is highly recommended
+    // to specify the caller script name.
+
+    final private static String CALLER_SCRIPT_NAME = "-caller-script-name";
+
+    final private static String CALLER_SCRIPT_PREFIX_LEGAL_ARGS          = "-caller-pre-legal-args";
+    final private static String CALLER_SCRIPT_PREFIX_ARG_VALUE_TYPES     = "-caller-pre-arg-val-types";
+    final private static String CALLER_SCRIPT_PREFIX_MUTEX_ARGS          = "-caller-pre-mutex-args";
+    final private static String CALLER_SCRIPT_PREFIX_ALIAS_ARGS          = "-caller-pre-alias-args";
+    final private static String CALLER_SCRIPT_PREFIX_ARG_MIN_OCCURRENCES = "-caller-pre-arg-min-occurs";
+    final private static String CALLER_SCRIPT_PREFIX_ARG_MAX_OCCURRENCES = "-caller-pre-arg-max-occurs";
+
+    final private static String CALLER_SCRIPT_POSTFIX_LEGAL_ARGS          = "-caller-post-legal-args";
+    final private static String CALLER_SCRIPT_POSTFIX_ARG_VALUE_TYPES     = "-caller-post-arg-val-types";
+    final private static String CALLER_SCRIPT_POSTFIX_MUTEX_ARGS          = "-caller-post-mutex-args";
+    final private static String CALLER_SCRIPT_POSTFIX_ALIAS_ARGS          = "-caller-post-alias-args";
+    final private static String CALLER_SCRIPT_POSTFIX_ARG_MIN_OCCURRENCES = "-caller-post-arg-min-occurs";
+    final private static String CALLER_SCRIPT_POSTFIX_ARG_MAX_OCCURRENCES = "-caller-post-arg-max-occurs";
+
+    // =============================================
+
+    // Variables that are set on the command line when running this doclet:
+
+    /**
+     * Name of the executable / wrapper script that will actually invoke the java process.
+     * This wrapper script would call into the JAR and tell it which class to run.
+     */
+    private String callerScriptName = null;
+
+    /**
+     * Arguments to the executable / wrapper script that come before any Java class names / tools.
+     *
+     * This is expected to be a space-delimited string with the options themselves as they should be
+     * typed by the user.
+     *
+     * This syntax is used to pass this information to directly to the bash completion script.
+     *
+     * Example: {@code "--help --info --list --inputFile --outFolder --memSize --multiplier"}
+     */
+    private String callerScriptPrefixLegalArgs       = "";
+
+    /**
+     * Types of the arguments that the executable / wrapper script is expecting before any Java class names / tools.
+     * The order of these space-delimited types should correspond to the contents of {@link #callerScriptPrefixLegalArgs}
+     *
+     * This is expected to be a space-delimited string of types.
+     *
+     * Currently accepted type values are the following (not case-sensitive):
+     *
+     *      {@code file}
+     *      {@code folder}
+     *      {@code directory}
+     *      {@code int}
+     *      {@code long}
+     *      {@code double}
+     *      {@code float}
+     *      {@code null}   (to be used in the case of an argument that acts as a flag [i.e. one that takes no additional input])
+     *
+     * This syntax is used to pass this information to directly to the bash completion script.
+     *
+     * Example: {@code "null null null file directory int double"}
+     */
+    private String callerScriptPrefixArgValueTypes   = "";
+
+    /**
+     * Sets of arguments to the executable / wrapper script that are mutually exclusive to each other and
+     * are expected before any Java class names / tools.
+     *
+     * This is expected to be a string with mutex information for each argument that is mutually exclusive with another
+     * argument.  The format for this string is:
+     *
+     * {@code FOO;mutexToFoo1[,mutexToFoo2][,mutexToFoo3]... BAR;mutexToBar1[,mutexToBar2][,mutexToBar3]... }
+     *
+     *  where:
+     *
+     * {@code FOO} is an argument to the wrapper script which is expected before any Java class names / tools
+     * {@code mutexToFoo1} is an argument with which {@code FOO} is mutually exclusive without leading decorators (usually - or --)
+     * {@code mutexToFoo2} is an argument with which {@code FOO} is mutually exclusive without leading decorators (usually - or --)
+     * {@code mutexToFoo3} is an argument with which {@code FOO} is mutually exclusive without leading decorators (usually - or --)
+     *  and
+     * {@code BAR} is an argument to the wrapper script which is expected before any Java class names / tools
+     * {@code mutexToBar1} is an argument with which {@code BAR} is mutually exclusive without leading decorators (usually - or --)
+     * {@code mutexToBar2} is an argument with which {@code BAR} is mutually exclusive without leading decorators (usually - or --)
+     * {@code mutexToBar3} is an argument with which {@code BAR} is mutually exclusive without leading decorators (usually - or --)
+     *
+     * This can be thought of as a set of such argument relationships and does not have any ordering scheme.
+     *
+     * This syntax is used to pass this information to directly to the bash completion script.
+     *
+     * Example: {@code "--help;info,list,inputFile --info;help,list,inputFile"}
+     */
+    private String callerScriptPrefixMutexArgs       = "";
+
+    /**
+     * Sets of arguments to the executable / wrapper script that are aliases of each other and
+     * are expected before any Java class names / tools.
+     * For example, full argument names and short names for those arguments.
+     *
+     * This is expected to be a string with alias information for each argument that is an alias of another
+     * argument.  The format for this string is:
+     *
+     * {@code FOO;aliasToFoo1[,aliasToFoo2][,aliasToFoo3]... BAR;aliasToBar1[,aliasToBar2][,aliasToBar3]... }
+     *
+     *  where:
+     *
+     * {@code FOO} is an argument to the wrapper script which is expected before any Java class names / tools
+     * {@code aliasToFoo1} is an argument which is an alias to {@code FOO}
+     * {@code aliasToFoo2} is an argument which is an alias to {@code FOO}
+     * {@code aliasToFoo3} is an argument which is an alias to {@code FOO}
+     *  and
+     * {@code BAR} is an argument to the wrapper script which is expected before any Java class names / tools
+     * {@code aliasToBar1} is an argument which is an alias to {@code BAR}
+     * {@code aliasToBar2} is an argument which is an alias to {@code BAR}
+     * {@code aliasToBar3} is an argument which is an alias to {@code BAR}
+     *
+     * This can be thought of as a set of such argument relationships and does not have any ordering scheme.
+     *
+     * This syntax is used to pass this information to directly to the bash completion script.
+     *
+     * Example: {@code "--help;-h --info;-i --inputFile;-if,-infile,-inny"}
+     */
+    private String callerScriptPrefixAliasArgs       = "";
+
+    /**
+     * The minimum number of occurrences of each argument that the executable / wrapper script is expecting
+     * before any Java class names / tools.
+     * This is expected to be a space-delimited string with the min occurrences as {@code integer} values.
+     *
+     * The order of these space-delimited values should correspond to the contents of {@link #callerScriptPrefixLegalArgs}
+     *
+     * This is used in the logic that tracks the number of times an option is specified.
+     *
+     * This syntax is used to pass this information to directly to the bash completion script.
+     *
+     * Example: {@code "0 0 0 1 1 0 0 0"}
+     */
+    private String callerScriptPrefixMinOccurrences  = "";
+
+    /**
+     * The maximum number of occurrences of each argument that the executable / wrapper script is expecting
+     * before any Java class names / tools.
+     * This is expected to be a space-delimited string with the max occurrences as {@code integer} values.
+     *
+     * The order of these space-delimited values should correspond to the contents of {@link #callerScriptPrefixLegalArgs}
+     *
+     * This is used in the logic that tracks the number of times an option is specified.
+     *
+     * This syntax is used to pass this information to directly to the bash completion script.
+     *
+     * Example: {@code "1 1 1 1 1 1 1 1"}
+     */
+    private String callerScriptPrefixMaxOccurrences  = "";
+
+
+    /**
+     * Arguments to the executable / wrapper script that come after any Java class names / tools.  The start of these
+     * options is indicated by the user inputting the special option {@code --}
+     *
+     * The format of this variable is identical to {@link #callerScriptPrefixLegalArgs}
+     */
+    private String callerScriptPostfixLegalArgs      = "";
+
+    /**
+     * Types of the arguments that the executable / wrapper script is expecting after any Java class
+     * names / tools.  The start of these options is indicated by the user inputting the special option {@code --}
+     *
+     * The order of these space-delimited types should correspond to the contents of {@link #callerScriptPostfixLegalArgs}
+     *
+     * The format of this variable is identical to {@link #callerScriptPrefixArgValueTypes}
+     */
+    private String callerScriptPostfixArgValueTypes  = "";
+
+    /**
+     * Sets of arguments to the executable / wrapper script that are mutually exclusive to each other and
+     * are expected after any Java class names / tools.  The start of these options is indicated by the user
+     * inputting the special option {@code --}
+     *
+     * The format of this variable is identical to {@link #callerScriptPrefixMutexArgs}
+     */
+    private String callerScriptPostfixMutexArgs      = "";
+
+    /**
+     * Sets of arguments to the executable / wrapper script that are aliases of each other and
+     * are expected after any Java class names / tools.  The start of these options is indicated by the user
+     * inputting the special option {@code --}
+     *
+     * The format of this variable is identical to {@link #callerScriptPrefixAliasArgs}
+     */
+    private String callerScriptPostfixAliasArgs      = "";
+
+    /**
+     * The minimum number of occurrences of each argument that the executable / wrapper script is expecting
+     * after any Java class names / tools.  The start of these options is indicated by the user
+     * inputting the special option {@code --}
+     *
+     * The order of these space-delimited types should correspond to the contents of {@link #callerScriptPostfixLegalArgs}
+     *
+     * The format of this variable is identical to {@link #callerScriptPrefixMinOccurrences}
+     */
+    private String callerScriptPostfixMinOccurrences = "";
+
+    /**
+     * The maximum number of occurrences of each argument that the executable / wrapper script is expecting
+     * after any Java class names / tools.  The start of these options is indicated by the user
+     * inputting the special option {@code --}
+     *
+     * The order of these space-delimited types should correspond to the contents of {@link #callerScriptPostfixLegalArgs}
+     *
+     * The format of this variable is identical to {@link #callerScriptPrefixMaxOccurrences}
+     */
+    private String callerScriptPostfixMaxOccurrences = "";
+
+    /**
+     * True if the executable / wrapper script has arguments that are expected after any Java class names / tools.
+     * The start of these options is indicated by the user inputting the special option {@code --}
+     * The value of this is set internally based on the contents of {@link #callerScriptPostfixLegalArgs}
+     */
+    private boolean hasCallerScriptPostfixArgs       = false;
+
+    // =============================================
+
+    // Member variables:
+    protected static String outputFileExtension = "sh";
+    protected static String indexFileExtension = "sh";
+
+    // =============================================
+
+    public static boolean start(RootDoc rootDoc) {
+        try {
+            return new BashTabCompletionDoclet().startProcessDocs(rootDoc);
+        } catch (IOException e) {
+            throw new DocException("Exception processing javadoc", e);
+        }
+    }
+
+    private String quoteEachWord(final String sentence) {
+        return quoteEachWord(sentence, " ");
+    }
+    private String quoteEachWord(final String sentence, final String sep) {
+
+        return Stream.of(sentence.split(sep))
+                .map(s -> String.format("\"%s\"", s))
+                .collect(Collectors.joining(sep));
+    }
+
+    // Must add options that are applicable to this doclet so that they will be accepted.
+    public static int optionLength(final String option) {
+
+        if (option.equals(CALLER_SCRIPT_NAME) ||
+            option.equals(CALLER_SCRIPT_PREFIX_LEGAL_ARGS) ||
+            option.equals(CALLER_SCRIPT_PREFIX_ARG_VALUE_TYPES) ||
+            option.equals(CALLER_SCRIPT_PREFIX_MUTEX_ARGS) ||
+            option.equals(CALLER_SCRIPT_PREFIX_ALIAS_ARGS) ||
+            option.equals(CALLER_SCRIPT_PREFIX_ARG_MIN_OCCURRENCES) ||
+            option.equals(CALLER_SCRIPT_PREFIX_ARG_MAX_OCCURRENCES) ||
+            option.equals(CALLER_SCRIPT_POSTFIX_LEGAL_ARGS) ||
+            option.equals(CALLER_SCRIPT_POSTFIX_ARG_VALUE_TYPES) ||
+            option.equals(CALLER_SCRIPT_POSTFIX_MUTEX_ARGS) ||
+            option.equals(CALLER_SCRIPT_POSTFIX_ALIAS_ARGS) ||
+            option.equals(CALLER_SCRIPT_POSTFIX_ARG_MIN_OCCURRENCES) ||
+            option.equals(CALLER_SCRIPT_POSTFIX_ARG_MAX_OCCURRENCES) )
+        {
+            return 2;
+        }
+        else {
+            return HelpDoclet.optionLength(option);
+        }
+    }
+
+    @Override
+    protected void validateDocletStartingState() {
+        if ( callerScriptName == null ) {
+            // The user did not specify the caller script name.
+            // We cannot function under these conditions:
+            throw new RuntimeException("ERROR: You must specify a caller script name using the option: " + CALLER_SCRIPT_NAME);
+        }
+    }
+
+    @Override
+    protected boolean parseOption(final String[] option) {
+
+        // Do the stuff that HelpDoclet needs:
+        boolean hasParsedOption = super.parseOption(option);
+
+        if ( !hasParsedOption ) {
+
+            // Do the stuff that BashTabCompletionDoclet needs:
+            // (Yes this is inefficient because the parent class cycles through input args too.)
+            if (option[0].equals(CALLER_SCRIPT_NAME)) {
+
+                // Remove the last period and anything after it:
+                final int lastDotIndex = option[1].lastIndexOf('.');
+                if ( lastDotIndex != -1 ) {
+                    callerScriptName = option[1].substring(0, lastDotIndex);
+                }
+                else {
+                    callerScriptName = option[1];
+                }
+
+                hasParsedOption = true;
+            }
+
+            else if (option[0].equals(CALLER_SCRIPT_PREFIX_LEGAL_ARGS)) {
+                callerScriptPrefixLegalArgs = option[1];
+                hasParsedOption = true;
+            }
+            else if (option[0].equals(CALLER_SCRIPT_PREFIX_ARG_VALUE_TYPES)) {
+                // We have to format this option to contain quotes around each word:
+                callerScriptPrefixArgValueTypes = quoteEachWord(option[1]);
+                hasParsedOption = true;
+            }
+            else if (option[0].equals(CALLER_SCRIPT_PREFIX_MUTEX_ARGS)) {
+                // We have to format this option to contain quotes around each group of options:
+                callerScriptPrefixMutexArgs = quoteEachWord(option[1]);
+                hasParsedOption = true;
+            }
+            else if (option[0].equals(CALLER_SCRIPT_PREFIX_ALIAS_ARGS)) {
+                // We have to format this option to contain quotes around each group of options:
+                callerScriptPrefixAliasArgs = quoteEachWord(option[1]);
+                hasParsedOption = true;
+            }
+            else if (option[0].equals(CALLER_SCRIPT_PREFIX_ARG_MIN_OCCURRENCES)) {
+                callerScriptPrefixMinOccurrences = option[1];
+                hasParsedOption = true;
+            }
+            else if (option[0].equals(CALLER_SCRIPT_PREFIX_ARG_MAX_OCCURRENCES)) {
+                callerScriptPrefixMaxOccurrences = option[1];
+                hasParsedOption = true;
+            }
+
+            else if (option[0].equals(CALLER_SCRIPT_POSTFIX_LEGAL_ARGS)) {
+                callerScriptPostfixLegalArgs = option[1];
+                hasCallerScriptPostfixArgs = !callerScriptPostfixLegalArgs.isEmpty();
+                hasParsedOption = true;
+            }
+            else if (option[0].equals(CALLER_SCRIPT_POSTFIX_ARG_VALUE_TYPES)) {
+                // We have to format this option to contain quotes around each word:
+                callerScriptPostfixArgValueTypes = quoteEachWord(option[1]);
+                hasParsedOption = true;
+            }
+            else if (option[0].equals(CALLER_SCRIPT_POSTFIX_MUTEX_ARGS)) {
+                // We have to format this option to contain quotes around each word:
+                callerScriptPostfixMutexArgs = quoteEachWord(option[1]);
+                hasParsedOption = true;
+            }
+            else if (option[0].equals(CALLER_SCRIPT_POSTFIX_ALIAS_ARGS)) {
+                // We have to format this option to contain quotes around each word:
+                callerScriptPostfixAliasArgs = quoteEachWord(option[1]);
+                hasParsedOption = true;
+            }
+            else if (option[0].equals(CALLER_SCRIPT_POSTFIX_ARG_MIN_OCCURRENCES)) {
+                callerScriptPostfixMinOccurrences = option[1];
+                hasParsedOption = true;
+            }
+            else if (option[0].equals(CALLER_SCRIPT_POSTFIX_ARG_MAX_OCCURRENCES)) {
+                callerScriptPostfixMaxOccurrences = option[1];
+                hasParsedOption = true;
+            }
+        }
+
+        return hasParsedOption;
+    }
+
+    @Override
+    protected void processWorkUnitTemplate(
+            final Configuration cfg,
+            final DocWorkUnit workUnit,
+            final List<Map<String, String>> indexByGroupMaps,
+            final List<Map<String, String>> featureMaps) {
+
+            // For the Bash Test Doclet, this is a no-op.
+            // We only care about the index file.
+    }
+
+    /**
+     * The Index file in the Bash Completion Doclet is what generates the actual tab-completion script.
+     *
+     * This will actually write out the shell completion output file.
+     * The Freemarker instance will see a top-level map that has two keys in it.
+     *
+     * The first key is for caller script options:
+     *
+     * SimpleMap callerScriptOptions = SimpleMap {
+     *
+     *   "callerScriptName"                 : caller script name
+     *
+     *   "callerScriptPrefixLegalArgs"      : caller Script Prefix Legal Args
+     *   "callerScriptPrefixArgValueTypes"  : caller Script Prefix Arg Value Types
+     *   "callerScriptPrefixMutexArgs"      : caller Script Prefix Mutex Args
+     *   "callerScriptPrefixAliasArgs"      : caller Script Prefix Alias Args
+     *   "callerScriptPrefixMinOccurrences" : caller Script Prefix Min Occurrences
+     *   "callerScriptPrefixMaxOccurrences" : caller Script Prefix Max Occurrences
+     *   "hasCallerScriptPrefixArgs"        : has Caller Script Prefix Args
+     *
+     *   "callerScriptPostfixLegalArgs"      : caller Script Postfix Legal Args
+     *   "callerScriptPostfixArgValueTypes"  : caller Script Postfix Arg Value Types
+     *   "callerScriptPostfixMutexArgs"      : caller Script Postfix Mutex Args
+     *   "callerScriptPostfixAliasArgs"      : caller Script Postfix Alias Args
+     *   "callerScriptPostfixMinOccurrences" : caller Script Postfix Min Occurrences
+     *   "callerScriptPostfixMaxOccurrences" : caller Script Postfix Max Occurrences
+     *   "hasCallerScriptPostfixArgs"        : has Caller Script Postfix Args
+     *
+     * }
+     *
+     * The second key is for tool options:
+     *
+     * SimpleMap tools = SimpleMap { ToolName : MasterPropertiesMap }
+     *
+     *     where
+     *
+     *     MasterPropertiesMap is a map containing the following Keys:
+     *         all
+     *         common
+     *         positional
+     *         hidden
+     *         advanced
+     *         deprecated
+     *         optional
+     *         dependent
+     *         required
+     *
+     *         Each of those keys maps to a List<SimpleMap> representing each property.
+     *         These property maps each contain the following keys:
+     *
+     *             kind
+     *             name
+     *             summary
+     *             fulltext
+     *             otherArgumentRequired
+     *             synonyms
+     *             exclusiveOf
+     *             type
+     *             options
+     *             attributes
+     *             required
+     *             minRecValue
+     *             maxRecValue
+     *             minValue
+     *             maxValue
+     *             defaultValue
+     *             minElements
+     *             maxElements
+     *
+     * @param cfg
+     * @param workUnitList
+     * @param groupMaps
+     * @throws IOException
+     */
+    @Override
+    protected void processIndexTemplate(
+            final Configuration cfg,
+            final List<DocWorkUnit> workUnitList,
+            final List<Map<String, String>> groupMaps
+    ) throws IOException {
+        // Create a root map for all the work units so we can access all the info we need:
+        final Map<String, Object> propertiesMap = new HashMap<>();
+        workUnits.stream().forEach( workUnit -> propertiesMap.put(workUnit.getName(), workUnit.getRootMap()) );
+
+        // Add everything into a nice package that we can iterate over
+        // while exposing the command line program names as keys:
+        final Map<String, Object> rootMap = new HashMap<>();
+        rootMap.put("tools", propertiesMap);
+
+        // Add the caller script options into another top-level tree node:
+        final Map<String, Object> callerScriptOptionsMap = new HashMap<>();
+        callerScriptOptionsMap.put("callerScriptName", callerScriptName);
+
+        callerScriptOptionsMap.put("callerScriptPrefixLegalArgs", callerScriptPrefixLegalArgs);
+        callerScriptOptionsMap.put("callerScriptPrefixArgValueTypes", callerScriptPrefixArgValueTypes);
+        callerScriptOptionsMap.put("callerScriptPrefixMutexArgs", callerScriptPrefixMutexArgs);
+        callerScriptOptionsMap.put("callerScriptPrefixAliasArgs", callerScriptPrefixAliasArgs);
+        callerScriptOptionsMap.put("callerScriptPrefixMinOccurrences", callerScriptPrefixMinOccurrences);
+        callerScriptOptionsMap.put("callerScriptPrefixMaxOccurrences", callerScriptPrefixMaxOccurrences);
+
+        callerScriptOptionsMap.put("callerScriptPostfixLegalArgs", callerScriptPostfixLegalArgs);
+        callerScriptOptionsMap.put("callerScriptPostfixArgValueTypes", callerScriptPostfixArgValueTypes);
+        callerScriptOptionsMap.put("callerScriptPostfixMutexArgs", callerScriptPostfixMutexArgs);
+        callerScriptOptionsMap.put("callerScriptPostfixAliasArgs", callerScriptPostfixAliasArgs);
+        callerScriptOptionsMap.put("callerScriptPostfixMinOccurrences", callerScriptPostfixMinOccurrences);
+        callerScriptOptionsMap.put("callerScriptPostfixMaxOccurrences", callerScriptPostfixMaxOccurrences);
+        if ( hasCallerScriptPostfixArgs ) {
+            callerScriptOptionsMap.put("hasCallerScriptPostfixArgs", "true");
+        }
+        else {
+            callerScriptOptionsMap.put("hasCallerScriptPostfixArgs", "false");
+        }
+
+        rootMap.put("callerScriptOptions", callerScriptOptionsMap);
+
+        // Get or create a template
+        final Template template = cfg.getTemplate(getIndexTemplateName());
+
+        // Create the output file
+        final File indexFile = new File(getDestinationDir(),
+                getIndexBaseFileName() + "." + getIndexFileExtension()
+        );
+
+        // Run the template and merge in the data
+        try (final FileOutputStream fileOutStream = new FileOutputStream(indexFile);
+             final OutputStreamWriter outWriter = new OutputStreamWriter(fileOutStream)) {
+            template.process(rootMap, outWriter);
+        } catch (TemplateException e) {
+            throw new DocException("Freemarker Template Exception during documentation index creation", e);
+        }
+    }
+
+    /**
+     * @return the name of the index template to be used for this doclet
+     */
+    @Override
+    public String getIndexTemplateName() { return "bash-completion.ftl"; }
+
+    /**
+     * @return The base filename for the index file associated with this doclet.
+     */
+    @Override
+    public String getIndexBaseFileName() { return callerScriptName + "-completion"; }
+
+}
diff --git a/src/main/java/org/broadinstitute/barclay/help/DefaultDocWorkUnitHandler.java b/src/main/java/org/broadinstitute/barclay/help/DefaultDocWorkUnitHandler.java
new file mode 100644
index 0000000..09d63bd
--- /dev/null
+++ b/src/main/java/org/broadinstitute/barclay/help/DefaultDocWorkUnitHandler.java
@@ -0,0 +1,812 @@
+package org.broadinstitute.barclay.help;
+
+import com.sun.javadoc.ClassDoc;
+import com.sun.javadoc.FieldDoc;
+
+import org.apache.commons.lang3.tuple.Pair;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.broadinstitute.barclay.argparser.*;
+
+import java.lang.reflect.*;
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * Default implementation of DocWorkUnitHandler. The DocWorkUnitHandler determines the template that will be
+ * used for a given work unit, and populates the Freemarker property map used for a single feature/work-unit.
+ * *
+ * Most consumers will subclass this to provide at least provide a custom FreeMarker Template, and possibly
+ * other custom behavior.
+ */
+public class DefaultDocWorkUnitHandler extends DocWorkUnitHandler {
+    final protected static Logger logger = LogManager.getLogger(DefaultDocWorkUnitHandler.class);
+
+    private static final String NAME_FOR_POSITIONAL_ARGS = "[NA - Positional]";
+    private static final String DEFAULT_FREEMARKER_TEMPLATE_NAME = "generic.html.ftl";
+
+    /**
+     * @param doclet for this documentation run. May not be null.
+     */
+    public DefaultDocWorkUnitHandler(final HelpDoclet doclet) {
+        super(doclet);
+    }
+
+    /**
+     * Return the template to be used for the particular workUnit. Must be present in the location
+     * specified is the -settings-dir doclet parameter.
+     *
+     * @param workUnit workUnit for which a template ie being requested
+     * @return name of the template file to use, relative to -settings-dir
+     */
+    @Override
+    public String getTemplateName(final DocWorkUnit workUnit) {
+        return DEFAULT_FREEMARKER_TEMPLATE_NAME;
+    }
+
+    /**
+     * Get the summary string to be used for a given work unit, applying any fallback policy. This is
+     * called by the work unit handler after the work unit has been populated, and may be overridden by
+     * subclasses to provide custom behavior.
+     *
+     * @param workUnit
+     * @return the summary string to be used for this work unit
+     */
+    @Override
+    public String getSummaryForWorkUnit(final DocWorkUnit workUnit) {
+        String summary = workUnit.getDocumentedFeature().summary();
+        if (summary == null || summary.isEmpty()) {
+            final CommandLineProgramProperties commandLineProperties = workUnit.getCommandLineProperties();
+            if (commandLineProperties != null) {
+                summary = commandLineProperties.oneLineSummary();
+            }
+            if (summary == null || summary.isEmpty()) {
+                // If no summary was found from annotations, use the javadoc if there is any
+                summary = Arrays.stream(workUnit.getClassDoc().firstSentenceTags())
+                        .map(tag -> tag.text())
+                        .collect(Collectors.joining());
+            }
+        }
+
+        return summary == null ? "" : summary;
+    }
+
+    /**
+     * Get the group name string to be used for a given work unit, applying any fallback policy. This is
+     * called by the work unit handler after the work unit has been populated, and may be overridden by
+     * subclasses to provide custom behavior.
+     *
+     * @param workUnit
+     * @return the group name to be used for this work unit
+     */
+    @Override
+    public String getGroupNameForWorkUnit(final DocWorkUnit workUnit) {
+        String groupName = workUnit.getDocumentedFeature().groupName();
+        if (groupName == null || groupName.isEmpty()) {
+            final CommandLineProgramGroup clpGroup = workUnit.getCommandLineProgramGroup();
+            if (clpGroup != null) {
+                groupName = clpGroup.getName();
+            }
+            if (groupName == null || groupName.isEmpty()) {
+                logger.warn("No group name declared for: " + workUnit.getClazz().getCanonicalName());
+                groupName = "";
+            }
+        }
+        return groupName;
+    }
+
+    /**
+     * Get the group summary string to be used for a given work unit's group, applying any fallback policy.
+     * This is called by the work unit handler after the work unit has been populated, and may be overridden by
+     * subclasses to provide custom behavior.
+     *
+     * @param workUnit
+     * @return the group summary to be used for this work unit's group
+     */
+    @Override
+    public String getGroupSummaryForWorkUnit( final DocWorkUnit workUnit){
+        String groupSummary = workUnit.getDocumentedFeature().groupSummary();
+        final CommandLineProgramGroup clpGroup = workUnit.getCommandLineProgramGroup();
+        if (groupSummary == null || groupSummary.isEmpty()) {
+            if (clpGroup != null) {
+                groupSummary = clpGroup.getDescription();
+            }
+            if (groupSummary == null || groupSummary.isEmpty()) {
+                logger.warn("No group summary declared for: " + workUnit.getClazz().getCanonicalName());
+                groupSummary = "";
+            }
+        }
+        return groupSummary;
+    }
+
+    /**
+     * Return the description to be used for the work unit. We need to manually strip
+     * out any inline custom javadoc tags since we don't those in the summary.
+     *
+     * @param currentWorkUnit
+     * @return Description to be used or the work unit.
+     */
+    protected String getDescription(final DocWorkUnit currentWorkUnit) {
+        return Arrays.stream(currentWorkUnit.getClassDoc().inlineTags())
+                .filter(t -> getTagPrefix() == null || !t.name().startsWith(getTagPrefix()))
+                .map(t -> t.text())
+                .collect(Collectors.joining());
+    }
+
+    /**
+     * Create the Freemarker Template Map, with the following top-level structure:
+     *
+     * The overall default key/value structure of featureMaps looks like this:
+     *
+     * name -> String
+     * group -> String
+     * version -> String
+     * timestamp -> String
+     * summary -> String
+     * description -> String
+     * extraDocs -> List
+     * plugin1 ->
+     *      name -> String
+     *      filename -> String (link)
+     *   .
+     *   .
+     * pluginN -> ...
+     * arguments -> List
+     * gson-arguments -> List
+     * groups -> list of maps, one per group
+     *      name -> ..
+     *      id -> ..
+     *      summary ->..
+     * data -> list of maps, one per documented feature
+     *      name ->..
+     *      summary -> ..
+     *      group -> ..
+     *      filename -> ..
+     *
+     *
+     * Key/value structure of groupMaps:
+     *      name -> ..
+     *      id -> ..
+     *      summary ->..
+     *
+     * @param workUnit work unit to process
+     * @param featureMaps list of feature maps, one per documented feature, as defined above
+     * @param groupMaps list of group maps, one per group, as defined above
+     */
+    @Override
+    public void processWorkUnit(
+            final DocWorkUnit workUnit,
+            final List<Map<String, String>> featureMaps,
+            final List<Map<String, String>> groupMaps) {
+
+        CommandLineArgumentParser clp = null;
+        List<? extends CommandLinePluginDescriptor<?>> pluginDescriptors = new ArrayList<>();
+
+        // Not all DocumentedFeature targets are CommandLinePrograms, and thus not all can be instantiated via
+        // a no-arg constructor. But we do want to generate a doc page for them. Any arguments associated with
+        // such a feature will show up in the doc page for any referencing CommandLinePrograms, instead of in
+        // the standalone feature page.
+        //
+        // Ex: We may want to document an input or output file format by adding @DocumentedFeature
+        // to the format's reader/writer class (i.e. TableReader), and then reference that feature
+        // in the extraDocs attribute in a CommandLineProgram that reads/writes that format.
+        if (workUnit.getCommandLineProperties() != null) {
+            try {
+                final Object argumentContainer = workUnit.getClazz().newInstance();
+                if (argumentContainer instanceof CommandLinePluginProvider ) {
+                    pluginDescriptors = ((CommandLinePluginProvider) argumentContainer).getPluginDescriptors();
+                    clp = new CommandLineArgumentParser(
+                            argumentContainer, pluginDescriptors, Collections.emptySet()
+                    );
+                } else {
+                    clp = new CommandLineArgumentParser(argumentContainer);
+                }
+            } catch (IllegalAccessException | InstantiationException e) {
+                throw new RuntimeException(e);
+            }
+        }
+
+        workUnit.setProperty("groups", groupMaps);
+        workUnit.setProperty("data", featureMaps);
+
+        addHighLevelBindings(workUnit);
+        addCommandLineArgumentBindings(workUnit, clp);
+        addDefaultPlugins(workUnit, pluginDescriptors);
+        addExtraDocsBindings(workUnit);
+        addCustomBindings(workUnit);
+    }
+
+    /**
+     * Add high-level summary information, such as name, summary, description, version, etc.
+     *
+     * @param workUnit
+     */
+    protected void addHighLevelBindings(final DocWorkUnit workUnit)
+    {
+        workUnit.setProperty("name", workUnit.getName());
+        workUnit.setProperty("group", workUnit.getGroupName());
+        workUnit.setProperty("summary", workUnit.getSummary());
+        workUnit.setProperty("beta", workUnit.getBetaFeature());
+
+        workUnit.setProperty("description", getDescription(workUnit));
+
+        workUnit.setProperty("version", getDoclet().getBuildVersion());
+        workUnit.setProperty("timestamp", getDoclet().getBuildTimeStamp());
+    }
+
+    /**
+     * Add any custom freemarker bindings discovered via custom javadoc tags. Subclasses can override this to
+     * provide additional custom bindings.
+     *
+     * @param currentWorkUnit the work unit for the feature being documented
+     */
+    protected void addCustomBindings(final DocWorkUnit currentWorkUnit) {
+        final String tagFilterPrefix = getTagPrefix();
+        Arrays.stream(currentWorkUnit.getClassDoc().inlineTags())
+                .filter(t -> t.name().startsWith(tagFilterPrefix))
+                .forEach(t -> currentWorkUnit.setProperty(t.name().substring(tagFilterPrefix.length()), t.text()));
+    }
+
+    /**
+     * Subclasses override this to have javadoc tags with this prefix placed in the freemarker map.
+     * @return string refix used for custom javadoc tags
+     */
+    protected String getTagFilterPrefix(){ return ""; }
+
+    /**
+     * Add bindings describing related capabilities to currentWorkUnit
+     */
+    protected void addExtraDocsBindings(final DocWorkUnit currentWorkUnit)
+    {
+        final List<Map<String, Object>> extraDocsData = new ArrayList<Map<String, Object>>();
+
+        // add in all of the explicitly related extradocs items
+        for (final Class<?> extraDocClass : currentWorkUnit.getDocumentedFeature().extraDocs()) {
+            final DocWorkUnit otherUnit = getDoclet().findWorkUnitForClass(extraDocClass);
+            if (otherUnit != null) {
+                extraDocsData.add(
+                        new HashMap<String, Object>() {
+                            static final long serialVersionUID = 0L;
+                            {
+                                put("name", otherUnit.getName());
+                                put("filename", otherUnit.getTargetFileName());
+                            }
+                        });
+            } else {
+                final String msg = String.format(
+                        "An \"extradocs\" value (%s) was specified for (%s), but the target was not included in the " +
+                        "source list for this javadoc run, or the target has no documentation.",
+                        extraDocClass,
+                        currentWorkUnit.getName()
+                );
+                throw new DocException(msg);
+            }
+        }
+        currentWorkUnit.setProperty("extradocs", extraDocsData);
+    }
+
+    @SuppressWarnings("unchecked")
+    /**
+     * Add information about all of the arguments available to toProcess root
+     */
+    protected void addCommandLineArgumentBindings(final DocWorkUnit currentWorkUnit, final CommandLineArgumentParser clp)
+    {
+        final Map<String, List<Map<String, Object>>> argMap = createArgumentMap();
+        currentWorkUnit.setProperty("arguments", argMap);
+
+        if (clp != null) {
+            // do positional arguments, followed by named arguments
+            processPositionalArguments(clp, argMap);
+            clp.getArgumentDefinitions().stream().forEach(argDef -> processNamedArgument(currentWorkUnit, argMap, argDef));
+
+            // sort the resulting args
+            argMap.entrySet().stream().forEach( entry -> entry.setValue(sortArguments(entry.getValue())));
+
+            // Write out the GSON version
+            // make a GSON-friendly map of arguments -- uses some hacky casting
+            final List<GSONArgument> allGSONArgs = new ArrayList<>();
+            for (final Map<String, Object> item : argMap.get("all")) {
+                GSONArgument itemGSONArg = new GSONArgument();
+
+                itemGSONArg.populate(item.get("summary").toString(),
+                        item.get("name").toString(),
+                        item.get("synonyms").toString(),
+                        item.get("type").toString(),
+                        item.get("required").toString(),
+                        item.get("fulltext").toString(),
+                        item.get("defaultValue").toString(),
+                        item.get("minValue").toString(),
+                        item.get("maxValue").toString(),
+                        item.get("minRecValue").toString(),
+                        item.get("maxRecValue").toString(),
+                        item.get("kind").toString(),
+                        (List<Map<String, Object>>)item.get("options")
+                );
+                allGSONArgs.add(itemGSONArg);
+            }
+            currentWorkUnit.setProperty("gson-arguments", allGSONArgs);
+        }
+    }
+
+    private String getTagPrefix() {
+        String customPrefix = getTagFilterPrefix();
+        return customPrefix == null ?
+                customPrefix : "@" + customPrefix + ".";
+
+    }
+
+    // Add top level bindings for the default instances for each plugin descriptor
+    // NOTE: The default Freemarker template provided by Barclay has no references to plugins, since they're
+    // defined by the consumer. However, if a custom freemarker template is being used that DOES contain references
+    // to plugins, the corresponding custom doclet class or documentation handler should ensure that the root
+    // map is populated with proper values. Otherwise, when the template is run on any documentable instance
+    // that happens to not have any plugin instances present, freemarker will throw when it finds the undefined
+    // reference.
+    protected void addDefaultPlugins(
+            final DocWorkUnit currentWorkUnit,
+            final List<? extends CommandLinePluginDescriptor<?>> pluginDescriptors)
+    {
+        for (final CommandLinePluginDescriptor<?> descriptor :  pluginDescriptors) {
+            final String descriptorName = descriptor.getDisplayName();  // key/name at the root of the freemarker map
+            final HashSet<HashMap<String, Object>> defaultsForPlugins = new HashSet<>();
+            currentWorkUnit.setProperty(descriptorName, defaultsForPlugins);
+
+            for (final Object plugin : descriptor.getDefaultInstances()) {
+                final HashMap<String, Object> pluginDetails = new HashMap<>();
+                pluginDetails.put("name", plugin.getClass().getSimpleName());
+                pluginDetails.put("filename", DocletUtils.phpFilenameForClass(plugin.getClass(), getDoclet().getOutputFileExtension()));
+                defaultsForPlugins.add(pluginDetails);
+            }
+        }
+    }
+
+    private void processNamedArgument(
+            final DocWorkUnit currentWorkUnit,
+            final Map<String, List<Map<String, Object>>> args,
+            final CommandLineArgumentParser.ArgumentDefinition argDef)
+    {
+        if (!argDef.isControlledByPlugin() &&
+                (argDef.field.getAnnotation(Hidden.class) == null || getDoclet().showHiddenFeatures())) {
+            // first find the fielddoc for the target
+            FieldDoc fieldDoc = getFieldDocForCommandLineArgument(currentWorkUnit, argDef);
+            final Map<String, Object> argBindings = docForArgument(fieldDoc, argDef);
+            final String kind = docKindOfArg(argDef);
+            argBindings.put("kind", kind);
+
+            // Retrieve default value
+            final Object fieldValue = argDef.getFieldValue();
+            argBindings.put("defaultValue",
+                    fieldValue == null ?
+                            argDef.defaultValue :
+                            prettyPrintValueString(fieldValue));
+
+            if (fieldValue instanceof Number) {
+                // Retrieve min and max / hard and soft value thresholds for numeric args
+                Argument argAnnotation = argDef.field.getAnnotation(Argument.class);
+                argBindings.put("minValue", argAnnotation.minValue());
+                argBindings.put("maxValue", argAnnotation.maxValue());
+                argBindings.put("minRecValue",
+                        argAnnotation.minRecommendedValue() != Double.NEGATIVE_INFINITY ?
+                                argAnnotation.minRecommendedValue() :
+                                "NA");
+                argBindings.put("maxRecValue",
+                        argAnnotation.maxRecommendedValue() != Double.POSITIVE_INFINITY ?
+                                argAnnotation.maxRecommendedValue() :
+                                "NA");
+            } else {
+                argBindings.put("minValue", "NA");
+                argBindings.put("maxValue", "NA");
+                argBindings.put("minRecValue", "NA");
+                argBindings.put("maxRecValue", "NA");
+            }
+
+            // Add in the number of times you can specify it:
+            argBindings.put("minElements", argDef.field.getAnnotation(Argument.class).minElements());
+            argBindings.put("maxElements", argDef.field.getAnnotation(Argument.class).maxElements());
+
+            // if its a plugin descriptor arg, get the allowed values
+            processPluginDescriptorArgument(argDef, argBindings);
+
+            // Finalize argument bindings
+            args.get(kind).add(argBindings);
+            args.get("all").add(argBindings);
+        }
+    }
+
+    private FieldDoc getFieldDocForCommandLineArgument(
+            final DocWorkUnit currentWorkUnit,
+            final CommandLineArgumentParser.ArgumentDefinition argDef) {
+        FieldDoc fieldDoc = getFieldDoc(currentWorkUnit.getClassDoc(), argDef.field.getName());
+        if (fieldDoc == null) {
+            for (final ClassDoc classDoc : getDoclet().getRootDoc().classes()) {
+                fieldDoc = getFieldDoc(classDoc, argDef.field.getName());
+                if (fieldDoc != null) {
+                    break;
+                }
+            }
+        }
+        if (fieldDoc == null) {
+            throw new DocException(
+                 String.format(
+                         "The class \"%s\" is referenced by \"%s\", and must be included in the list of target documentation classes.",
+                         argDef.field.getDeclaringClass().getCanonicalName(),
+                         currentWorkUnit.getClassDoc().qualifiedTypeName())
+            );
+        }
+        return fieldDoc;
+    }
+
+    private void processPositionalArguments(
+            final CommandLineArgumentParser clp,
+            final Map<String, List<Map<String, Object>>> args) {
+        // first get the positional arguments
+        final Field positionalField = clp.getPositionalArguments();
+        if (positionalField != null) {
+            final Map<String, Object> argBindings = new HashMap<>();
+            PositionalArguments posArgs = positionalField.getAnnotation(PositionalArguments.class);
+            argBindings.put("kind", "positional");
+            argBindings.put("name", NAME_FOR_POSITIONAL_ARGS);
+            argBindings.put("summary", posArgs.doc());
+            argBindings.put("fulltext", posArgs.doc());
+            argBindings.put("otherArgumentRequired", "NA");
+            argBindings.put("synonyms", "NA");
+            argBindings.put("exclusiveOf", "NA");
+            argBindings.put("type", argumentTypeString(positionalField.getGenericType()));
+            argBindings.put("options", Collections.EMPTY_LIST);
+            argBindings.put("attributes", "NA");
+            argBindings.put("required", "yes");
+            argBindings.put("minRecValue", "NA");
+            argBindings.put("maxRecValue", "NA");
+            argBindings.put("minValue", "NA");
+            argBindings.put("maxValue", "NA");
+            argBindings.put("defaultValue", "NA");
+            argBindings.put("minElements", posArgs.minElements());
+            argBindings.put("maxElements", posArgs.maxElements());
+
+            args.get("positional").add(argBindings);
+            args.get("all").add(argBindings);
+        }
+    }
+
+    protected void processPluginDescriptorArgument(
+            final CommandLineArgumentParser.ArgumentDefinition argDef,
+            final Map<String, Object> argBindings) {
+        if (CommandLinePluginDescriptor.class.isAssignableFrom(argDef.parent.getClass()) &&
+                CommandLineParser.getUnderlyingType(argDef.field).equals(String.class)) {
+            final CommandLinePluginDescriptor<?> descriptor = (CommandLinePluginDescriptor<?>) argDef.parent;
+            //TODO: need a way to emit a link to the index group for the plugin
+        }
+    }
+
+    /**
+     * Return the argument kind (required, advanced, hidden, etc) of this argumentDefinition
+     *
+     * @param argumentDefinition
+     * @return
+     */
+    private String docKindOfArg(final CommandLineArgumentParser.ArgumentDefinition argumentDefinition) {
+
+        // positional
+        // required (common or otherwise)
+        // common optional
+        // advanced
+        // hidden
+        // deprecated
+
+        // Required first (after positional, which are separate), regardless of what else it might be
+        if (argumentDefinition.isControlledByPlugin()) {
+            return "dependent";
+        }
+        if (!argumentDefinition.optional) {
+            return "required";
+        }
+        if (argumentDefinition.isCommon) {  // these will all be optional
+            return "common";
+        }
+        if (argumentDefinition.field.isAnnotationPresent(Advanced.class)) {
+            return "advanced";
+        }
+        if (argumentDefinition.field.isAnnotationPresent(Hidden.class)) {
+            return "hidden";
+        }
+        if (argumentDefinition.field.isAnnotationPresent(Deprecated.class)) {
+            return "deprecated";
+        }
+        return "optional";
+    }
+
+    /**
+     * Create the argument map for holding class arguments
+     *
+     * @return
+     */
+    private Map<String, List<Map<String, Object>>> createArgumentMap() {
+        final Map<String, List<Map<String, Object>>> args = new HashMap<String, List<Map<String, Object>>>();
+        args.put("all", new ArrayList<>());
+        args.put("common", new ArrayList<>());
+        args.put("positional", new ArrayList<>());
+        args.put("required", new ArrayList<>());
+        args.put("optional", new ArrayList<>());
+        args.put("advanced", new ArrayList<>());
+        args.put("dependent", new ArrayList<>());
+        args.put("hidden", new ArrayList<>());
+        args.put("deprecated", new ArrayList<>());
+        return args;
+    }
+
+    /**
+     * Sorts the individual argument list in unsorted according to CompareArgumentsByName
+     *
+     * @param unsorted
+     * @return
+     */
+    private List<Map<String, Object>> sortArguments(final List<Map<String, Object>> unsorted) {
+        Collections.sort(unsorted, new CompareArgumentsByName());
+        return unsorted;
+    }
+
+    /**
+     * Sort arguments by case-insensitive comparison ignoring the -- and - prefixes
+     */
+    private class CompareArgumentsByName implements Comparator<Map<String, Object>> {
+        public int compare(Map<String, Object> x, Map<String, Object> y) {
+            return elt(x).compareTo(elt(y));
+        }
+
+        private String elt(Map<String, Object> m) {
+            final String v = m.get("name").toString().toLowerCase();
+            if (v.startsWith("--"))
+                return v.substring(2);
+            else if (v.startsWith("-"))
+                return v.substring(1);
+            else if (v.equals(NAME_FOR_POSITIONAL_ARGS.toLowerCase()))
+                return "Positional";
+            else
+                throw new RuntimeException("Expect to see arguments beginning with at least one -, but found " + v);
+        }
+    }
+
+    /**
+     * Pretty prints value
+     *
+     * Assumes value != null
+     *
+     * @param value
+     * @return
+     */
+    private Object prettyPrintValueString(final Object value) {
+        if (value.getClass().isArray()) {
+            final Class<?> type = value.getClass().getComponentType();
+            if (boolean.class.isAssignableFrom(type))
+                return Arrays.toString((boolean[]) value);
+            if (byte.class.isAssignableFrom(type))
+                return Arrays.toString((byte[]) value);
+            if (char.class.isAssignableFrom(type))
+                return Arrays.toString((char[]) value);
+            if (double.class.isAssignableFrom(type))
+                return Arrays.toString((double[]) value);
+            if (float.class.isAssignableFrom(type))
+                return Arrays.toString((float[]) value);
+            if (int.class.isAssignableFrom(type))
+                return Arrays.toString((int[]) value);
+            if (long.class.isAssignableFrom(type))
+                return Arrays.toString((long[]) value);
+            if (short.class.isAssignableFrom(type))
+                return Arrays.toString((short[]) value);
+            if (Object.class.isAssignableFrom(type))
+                return Arrays.toString((Object[]) value);
+            else
+                throw new RuntimeException("Unexpected array type in prettyPrintValue.  Value was " + value + " type is " + type);
+        } else if (value instanceof String) {
+            return value.equals("") ? "\"\"" : value;
+        } else {
+            return value.toString();
+        }
+    }
+
+    /**
+     * Gets the javadocs associated with field name in classDoc.  Throws a
+     * runtime exception if this proves impossible.
+     */
+    private FieldDoc getFieldDoc(final ClassDoc classDoc, final String name) {
+        return getFieldDoc(classDoc, name, false);
+    }
+
+    /**
+     * Recursive helper routine to getFieldDoc()
+     */
+    private FieldDoc getFieldDoc(final ClassDoc classDoc, final String name, final boolean primary) {
+        for (final FieldDoc fieldDoc : classDoc.fields(false)) {
+            if (fieldDoc.name().equals(name))
+                return fieldDoc;
+
+            // This can return null, specifically, we can encounter https://bugs.openjdk.java.net/browse/JDK-8033735,
+            // which is fixed in JDK9 http://hg.openjdk.java.net/jdk9/jdk9/hotspot/rev/ba8c351b7096.
+            final Field field = DocletUtils.getFieldForFieldDoc(fieldDoc);
+            if (field == null) {
+                logger.warn(
+                    String.format(
+                        "Could not access the field definition for %s while searching for %s, presumably because the field is inaccessible",
+                        fieldDoc.name(),
+                        name)
+                );
+            } else if (field.isAnnotationPresent(ArgumentCollection.class)) {
+                final ClassDoc typeDoc = getDoclet().getRootDoc().classNamed(fieldDoc.type().qualifiedTypeName());
+                if (typeDoc == null)
+                    throw new DocException("Tried to get javadocs for ArgumentCollection field " +
+                            fieldDoc + " but couldn't find the class in the RootDoc");
+                else {
+                    FieldDoc result = getFieldDoc(typeDoc, name, false);
+                    if (result != null)
+                        return result;
+                    // else keep searching
+                }
+            }
+        }
+
+        // if we didn't find it here, wander up to the superclass to find the field
+        if (classDoc.superclass() != null) {
+            return getFieldDoc(classDoc.superclass(), name, false);
+        }
+
+        if (primary)
+            throw new RuntimeException("No field found for expected field " + name);
+        else
+            return null;
+    }
+
+    /**
+     * Returns a Pair of (main, synonym) names for argument with fullName s1 and
+     * shortName s2.
+     *
+     * Previously we had it so the main name was selected to be the longest of the two, provided
+     * it didn't exceed MAX_DISPLAY_NAME, in which case the shorter was taken. But we now disable
+     * the length-based name rearrangement in order to maintain consistency in the Docs table.
+     *
+     * This may cause messed up spacing in the CLI-help display but we don't care as much about that
+     * since more users use the online Docs for looking up arguments.
+     *
+     * @param s1 the short argument name without -, or null if not provided
+     * @param s2 the long argument name without --, or null if not provided
+     * @return A pair of fully qualified names (with - or --) for the argument.  The first
+     *         element is the primary display name while the second (potentially null) is a
+     *         synonymous name.
+     */
+    private Pair<String, String> displayNames(String s1, String s2) {
+        s1 = ((s1 == null) || (s1.length() == 0)) ? null : "-" + s1;
+        s2 = ((s2 == null) || (s2.length() == 0)) ? null : "--" + s2;
+
+        if (s1 == null) return Pair.of(s2, null);
+        if (s2 == null) return Pair.of(s1, null);
+
+        return Pair.of(s2, s1);
+    }
+
+    /**
+     * Returns a human readable string that describes the Type type of an argument.
+     *
+     * This will include parametrized types, so that Set{T} shows up as Set(T) and not
+     * just Set in the docs.
+     *
+     * @param type
+     * @return String representing the argument type
+     */
+    protected String argumentTypeString(final Type type) {
+        if (type instanceof ParameterizedType) {
+            final ParameterizedType parameterizedType = (ParameterizedType) type;
+            final List<String> subs = new ArrayList<>();
+            for (final Type actualType : parameterizedType.getActualTypeArguments())
+                subs.add(argumentTypeString(actualType));
+            return argumentTypeString(((ParameterizedType) type).getRawType()) + "[" + String.join(",", subs) + "]";
+        } else if (type instanceof GenericArrayType) {
+            return argumentTypeString(((GenericArrayType) type).getGenericComponentType()) + "[]";
+        } else if (type instanceof WildcardType) {
+            throw new RuntimeException("We don't support wildcards in arguments: " + type);
+        } else if (type instanceof Class<?>) {
+            return ((Class) type).getSimpleName();
+        } else {
+            throw new DocException("Unknown type: " + type);
+        }
+    }
+
+    /**
+     * High-level entry point for creating a FreeMarker map describing the argument
+     * source with definition def, with associated javadoc fieldDoc.
+     *
+     * @param fieldDoc
+     * @param def
+     * @return a non-null Map binding argument keys with their values
+     */
+    protected Map<String, Object> docForArgument(final FieldDoc fieldDoc, final CommandLineArgumentParser.ArgumentDefinition def) {
+        final Map<String, Object> root = new HashMap<>();
+
+        final Pair<String, String> names = displayNames(def.shortName, def.getLongName());
+        root.put("name", names.getLeft());
+        root.put("synonyms", names.getRight() != null ? names.getRight() : "NA");
+        root.put("required", def.optional ? "no": "yes") ;
+        root.put("type", argumentTypeString(def.field.getGenericType()));
+
+        // summary and fulltext
+        root.put("summary", def.doc != null ? def.doc : "");
+        root.put("fulltext", fieldDoc.commentText());
+
+        // Does this argument interact with any others?
+        if (def.isControlledByPlugin()) {
+            root.put("otherArgumentRequired",
+                    def.parent.getClass().getSimpleName().length() == 0 ?
+                        def.parent.getClass().getName() :
+                        def.parent.getClass().getSimpleName());
+        } else {
+            root.put("otherArgumentRequired", "NA");
+        }
+
+        root.put("exclusiveOf",
+                def.mutuallyExclusive != null && !def.mutuallyExclusive.isEmpty() ?
+                    String.join(", ", def.mutuallyExclusive) :
+                    "NA");
+
+        // enum options
+        root.put("options",
+                def.field.getType().isEnum() ?
+                        docForEnumArgument(def.field.getType()) :
+                        Collections.EMPTY_LIST);
+
+        List<String> attributes = new ArrayList<>();
+        if (!def.optional) {
+            attributes.add("required");
+        }
+        if (def.field.isAnnotationPresent(Deprecated.class)) {
+            attributes.add("deprecated");
+        }
+        root.put("attributes", attributes.size() > 0 ? String.join(", ", attributes) : "NA");
+
+        return root;
+    }
+
+    /**
+     * Helper routine that provides a FreeMarker map for an enumClass, grabbing the
+     * values of the enum and their associated javadoc documentation.
+     *
+     * @param enumClass
+     * @return
+     */
+    private List<Map<String, Object>> docForEnumArgument(final Class<?> enumClass) {
+        final ClassDoc doc = this.getDoclet().getClassDocForClass(enumClass);
+        if ( doc == null ) {
+            throw new RuntimeException("Tried to get docs for enum " + enumClass + " but got null instead");
+        }
+
+        final Set<String> enumConstantFieldNames = enumConstantsNames(enumClass);
+        final List<Map<String, Object>> bindings = new ArrayList<Map<String, Object>>();
+        for (final FieldDoc fieldDoc : doc.fields(false)) {
+            if (enumConstantFieldNames.contains(fieldDoc.name()) ) {
+                bindings.add(
+                        new HashMap<String, Object>() {
+                            static final long serialVersionUID = 0L;
+                            {
+                                put("name", fieldDoc.name());
+                                put("summary", fieldDoc.commentText());
+                            }
+                        }
+                );
+            }
+        }
+
+        return bindings;
+    }
+
+    /**
+     * @return a non-null set of fields that are enum constants
+     */
+    private Set<String> enumConstantsNames(final Class<?> enumClass) {
+        final Set<String> enumConstantFieldNames = new HashSet<String>();
+
+        for ( final Field field : enumClass.getFields() ) {
+            if ( field.isEnumConstant() )
+                enumConstantFieldNames.add(field.getName());
+        }
+
+        return enumConstantFieldNames;
+    }
+}
diff --git a/src/main/java/org/broadinstitute/barclay/help/DocException.java b/src/main/java/org/broadinstitute/barclay/help/DocException.java
new file mode 100644
index 0000000..23e1a63
--- /dev/null
+++ b/src/main/java/org/broadinstitute/barclay/help/DocException.java
@@ -0,0 +1,17 @@
+package org.broadinstitute.barclay.help;
+
+/**
+ * Base class for documentation generation exceptions generated by Barclay.
+ */
+public class DocException extends RuntimeException {
+    private static final long serialVersionUID = 1L;
+
+    public DocException( String msg ) {
+        super(msg);
+    }
+
+    public DocException( String message, Throwable throwable ) {
+        super(message, throwable);
+    }
+
+}
diff --git a/src/main/java/org/broadinstitute/barclay/help/DocWorkUnit.java b/src/main/java/org/broadinstitute/barclay/help/DocWorkUnit.java
new file mode 100644
index 0000000..e27cdda
--- /dev/null
+++ b/src/main/java/org/broadinstitute/barclay/help/DocWorkUnit.java
@@ -0,0 +1,196 @@
+package org.broadinstitute.barclay.help;
+
+import com.sun.javadoc.ClassDoc;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.broadinstitute.barclay.argparser.CommandLineProgramGroup;
+import org.broadinstitute.barclay.argparser.CommandLineProgramProperties;
+import org.broadinstitute.barclay.argparser.BetaFeature;
+import org.broadinstitute.barclay.utils.Utils;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Collection of all relevant information about a single feature the HelpDoclet can document
+ */
+public class DocWorkUnit implements Comparable<DocWorkUnit> {
+    final protected static Logger logger = LogManager.getLogger(DocWorkUnit.class);
+
+    private final String name;                          // name of the this work unit/feature
+
+    private final Class<?> clazz;                       // class that's being documented
+    private final ClassDoc classDoc;                    // javadoc documentation for clazz
+    private final DocWorkUnitHandler workUnitHandler;   // handler for this work unit
+
+    // Annotations attached to the feature class being documented by this work unit
+    private final DocumentedFeature documentedFeature;
+    private final CommandLineProgramProperties commandLineProperties;
+    private final BetaFeature betaFeature;
+
+    private Map<String, Object> propertyMap = new HashMap<>(); // propertyMap for this unit's template
+
+    // Cached values derived by fallback policies that are implemented by the work unit handler.
+    protected String summary;         // summary description of this feature
+    protected String groupName;       // name of the feature group to which this feature belongs
+    protected String groupSummary;    // summary description of this feature's feature group
+
+    /**
+     * @param workUnitHandler
+     * @param documentedFeatureAnnotation
+     * @param classDoc
+     * @param clazz
+     */
+    public DocWorkUnit(
+            final DocWorkUnitHandler workUnitHandler,
+            final DocumentedFeature documentedFeatureAnnotation,
+            final ClassDoc classDoc,
+            final Class<?> clazz)
+    {
+        Utils.nonNull(workUnitHandler, "workUnitHandler cannot be null");
+        Utils.nonNull(documentedFeatureAnnotation, "DocumentedFeature annotation cannot be null");
+        Utils.nonNull(classDoc, "classDoc cannot be null");
+        Utils.nonNull(clazz, "class cannot be null");
+
+        this.name = clazz.getSimpleName();
+
+        this.documentedFeature = documentedFeatureAnnotation;
+        this.commandLineProperties = clazz.getAnnotation(CommandLineProgramProperties.class);
+        this.betaFeature = clazz.getAnnotation(BetaFeature.class);
+        this.workUnitHandler = workUnitHandler;
+        this.classDoc = classDoc;
+        this.clazz = clazz;
+
+        // summary, groupName and groupSummary can each be determined via fallback policies dictated
+        // by the feature handler, so delegate back to the handler to allow it to do the initialization
+        // once, and then cache the results so that all consumers see consistent values.
+        summary = workUnitHandler.getSummaryForWorkUnit(this);
+        groupName = workUnitHandler.getGroupNameForWorkUnit(this);
+        groupSummary = workUnitHandler.getGroupSummaryForWorkUnit(this);
+    }
+
+    /**
+     * Get the root property map used for this work unit.
+     * @return Root property map for this work unit.
+     */
+    public Map<String, Object> getRootMap() {
+        return (this.propertyMap);
+    }
+
+    /**
+     * Set a property on the root property map for this work unit.
+     * @param key
+     * @param value
+     */
+    public void setProperty(final String key, final Object value) {
+        propertyMap.put(key, value);
+    }
+
+    /**
+     * Get a property from the root property map for this work unit
+     * @param key
+     * @return Object value for the given property, or null if property not found.
+     */
+    public Object getProperty(final String key) {
+        return propertyMap.get(key);
+    }
+
+    /**
+     * Get the DocumentedFeature annotation object for this class.
+     * @return DocumentedFeature object. Will not be null.
+     */
+    public DocumentedFeature getDocumentedFeature() { return documentedFeature; }
+
+    /**
+     * Get the JavDoc ClassDoc for this work unit.
+     * @return ClassDoc for this work unit. Will not be null.
+     */
+    public ClassDoc getClassDoc() { return classDoc; }
+
+    /**
+     * The name of this documentation unit
+     */
+    public String getName() {
+        return name;
+    }
+
+    /**
+     * The name of the documentation group (e.g., walkers, read filters) class belongs to
+     */
+    public String getGroupName() {
+        return groupName;
+    }
+
+    /**
+     * The group summary for the group for this object
+     */
+    public String getGroupSummary() {
+        return groupSummary;
+    }
+
+    /**
+     * The summary of the documentation object
+     */
+    public String getSummary() { return summary; }
+
+    public Class<?> getClazz() { return clazz; }
+
+    /**
+     * Populate the property map for this work unit by delegating to the documented feature handler for this work unit.
+     * @param featureMaps map of all features included in this javadoc run
+     * @param groupMaps map of all groups included in the javadoc run
+     */
+    public void processDoc(final List<Map<String, String>> featureMaps, final List<Map<String, String>> groupMaps) {
+        workUnitHandler.processWorkUnit(this, featureMaps, groupMaps);
+
+    };
+
+    /**
+     * Get the template to be used for this work unit. Delegates to the feature handler.
+     * @return name of the template (relative to the input path specified to the doclet) for the template to be used
+     * for this work unit.
+     */
+    public String getTemplateName() { return workUnitHandler.getTemplateName(this); }
+
+    public String getTargetFileName() { return workUnitHandler.getDestinationFilename(this); }
+
+    public String getJSONFileName() { return workUnitHandler.getJSONFilename(this); }
+
+    /**
+     * Get the CommandLineProgramProperties annotation for this work unit.
+     * @return CommandLineProgramProperties object for this work unit. May be null for features that are not
+     * command line programs.
+     */
+    public CommandLineProgramProperties getCommandLineProperties() { return commandLineProperties; }
+
+    /**
+     * @return a boolean determining if this documented feature is marked as a beta feature
+     */
+    public boolean getBetaFeature() { return betaFeature != null; }
+
+    /**
+     * Get the CommandLineProgramGroup object from the CommandLineProgramProperties of this work unit.
+     * @return CommandLineProgramGroup if the feature has one, otherwise null.
+     */
+    public CommandLineProgramGroup getCommandLineProgramGroup() {
+        if (commandLineProperties != null) {
+            try {
+                return commandLineProperties.programGroup().newInstance();
+            } catch (IllegalAccessException | InstantiationException e) {
+                logger.warn(
+                        String.format("Can't instantiate program group class to retrieve summary for group %s for class %s",
+                                commandLineProperties.programGroup().getName(),
+                                clazz.getName()));
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Sort in order of the name of this WorkUnit
+     */
+    public int compareTo(DocWorkUnit other) {
+        return this.name.compareTo(other.name);
+    }
+}
diff --git a/src/main/java/org/broadinstitute/barclay/help/DocWorkUnitHandler.java b/src/main/java/org/broadinstitute/barclay/help/DocWorkUnitHandler.java
new file mode 100644
index 0000000..cd496bb
--- /dev/null
+++ b/src/main/java/org/broadinstitute/barclay/help/DocWorkUnitHandler.java
@@ -0,0 +1,95 @@
+package org.broadinstitute.barclay.help;
+
+import org.broadinstitute.barclay.utils.Utils;
+
+import java.io.*;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Abstract base class for work unit handlers for docs. The DocWorkUnitHandler defines the template
+ * used for each documented feature, and populates the template property map for that template.
+ */
+public abstract class DocWorkUnitHandler {
+    private final HelpDoclet doclet;
+
+    /**
+     * @param doclet the HelpDoclet driving this documentation run. Can not be null.
+     */
+    public DocWorkUnitHandler(final HelpDoclet doclet) {
+        Utils.nonNull("Doclet cannot be null");
+        this.doclet = doclet;
+    }
+
+    /**
+     * @return the HelpDoclet driving this documentation run
+     */
+    public HelpDoclet getDoclet() {
+        return doclet;
+    }
+
+    /**
+     * Actually generate the documentation map by populating the associated workUnit's properties.
+     *
+     * @param workUnit work unit to generate documentation for
+     */
+    public abstract void processWorkUnit(DocWorkUnit workUnit, List<Map<String, String>>featureMaps, List<Map<String, String>> groupMaps);
+
+    /**
+     * Return the name of the FreeMarker template to be used to process the work unit.
+     *
+     * Note this is a flat filename relative to settings/helpTemplates in the source tree
+     * @param workUnit template to use for this work unit
+     * @return name of the template
+     * @throws IOException
+     */
+    public abstract String getTemplateName(DocWorkUnit workUnit);
+
+    /**
+     * Return the flat filename (no paths) that the handler would like the Doclet to
+     * write out the documentation for workUnit
+     * @param workUnit
+     * @return the name of the destination file to which documentation output will be written
+     */
+    public String getDestinationFilename(final DocWorkUnit workUnit) {
+        return DocletUtils.phpFilenameForClass(workUnit.getClazz(), HelpDoclet.outputFileExtension);
+    }
+
+    /**
+     * Returns the JSON output file name.
+     */
+    public String getJSONFilename(final DocWorkUnit workUnit) {
+        return DocletUtils.phpFilenameForClass(workUnit.getClazz(), "json");
+    }
+
+    /**
+     * Apply any fallback rules to determine the summary line that should be used for the work unit.
+     * Default implementation uses the value from the DocumentedFeature annotation.
+     * @param workUnit
+     * @return Summary for this work unit.
+     */
+    public String getSummaryForWorkUnit(final DocWorkUnit workUnit) {
+        return workUnit.getDocumentedFeature().summary();
+    }
+
+    /**
+     * Apply any fallback rules to determine the group name line that should be used for the work unit.
+     * Default implementation uses the value from the DocumentedFeature annotation.
+     * @param workUnit
+     * @return Group name to be used for this work unit.
+     */
+    public String getGroupNameForWorkUnit(final DocWorkUnit workUnit) {
+        return workUnit.getDocumentedFeature().groupName();
+    }
+
+    /**
+     * Apply any fallback rules to determine the group summary line that should be used for the work unit.
+     * Default implementation uses the value from the DocumentedFeature annotation.
+     * @param workUnit
+     * @return Group summary to be used for this work unit.
+     */
+    public String getGroupSummaryForWorkUnit(final DocWorkUnit workUnit) {
+        return workUnit.getDocumentedFeature().groupSummary();
+    }
+
+}
diff --git a/src/main/java/org/broadinstitute/barclay/help/DocletUtils.java b/src/main/java/org/broadinstitute/barclay/help/DocletUtils.java
new file mode 100644
index 0000000..935db52
--- /dev/null
+++ b/src/main/java/org/broadinstitute/barclay/help/DocletUtils.java
@@ -0,0 +1,64 @@
+package org.broadinstitute.barclay.help;
+
+import com.sun.javadoc.FieldDoc;
+import com.sun.javadoc.PackageDoc;
+import com.sun.javadoc.ProgramElementDoc;
+import org.broadinstitute.barclay.utils.JVMUtils;
+
+import java.lang.reflect.Field;
+
+/**
+ * Package protected - Methods in the class must ONLY be used by doclets, since the com.sun.javadoc.* classes
+ * are not available on all systems, and we don't want the GATK proper to depend on them.
+ */
+class DocletUtils {
+
+    protected static Class<?> getClassForDoc(ProgramElementDoc doc) throws ClassNotFoundException {
+        return Class.forName(getClassName(doc, true));
+    }
+
+    protected static Field getFieldForFieldDoc(FieldDoc fieldDoc) {
+        try {
+            Class<?> clazz = getClassForDoc(fieldDoc.containingClass());
+            return JVMUtils.findField(clazz, fieldDoc.name());
+        } catch (ClassNotFoundException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    /**
+     * Reconstitute the class name from the given class JavaDoc object.
+     *
+     * @param doc the Javadoc model for the given class.
+     * @return The (string) class name of the given class.
+     */
+    protected static String getClassName(ProgramElementDoc doc, boolean binaryName) {
+        PackageDoc containingPackage = doc.containingPackage();
+        String className = doc.name();
+        if (binaryName) {
+            className = className.replaceAll("\\.", "\\$");
+        }
+        return containingPackage.name().length() > 0 ?
+                String.format("%s.%s", containingPackage.name(), className) :
+                String.format("%s", className);
+    }
+
+    /**
+     * Return the filename of the GATKDoc PHP that would be generated for Class.  This
+     * does not guarantee that the docs exist, or that docs would actually be generated
+     * for class (might not be annotated for documentation, for example).  But if
+     * this class is documented, GATKDocs will write the docs to a file named as returned
+     * by this function.
+     *
+     * @param c
+     * @return
+     */
+    public static String phpFilenameForClass(Class<?> c) {
+        return phpFilenameForClass(c, "php");
+    }
+
+    public static String phpFilenameForClass(Class<?> c, String extension) {
+        return c.getName().replace(".", "_") + "." + extension;
+    }
+
+}
\ No newline at end of file
diff --git a/src/main/java/org/broadinstitute/barclay/help/DocumentedFeature.java b/src/main/java/org/broadinstitute/barclay/help/DocumentedFeature.java
new file mode 100644
index 0000000..add19ff
--- /dev/null
+++ b/src/main/java/org/broadinstitute/barclay/help/DocumentedFeature.java
@@ -0,0 +1,47 @@
+package org.broadinstitute.barclay.help;
+
+import java.lang.annotation.*;
+
+/**
+ * An annotation to identify a class as a target for documentation. Classes tagged with
+ * the annotation should have a no-arg constructor that can be called by the doc system.
+ *
+ * The properties of this annotation all have defaults so that this annotation can be used as a tag
+ * interface for classes that are also annotated with CommandLineProgramProperties. The doc gen system
+ * will attempt to retrieve any values that are missing from the DocumentedFeature annotation with values
+ * from the {@code CommandLineProgramProperties} and {@code CommandLineProgramGroup} annotations.
+ *
+ * @author depristo
+ */
+ at Documented
+ at Retention(RetentionPolicy.RUNTIME)
+ at Target(ElementType.TYPE)
+public @interface DocumentedFeature {
+    /**
+     * Should we actually document this feature, even though it's annotated?
+     */
+    public boolean enable() default true;
+    /**
+     * The overall group name (walkers, readfilters) this feature is associated with
+     * @return The overall group name (walkers, readfilters) this feature is associated with
+     */
+    public String groupName() default "";
+
+    /**
+     * A human readable summary of the purpose of this group of features
+     * @return A human readable summary of the purpose of this group of features
+     */
+    public String groupSummary() default "";
+
+    /**
+     * A human readable summary of the purpose of this feature
+     * @return A human readable summary of the purpose of this feature
+     */
+    public String summary() default "";
+
+    /**
+     * Are there links to other docs that we should include?  Must reference a class that itself uses
+     * the DocumentedFeature documentedFeatureObject.
+     */
+    public Class<?>[] extraDocs() default {};
+}
diff --git a/src/main/java/org/broadinstitute/barclay/help/GSONArgument.java b/src/main/java/org/broadinstitute/barclay/help/GSONArgument.java
new file mode 100644
index 0000000..af3db7d
--- /dev/null
+++ b/src/main/java/org/broadinstitute/barclay/help/GSONArgument.java
@@ -0,0 +1,54 @@
+package org.broadinstitute.barclay.help;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * GSON-friendly version of the argument bindings
+ */
+public class GSONArgument {
+
+    String summary;
+    String name;
+    String synonyms;
+    String type;
+    String required;
+    String fulltext;
+    String defaultValue;
+    String minValue;
+    String maxValue;
+    String minRecValue;
+    String maxRecValue;
+    String kind;
+    List<Map<String, Object>> options;
+
+    public void populate(   final String summary,
+                            final String name,
+                            final String synonyms,
+                            final String type,
+                            final String required,
+                            final String fulltext,
+                            final String defaultValue,
+                            final String minValue,
+                            final String maxValue,
+                            final String minRecValue,
+                            final String maxRecValue,
+                            final String kind,
+                            final List<Map<String, Object>> options
+    ) {
+        this.summary = summary;
+        this.name = name;
+        this.synonyms = synonyms;
+        this.type = type;
+        this.required = required;
+        this.fulltext = fulltext;
+        this.defaultValue = defaultValue;
+        this.minValue = minValue;
+        this.maxValue = maxValue;
+        this.minRecValue = minRecValue;
+        this.maxRecValue = maxRecValue;
+        this.kind = kind;
+        this.options = options;
+    }
+
+}
diff --git a/src/main/java/org/broadinstitute/barclay/help/GSONWorkUnit.java b/src/main/java/org/broadinstitute/barclay/help/GSONWorkUnit.java
new file mode 100644
index 0000000..bc2a1f3
--- /dev/null
+++ b/src/main/java/org/broadinstitute/barclay/help/GSONWorkUnit.java
@@ -0,0 +1,27 @@
+package org.broadinstitute.barclay.help;
+
+/**
+ * GSON-friendly version of the DocWorkUnit
+ */
+public class GSONWorkUnit {
+
+    String summary;
+    Object arguments;
+    String description;
+    String name;
+    String group;
+
+    public void populate(String summary,
+                         Object arguments,
+                         String description,
+                         String name,
+                         String group
+    ) {
+        this.summary = summary;
+        this.arguments = arguments;
+        this.description = description;
+        this.name = name;
+        this.group = group;
+    }
+
+}
diff --git a/src/main/java/org/broadinstitute/barclay/help/HelpDoclet.java b/src/main/java/org/broadinstitute/barclay/help/HelpDoclet.java
new file mode 100644
index 0000000..2f49183
--- /dev/null
+++ b/src/main/java/org/broadinstitute/barclay/help/HelpDoclet.java
@@ -0,0 +1,635 @@
+package org.broadinstitute.barclay.help;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.sun.javadoc.ClassDoc;
+import com.sun.javadoc.RootDoc;
+
+import freemarker.cache.TemplateLoader;
+import freemarker.cache.FileTemplateLoader;
+import freemarker.cache.ClassTemplateLoader;
+import freemarker.cache.MultiTemplateLoader;
+
+import freemarker.template.Configuration;
+import freemarker.template.DefaultObjectWrapper;
+import freemarker.template.Template;
+import freemarker.template.TemplateException;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.broadinstitute.barclay.argparser.Hidden;
+import org.broadinstitute.barclay.utils.JVMUtils;
+
+import java.io.*;
+import java.util.*;
+
+/**
+ * Javadoc Doclet that combines javadoc, Barclay annotations, and FreeMarker
+ * templates to produce formatted docs for classes.
+ * <p/>
+ * The doclet has the following workflow:
+ * <p/>
+ * 1 -- walk the javadoc hierarchy, looking for classes that have the DocumentedFeature annotation
+ * 2 -- for each annotated class, construct a WorkUnit/Handler to determine if that feature
+ * should be included in the ouput
+ * 3 -- for each included feature, construct a DocWorkUnit consisting of all documentation
+ * evidence (DocumentedFeature/CommandLineProgramProperties annotations, javadoc ClassDoc,
+ * java Class, and DocWorkUnitHandler
+ * 4 -- After all DocWorkUnits are accumulated, delegate the processing of each work unit to
+ * the work unit's handler, allowing it to populate the work unit's Freemarker property map, after
+ * which each work unit is written to it's template-based output file and GSON file
+ * 5 -- write out an index of all units, organized by group
+ * <p/>
+ * Note: although this class can be used to generate documentation directly, most consumers will
+ * want to subclass it to override the following methods in order to create application-specific
+ * templates and template property maps:
+ *
+ * {@link #getIndexTemplateName}
+ * {@link #createWorkUnit}
+ * {@link #createGSONWorkUnit}
+ * {@link #start(RootDoc)} A static method that instantiates the subclass and delegates to the
+ * instance method {@link #startProcessDocs(RootDoc)}.
+ */
+public class HelpDoclet {
+    final protected static Logger logger = LogManager.getLogger(HelpDoclet.class);
+
+    // Builtin javadoc command line arguments
+    final private static String DESTINATION_DIR_OPTION = "-d";
+    final private static String WINDOW_TITLE_OPTION = "-windowtitle";
+    final private static String DOC_TITLE_OPTION = "-doctitle";
+    final private static String QUIET_OPTION = "-quiet";
+
+    // Barclay HelpDoclet custom command line options
+    final private static String SETTINGS_DIR_OPTION = "-settings-dir";
+    final private static String BUILD_TIMESTAMP_OPTION = "-build-timestamp";
+    final private static String ABSOLUTE_VERSION_OPTION = "-absolute-version";
+    final private static String INCLUDE_HIDDEN_OPTION = "-hidden-version";
+    final private static String OUTPUT_FILE_EXTENSION_OPTION = "-output-file-extension";
+    final private static String INDEX_FILE_EXTENSION_OPTION = "-index-file-extension";
+    final private static String USE_DEFAULT_TEMPLATES_OPTION = "-use-default-templates";
+
+    // Where we find the help FreeMarker templates
+    final private static File DEFAULT_SETTINGS_DIR = new File("settings/helpTemplates");
+    final private static String DEFAULT_SETTINGS_CLASSPATH = "/org/broadinstitute/barclay/helpTemplates";
+    // Where we write the output
+    final private static File DEFAULT_DESTINATION_DIR = new File("barclaydocs");
+    // Default output file extension
+    final private static String DEFAULT_OUTPUT_FILE_EXTENSION = "html";
+
+    // ----------------------------------------------------------------------
+    //
+    // Variables that are set on the command line when running javadoc
+    //
+    // ----------------------------------------------------------------------
+    protected static File settingsDir = DEFAULT_SETTINGS_DIR;
+    protected boolean isSettingsDirSet = false;
+    protected static File destinationDir = DEFAULT_DESTINATION_DIR;
+    protected static String outputFileExtension = DEFAULT_OUTPUT_FILE_EXTENSION;
+    protected static String indexFileExtension = DEFAULT_OUTPUT_FILE_EXTENSION;
+    protected static String buildTimestamp = "[no timestamp available]";
+    protected static String absoluteVersion = "[no version available]";
+    protected static boolean showHiddenFeatures = false;
+    protected boolean useDefaultTemplates = false;
+
+    // Variables to store data for Freemarker:
+    private RootDoc rootDoc;                // The javadoc root doc
+    protected Set<DocWorkUnit> workUnits;     // Set of all things we are going to document
+
+    /**
+     * The entry point for javadoc generation. Default implementation creates an instance of
+     * {@link HelpDoclet} and calls {@link #startProcessDocs(RootDoc)} on that instance.
+     *
+     * <p>Note: Custom Barclay doclets should subclass this class, and implement a similar static
+     * method that creates an instance of the doclet subclass and delegates to that instance's
+     * {@link #startProcessDocs(RootDoc)}.
+     */
+     public static boolean start(final RootDoc rootDoc) throws IOException {
+         return new HelpDoclet().startProcessDocs(rootDoc);
+     }
+
+    /**
+     * Ensure that {@link #settingsDir} exists and is a directory.
+     * Throws a {@link RuntimeException} if {@link #settingsDir} is invalid.
+     */
+    private void validateSettingsDir() {
+         if (!settingsDir.exists()) {
+             throw new RuntimeException(SETTINGS_DIR_OPTION + " : " + settingsDir.getPath() + " does not exist!");
+         }
+         else if (!settingsDir.isDirectory()) {
+             throw new RuntimeException(SETTINGS_DIR_OPTION + " : " + settingsDir.getPath() + " is not a directory!");
+         }
+     }
+
+    /**
+     * This method exists to allow child classes to do input argument / state checking.
+     * Designed to be overridden in child classes.
+     *
+     * Child classes should do internal validation and throw if there are issues.
+     */
+    protected void validateDocletStartingState() {
+
+    }
+
+    /**
+     * Extracts the contents of certain types of javadoc and adds them to an output file.
+     *
+     * @param rootDoc The documentation root.
+     * @return Whether the JavaDoc run succeeded.
+     * @throws java.io.IOException if output can't be written.
+     */
+    protected boolean startProcessDocs(final RootDoc rootDoc) throws IOException {
+        for (String[] options : rootDoc.options()) {
+            parseOption(options);
+        }
+
+        // Make sure the user specified a settings directory OR that we should use the defaults.
+        // Both are not allowed.
+        // Neither are not allowed.
+        if ( (useDefaultTemplates && isSettingsDirSet) ||
+                (!useDefaultTemplates && !isSettingsDirSet)) {
+            throw new RuntimeException("ERROR: must specify only ONE of: " + USE_DEFAULT_TEMPLATES_OPTION + " , " + SETTINGS_DIR_OPTION);
+        }
+
+        // Make sure we can use the directory for settings we have set:
+        if (!useDefaultTemplates) {
+            validateSettingsDir();
+        }
+
+        // Make sure we're in a good state to run:
+        validateDocletStartingState();
+
+
+
+        processDocs(rootDoc);
+        return true;
+    }
+
+    /**
+     * Handles javadoc command line options. The first entry in the {@code options} array is the
+     * option name. Subsequent entries contain the option's arguments, the number of which is
+     * determined by the value returned from {@link #optionLength(String)} for that option.
+     *
+     * <p>Custom Barclay doclets that want to have custom command line options should override this
+     * method, and also provide an implementation of the static method {@link #optionLength}).
+     * Both methods should handle the custom options, and then delegate back to the base class
+     * methods defined here to allow default handling of builtin options.
+     *
+     * @param options Options to parse.
+     * @return True if {@code options} was parsed, False otherwise.
+     */
+    protected boolean parseOption(final String[] options) {
+
+        boolean hasParsedOption = false;
+
+        if (options[0].equals(SETTINGS_DIR_OPTION)) {
+            settingsDir = new File(options[1]);
+            isSettingsDirSet = true;
+            hasParsedOption = true;
+        }
+        else if (options[0].equals(DESTINATION_DIR_OPTION)) {
+            destinationDir = new File(options[1]);
+            hasParsedOption = true;
+        }
+        else if (options[0].equals(BUILD_TIMESTAMP_OPTION)) {
+            buildTimestamp = options[1];
+            hasParsedOption = true;
+        }
+        else if (options[0].equals(ABSOLUTE_VERSION_OPTION)) {
+            absoluteVersion = options[1];
+            hasParsedOption = true;
+        }
+        else if (options[0].equals(INCLUDE_HIDDEN_OPTION)) {
+            showHiddenFeatures = true;
+            hasParsedOption = true;
+        }
+        else if (options[0].equals(OUTPUT_FILE_EXTENSION_OPTION)) {
+            outputFileExtension = options[1];
+            hasParsedOption = true;
+        }
+        else if (options[0].equals(INDEX_FILE_EXTENSION_OPTION)) {
+            indexFileExtension = options[1];
+            hasParsedOption = true;
+        }
+        else if (options[0].equals(USE_DEFAULT_TEMPLATES_OPTION)) {
+            useDefaultTemplates = true;
+            hasParsedOption = true;
+        }
+
+        return hasParsedOption;
+    }
+    /**
+     * Validates the given options against options supported by this doclet.
+     *
+     * <p>Custom Barclay doclets that want to have custom command line options should provide an
+     * implementation of this static method, and also override override {@link #parseOption}).
+     * Both methods should handle the custom options, delegating back to the base class methods
+     * defined here to allow default handling of builtin options.
+     *
+     * @param option Option to validate.
+     * @return Number of potential parameters; 0 if not supported.
+     */
+    public static int optionLength(final String option) {
+        // Any arguments used for the doclet need to be recognized here. Many javadoc plugins (ie. gradle)
+        // automatically add some such as "-doctitle", "-windowtitle", which we ignore.
+        if (option.equals(DOC_TITLE_OPTION) ||
+            option.equals(WINDOW_TITLE_OPTION) ||
+            option.equals(SETTINGS_DIR_OPTION) ||
+            option.equals(DESTINATION_DIR_OPTION) ||
+            option.equals(BUILD_TIMESTAMP_OPTION) ||
+            option.equals(ABSOLUTE_VERSION_OPTION) ||
+            option.equals(OUTPUT_FILE_EXTENSION_OPTION) ||
+            option.equals(INDEX_FILE_EXTENSION_OPTION)) {
+            return 2;
+        } else if (option.equals(QUIET_OPTION) ||
+                   option.equals(USE_DEFAULT_TEMPLATES_OPTION)) {
+            return 1;
+        } else {
+            logger.error("The Javadoc command line option is not recognized by the Barclay doclet: " + option);
+            return 0;
+        }
+    }
+
+    /**
+     * Process the classes that have been included by the javadoc process in the rootDoc object.
+     *
+     * @param rootDoc root structure containing the the set of objects accumulated by the javadoc process
+     */
+    private void processDocs(final RootDoc rootDoc) {
+        this.rootDoc = rootDoc;
+
+        // Get a list of all the features and groups that we'll actually retain
+        workUnits = computeWorkUnits();
+
+        final Set<String> uniqueGroups = new HashSet<>();
+        final List<Map<String, String>> featureMaps = new ArrayList<>();
+        final List<Map<String, String>> groupMaps = new ArrayList<>();
+
+        // First pass over work units: create the top level map of features and groups
+        workUnits.stream().forEach(
+                workUnit -> {
+                    featureMaps.add(indexDataMap(workUnit));
+                    if (!uniqueGroups.contains(workUnit.getGroupName())) {
+                        uniqueGroups.add(workUnit.getGroupName());
+                        groupMaps.add(getGroupMap(workUnit));
+                    }
+                }
+        );
+
+        // Second pass:  populate the property map for each work unit
+        workUnits.stream().forEach(workUnit -> { workUnit.processDoc(featureMaps, groupMaps); });
+
+        // Third pass: Generate the individual outputs for each work unit, and the top-level index file
+        emitOutputFromTemplates(groupMaps, featureMaps);
+    }
+
+
+    /**
+     * For each class in the rootDoc class list, delegate to the appropriate DocWorkUnitHandler to
+     * determine if it should be included in this run, and for each included feature, construct a DocWorkUnit.
+     *
+     * @return the set of all DocWorkUnits for which we are actually generating docs
+     */
+    private Set<DocWorkUnit> computeWorkUnits() {
+        final TreeSet<DocWorkUnit> workUnits = new TreeSet<>();
+
+        for (final ClassDoc classDoc : rootDoc.classes()) {
+            final Class<?> clazz = getClassForClassDoc(classDoc);
+            final DocumentedFeature documentedFeature = getDocumentedFeatureForClass(clazz);
+
+            if (documentedFeature != null) {
+                if (documentedFeature.enable()) {
+                    DocWorkUnit workUnit = createWorkUnit(
+                            documentedFeature,
+                            classDoc,
+                            clazz);
+                    if (workUnit != null) {
+                        workUnits.add(workUnit);
+                    }
+                } else {
+                    logger.info("Skipping disabled documentation for feature: " + classDoc);
+                }
+            }
+        }
+
+        return workUnits;
+    }
+
+    public RootDoc getRootDoc() { return rootDoc; }
+
+    public String getBuildTimeStamp() { return buildTimestamp; }
+
+    public String getBuildVersion() { return absoluteVersion; }
+
+    /**
+     * @return Boolean indicating whether to include @Hidden annotations in our documented output
+     */
+    public boolean showHiddenFeatures() { return showHiddenFeatures; }
+
+    /**
+     * @return the output extension to use, i.e., ".html" or ".php"
+     */
+    public String getOutputFileExtension() { return outputFileExtension; }
+
+    /**
+     * @return the output extension to use for the index, i.e., ".html" or ".php"
+     */
+    public String getIndexFileExtension() { return indexFileExtension; }
+
+    /**
+     * @return the name of the index template to be used for this doclet
+     */
+    public String getIndexTemplateName() { return "generic.index.html.ftl"; }
+
+    /**
+     * @return The base filename for the index file associated with this doclet.
+     */
+    public String getIndexBaseFileName() { return "index"; }
+
+    /**
+     * @return the file where the files will be output
+     */
+    public File getDestinationDir() { return  destinationDir; }
+
+    /**
+     * Determine if a particular class should be included in the output. This is called by the doclet
+     * to determine if a DocWorkUnit should be created for this feature.
+     *
+     * @param documentedFeature feature that is being considered for inclusion in the docs
+     * @param classDoc for the class that is being considered for inclusion in the docs
+     * @param clazz class that is being considered for inclusion in the docs
+     * @return true if the doc should be included, otherwise false
+     */
+    public boolean includeInDocs(final DocumentedFeature documentedFeature, final ClassDoc classDoc, final Class<?> clazz) {
+        boolean hidden = !showHiddenFeatures() && clazz.isAnnotationPresent(Hidden.class);
+        return !hidden && JVMUtils.isConcrete(clazz);
+    }
+
+    /**
+     * Actually write out the output files (html and gson file for each feature) and the index file.
+     */
+    private void emitOutputFromTemplates (
+            final List<Map<String, String>> groupMaps,
+            final List<Map<String, String>> featureMaps)
+    {
+        try {
+            /* ------------------------------------------------------------------- */
+            /* You should do this ONLY ONCE in the whole application life-cycle:   */
+            final Configuration cfg = new Configuration(Configuration.VERSION_2_3_23);
+            cfg.setObjectWrapper(new DefaultObjectWrapper(Configuration.VERSION_2_3_23));
+
+            // We need to set up a scheme to load our settings from wherever they may live.
+            // This means we need to set up a multi-loader including the classpath and any specified options:
+
+            TemplateLoader templateLoader;
+
+            // Only add the settings directory if we're supposed to:
+            if ( useDefaultTemplates ) {
+                templateLoader = new ClassTemplateLoader(getClass(), DEFAULT_SETTINGS_CLASSPATH);
+            }
+            else {
+                templateLoader = new FileTemplateLoader(new File(settingsDir.getPath()));
+            }
+
+            // Tell freemarker to load our templates as we specified above:
+            cfg.setTemplateLoader(templateLoader);
+
+            // Generate one template file for each work unit
+            workUnits.stream().forEach(workUnit -> processWorkUnitTemplate(cfg, workUnit, groupMaps, featureMaps));
+            processIndexTemplate(cfg, new ArrayList<>(workUnits), groupMaps);
+
+        } catch (FileNotFoundException e) {
+            throw new RuntimeException("FileNotFoundException processing javadoc template", e);
+        } catch (IOException e) {
+            throw new RuntimeException("IOException processing javadoc template", e);
+        }
+    }
+
+    /**
+     * Create a work unit and handler capable of documenting the feature specified by the input arguments.
+     * Returns null if no appropriate handler is found or doc shouldn't be documented at all.
+     */
+    protected DocWorkUnit createWorkUnit(
+            final DocumentedFeature documentedFeature,
+            final ClassDoc classDoc,
+            final Class<?> clazz)
+    {
+        return new DocWorkUnit(
+                new DefaultDocWorkUnitHandler(this),
+                documentedFeature,
+                classDoc,
+                clazz);
+    }
+
+    /**
+     * Returns the instantiated DocumentedFeature that describes the doc for this class.
+     *
+     * @param clazz
+     * @return DocumentedFeature, or null if this classDoc shouldn't be included/documented
+     */
+    private DocumentedFeature getDocumentedFeatureForClass(final Class<?> clazz) {
+        if (clazz != null && clazz.isAnnotationPresent(DocumentedFeature.class)) {
+            return clazz.getAnnotation(DocumentedFeature.class);
+        }
+        else {
+            return null;
+        }
+    }
+
+    /**
+     * Return the Java class described by the ClassDoc doc
+     *
+     * @param doc
+     * @return
+     */
+    private Class<? extends Object> getClassForClassDoc(final ClassDoc doc) {
+        try {
+            return DocletUtils.getClassForDoc(doc);
+        } catch (ClassNotFoundException e) {
+            // we got a classdoc for a class we can't find.  Maybe in a library or something
+            return null;
+        } catch (NoClassDefFoundError e) {
+            return null;
+        } catch (UnsatisfiedLinkError e) {
+            return null; // naughty BWA bindings
+        }
+    }
+
+    /**
+     * Create the php index listing all of the Docs features
+     *
+     * @param cfg
+     * @param workUnitList
+     * @param groupMaps
+     * @throws IOException
+     */
+    protected void processIndexTemplate(
+            final Configuration cfg,
+            final List<DocWorkUnit> workUnitList,
+            final List<Map<String, String>> groupMaps
+   ) throws IOException {
+        // Get or create a template and merge in the data
+        final Template template = cfg.getTemplate(getIndexTemplateName());
+
+        final File indexFile = new File(getDestinationDir(),
+                            getIndexBaseFileName() + '.' + getIndexFileExtension()
+        );
+
+        try (final FileOutputStream fileOutStream = new FileOutputStream(indexFile);
+             final OutputStreamWriter outWriter = new OutputStreamWriter(fileOutStream)) {
+            template.process(groupIndexMap(workUnitList, groupMaps), outWriter);
+        } catch (TemplateException e) {
+            throw new DocException("Freemarker Template Exception during documentation index creation", e);
+        }
+    }
+
+    /**
+     * Helpful function to create the php index.  Given all of the already run DocWorkUnits,
+     * create the high-level grouping data listing individual features by group.
+     *
+     * @param workUnitList
+     * @return The map used to populate the index template used by this doclet.
+     */
+    protected Map<String, Object> groupIndexMap(
+            final List<DocWorkUnit> workUnitList,
+            final List<Map<String, String>> groupMaps
+    ) {
+        //
+        // root -> data -> { summary -> y, filename -> z }, etc
+        //      -> groups -> group1, group2, etc.
+        Map<String, Object> root = new HashMap<>();
+
+        Collections.sort(workUnitList);
+
+        List<Map<String, String>> data = new ArrayList<>();
+        workUnitList.stream().forEach(workUnit -> data.add(indexDataMap(workUnit)));
+
+        root.put("data", data);
+        root.put("groups", groupMaps);
+        root.put("timestamp", getBuildTimeStamp());
+        root.put("version", getBuildVersion());
+
+        return root;
+    }
+
+    /**
+     * Helper routine that returns the map of group name and summary given the workUnit. Subclasses that
+     * override this should call this method before doing further processing.
+     *
+     * @param workUnit
+     * @return The property map for the work unit's entry in the index map for this doclet.
+     */
+    protected Map<String, String> getGroupMap(final DocWorkUnit workUnit) {
+        Map<String, String> propertyMap = new HashMap<>();
+        propertyMap.put("id", getGroupIdFromName(workUnit.getGroupName()));
+        propertyMap.put("name", workUnit.getGroupName());
+        propertyMap.put("summary", workUnit.getGroupSummary());
+        return propertyMap;
+    };
+
+    private String getGroupIdFromName(final String groupName) { return groupName.replaceAll("\\W", ""); }
+
+    /**
+     * Return a String -> String map suitable for FreeMarker to create an index to this WorkUnit
+     *
+     * @return
+     */
+    public Map<String, String> indexDataMap(final DocWorkUnit workUnit) {
+        Map<String, String> propertyMap = new HashMap<>();
+        propertyMap.put("name", workUnit.getName());
+        propertyMap.put("summary", workUnit.getSummary());
+        propertyMap.put("filename", workUnit.getTargetFileName());
+        propertyMap.put("group", workUnit.getGroupName());
+        propertyMap.put("beta", Boolean.toString(workUnit.getBetaFeature()));
+        return propertyMap;
+    }
+
+    /**
+     * Helper function that finding the DocWorkUnit associated with class from among all of the work units
+     *
+     * @param c the class we are looking for
+     * @return the DocWorkUnit whose .clazz.equals(c), or null if none could be found
+     */
+    public final DocWorkUnit findWorkUnitForClass(final Class<?> c) {
+        for (final DocWorkUnit workUnit : this.workUnits)
+            if (workUnit.getClazz().equals(c))
+                return workUnit;
+        return null;
+    }
+
+    /**
+     * Return the ClassDoc associated with clazz
+     *
+     * @param clazz
+     * @return
+     */
+    public ClassDoc getClassDocForClass(final Class<?> clazz) {
+        return rootDoc.classNamed(clazz.getName());
+    }
+
+    /**
+     * High-level function that processes a single DocWorkUnit unit using its handler
+     *
+     * @param cfg
+     * @param workUnit
+     * @param featureMaps
+     * @throws IOException
+     */
+    protected void processWorkUnitTemplate(
+            final Configuration cfg,
+            final DocWorkUnit workUnit,
+            final List<Map<String, String>> indexByGroupMaps,
+            final List<Map<String, String>> featureMaps)
+    {
+        try {
+            // Merge data-model with template
+            Template template = cfg.getTemplate(workUnit.getTemplateName());
+            File outputPath = new File(getDestinationDir(), workUnit.getTargetFileName());
+            try (final Writer out = new OutputStreamWriter(new FileOutputStream(outputPath))) {
+                template.process(workUnit.getRootMap(), out);
+            }
+        } catch (IOException e) {
+            throw new DocException("IOException during documentation creation", e);
+        } catch (TemplateException e) {
+            throw new DocException("TemplateException during documentation creation", e);
+        }
+
+        // Create GSON-friendly container object
+        GSONWorkUnit gsonworkunit = createGSONWorkUnit(workUnit, indexByGroupMaps, featureMaps);
+
+        gsonworkunit.populate(
+                workUnit.getProperty("summary").toString(),
+                workUnit.getProperty("gson-arguments"),
+                workUnit.getProperty("description").toString(),
+                workUnit.getProperty("name").toString(),
+                workUnit.getProperty("group").toString()
+        );
+
+        // Convert object to JSON and write JSON entry to file
+        File outputPathForJSON = new File(getDestinationDir(), workUnit.getJSONFileName());
+
+        try (final BufferedWriter jsonWriter = new BufferedWriter(new FileWriter(outputPathForJSON))) {
+            Gson gson = new GsonBuilder()
+                .serializeSpecialFloatingPointValues()
+                .setPrettyPrinting()
+                .create();
+            String json = gson.toJson(gsonworkunit);
+            jsonWriter.write(json);
+        } catch (IOException e) {
+            throw new DocException("Failed to create JSON entry", e);
+        }
+    }
+
+    /**
+     * Doclet implementations (subclasses) should return a GSONWorkUnit-derived object if the GSON objects
+     * for the DocumentedFeature needs to contain custom values.
+     * @return a GSONWorkUnit-derived object
+     */
+    protected GSONWorkUnit createGSONWorkUnit(
+            final DocWorkUnit workUnit,
+            final List<Map<String, String>> indexByGroupMaps,
+            final List<Map<String, String>> featureMaps)
+    {
+        return new GSONWorkUnit();
+    }
+
+}
diff --git a/src/main/java/org/broadinstitute/barclay/help/package-info.java b/src/main/java/org/broadinstitute/barclay/help/package-info.java
new file mode 100644
index 0000000..9e71db6
--- /dev/null
+++ b/src/main/java/org/broadinstitute/barclay/help/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * See https://github.com/broadinstitute/barclay/wiki/Using-the-Documentation-Generation-utilities-in-Barclay
+ */
+package org.broadinstitute.barclay.help;
diff --git a/src/main/java/org/broadinstitute/barclay/utils/JVMUtils.java b/src/main/java/org/broadinstitute/barclay/utils/JVMUtils.java
new file mode 100644
index 0000000..e2ce240
--- /dev/null
+++ b/src/main/java/org/broadinstitute/barclay/utils/JVMUtils.java
@@ -0,0 +1,39 @@
+package org.broadinstitute.barclay.utils;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+public class JVMUtils {
+    /**
+     * Is the specified class a concrete implementation of baseClass?
+     * @param clazz Class to check.
+     * @return True if clazz is concrete.  False otherwise.
+     */
+    public static boolean isConcrete( Class<?> clazz ) {
+        return !Modifier.isAbstract(clazz.getModifiers()) &&
+                !Modifier.isInterface(clazz.getModifiers());
+    }
+
+    /**
+     * Find the field with the given name in the class.  Will inspect all fields, independent
+     * of access level.
+     * @param type Class in which to search for the given field.
+     * @param fieldName Name of the field for which to search.
+     * @return The field, or null if no such field exists.
+     */
+    public static Field findField(Class<?> type, String fieldName ) {
+        while( type != null ) {
+            Field[] fields = type.getDeclaredFields();
+            for( Field field: fields ) {
+                if( field.getName().equals(fieldName) )
+                    return field;
+            }
+            type = type.getSuperclass();
+        }
+        return null;
+    }
+
+}
diff --git a/src/main/java/org/broadinstitute/barclay/utils/Utils.java b/src/main/java/org/broadinstitute/barclay/utils/Utils.java
new file mode 100644
index 0000000..4387f37
--- /dev/null
+++ b/src/main/java/org/broadinstitute/barclay/utils/Utils.java
@@ -0,0 +1,139 @@
+package org.broadinstitute.barclay.utils;
+
+import java.util.*;
+import java.util.function.Supplier;
+
+/**
+ * Utility classes used by the command line parsers.
+ */
+public class Utils {
+
+    private static final int TEXT_WARNING_WIDTH = 68;
+    private static final String TEXT_WARNING_PREFIX = "* ";
+    private static final String TEXT_WARNING_BORDER = dupString('*', TEXT_WARNING_PREFIX.length() + TEXT_WARNING_WIDTH);
+    private static final char ESCAPE_CHAR = '\u001B';
+    // ASCII codes for making text blink
+    public static final String TEXT_BLINK = ESCAPE_CHAR + "[5m";
+    public static final String TEXT_RESET = ESCAPE_CHAR + "[m";
+
+    /**
+     * Checks that an Object {@code object} is not null and returns the same object or throws an {@link IllegalArgumentException}
+     * @param object any Object
+     * @return the same object
+     * @throws IllegalArgumentException if a {@code o == null}
+     */
+    public static <T> T nonNull(final T object) {
+        return Utils.nonNull(object, "Null object is not allowed here.");
+    }
+
+    /**
+     * Checks that an {@link Object} is not {@code null} and returns the same object or throws an {@link IllegalArgumentException}
+     * @param object any Object
+     * @param message the text message that would be passed to the exception thrown when {@code o == null}.
+     * @return the same object
+     * @throws IllegalArgumentException if a {@code o == null}
+     */
+    public static <T> T nonNull(final T object, final String message) {
+        if (object == null) {
+            throw new IllegalArgumentException(message);
+        }
+        return object;
+    }
+
+    /**
+     * Checks that an {@link Object} is not {@code null} and returns the same object or throws an {@link IllegalArgumentException}
+     * @param object any Object
+     * @param message the text message that would be passed to the exception thrown when {@code o == null}.
+     * @return the same object
+     * @throws IllegalArgumentException if a {@code o == null}
+     */
+    public static <T> T nonNull(final T object, final Supplier<String> message) {
+        if (object == null) {
+            throw new IllegalArgumentException(message.get());
+        }
+        return object;
+    }
+
+    public static List<String> warnUserLines(final String msg) {
+        List<String> results = new ArrayList<>();
+        results.add(String.format(TEXT_WARNING_BORDER));
+        results.add(String.format(TEXT_WARNING_PREFIX + "WARNING:"));
+        results.add(String.format(TEXT_WARNING_PREFIX));
+        prettyPrintWarningMessage(results, msg);
+        results.add(String.format(TEXT_WARNING_BORDER));
+        return results;
+    }
+
+    /**
+     * pretty print the warning message supplied
+     *
+     * @param results the pretty printed message
+     * @param message the message
+     */
+    private static void prettyPrintWarningMessage(final List<String> results, final String message) {
+        for (final String line: message.split("\\r?\\n")) {
+            final StringBuilder builder = new StringBuilder(line);
+            while (builder.length() > TEXT_WARNING_WIDTH) {
+                int space = getLastSpace(builder, TEXT_WARNING_WIDTH);
+                if (space <= 0) space = TEXT_WARNING_WIDTH;
+                results.add(String.format("%s%s", TEXT_WARNING_PREFIX, builder.substring(0, space)));
+                builder.delete(0, space + 1);
+            }
+            results.add(String.format("%s%s", TEXT_WARNING_PREFIX, builder));
+        }
+    }
+
+    /**
+     * Returns the last whitespace location in string, before width characters.
+     * @param message The message to break.
+     * @param width The width of the line.
+     * @return The last whitespace location.
+     */
+    private static int getLastSpace(final CharSequence message, int width) {
+        final int length = message.length();
+        int stopPos = width;
+        int currPos = 0;
+        int lastSpace = -1;
+        boolean inEscape = false;
+        while (currPos < stopPos && currPos < length) {
+            final char c = message.charAt(currPos);
+            if (c == ESCAPE_CHAR) {
+                stopPos++;
+                inEscape = true;
+            } else if (inEscape) {
+                stopPos++;
+                if (Character.isLetter(c))
+                    inEscape = false;
+            } else if (Character.isWhitespace(c)) {
+                lastSpace = currPos;
+            }
+            currPos++;
+        }
+        return lastSpace;
+    }
+
+    /**
+     * Create a new string thats a n duplicate copies of c
+     * @param c the char to duplicate
+     * @param nCopies how many copies?
+     * @return a string
+     */
+    public static String dupString(char c, int nCopies) {
+        char[] chars = new char[nCopies];
+        Arrays.fill(chars, c);
+        return new String(chars);
+    }
+
+    /**
+     * Compares two objects, either of which might be null.
+     *
+     * @param lhs One object to compare.
+     * @param rhs The other object to compare.
+     *
+     * @return True if the two objects are equal, false otherwise.
+     */
+    public static boolean equals(Object lhs, Object rhs) {
+        return lhs == null && rhs == null || lhs != null && lhs.equals(rhs);
+    }
+
+}
diff --git a/src/main/resources/org/broadinstitute/barclay/helpTemplates/bash-completion.ftl b/src/main/resources/org/broadinstitute/barclay/helpTemplates/bash-completion.ftl
new file mode 100644
index 0000000..f47f854
--- /dev/null
+++ b/src/main/resources/org/broadinstitute/barclay/helpTemplates/bash-completion.ftl
@@ -0,0 +1,451 @@
+
+####################
+# Tab completion file to allow for easy use of this tool with the command-line using Bash.
+####################
+
+<#include "bash-completion.macros.ftl"/>
+
+####################################################################################################
+
+# High-level caller/dispatch script information:
+
+CALLER_SCRIPT_NAME="${callerScriptOptions["callerScriptName"]}"
+
+# A description of these variables is below in the main completion function (_masterCompletionFunction)
+CS_PREFIX_OPTIONS_ALL_LEGAL_ARGUMENTS=(${callerScriptOptions["callerScriptPrefixLegalArgs"]} <@compress_single_line><@emitToolListForTopLevelComplete tools=tools /></@compress_single_line>)
+CS_PREFIX_OPTIONS_NORMAL_COMPLETION_ARGUMENTS=(${callerScriptOptions["callerScriptPrefixLegalArgs"]} <@compress_single_line><@emitToolListForTopLevelComplete tools=tools /></@compress_single_line>)
+CS_PREFIX_OPTIONS_ALL_ARGUMENT_VALUE_TYPES=(${callerScriptOptions["callerScriptPrefixArgValueTypes"]} <@compress_single_line><@emitStringsForToolList tools=tools repeatString="\"null\""/></@compress_single_line>)
+CS_PREFIX_OPTIONS_MUTUALLY_EXCLUSIVE_ARGS=(${callerScriptOptions["callerScriptPrefixMutexArgs"]})
+CS_PREFIX_OPTIONS_SYNONYMOUS_ARGS=(${callerScriptOptions["callerScriptPrefixAliasArgs"]})
+CS_PREFIX_OPTIONS_MIN_OCCURRENCES=(${callerScriptOptions["callerScriptPrefixMinOccurrences"]} <@compress_single_line><@emitStringsForToolList tools=tools repeatString="0"/></@compress_single_line>)
+CS_PREFIX_OPTIONS_MAX_OCCURRENCES=(${callerScriptOptions["callerScriptPrefixMaxOccurrences"]} <@compress_single_line><@emitStringsForToolList tools=tools repeatString="1"/></@compress_single_line>)
+
+CS_POSTFIX_OPTIONS_ALL_LEGAL_ARGUMENTS=(${callerScriptOptions["callerScriptPostfixLegalArgs"]})
+CS_POSTFIX_OPTIONS_NORMAL_COMPLETION_ARGUMENTS=(${callerScriptOptions["callerScriptPostfixLegalArgs"]})
+CS_POSTFIX_OPTIONS_ALL_ARGUMENT_VALUE_TYPES=(${callerScriptOptions["callerScriptPostfixArgValueTypes"]})
+CS_POSTFIX_OPTIONS_MUTUALLY_EXCLUSIVE_ARGS=(${callerScriptOptions["callerScriptPostfixMutexArgs"]})
+CS_POSTFIX_OPTIONS_SYNONYMOUS_ARGS=(${callerScriptOptions["callerScriptPostfixAliasArgs"]})
+CS_POSTFIX_OPTIONS_MIN_OCCURRENCES=(${callerScriptOptions["callerScriptPostfixMinOccurrences"]})
+CS_POSTFIX_OPTIONS_MAX_OCCURRENCES=(${callerScriptOptions["callerScriptPostfixMaxOccurrences"]})
+
+# Whether we have to worry about these extra script options at all.
+HAS_POSTFIX_OPTIONS="${callerScriptOptions["hasCallerScriptPostfixArgs"]}"
+
+# All the tool names we are able to complete:
+ALL_TOOLS=(<@compress_single_line><@emitToolListForTopLevelComplete tools=tools /></@compress_single_line>)
+
+####################################################################################################
+
+# Get the name of the tool that we're currently trying to call
+_${callerScriptOptions["callerScriptName"]}_getToolName()
+{
+    # Naively go through each word in the line until we find one that is in our list of tools:
+    for word in ${r"${COMP_WORDS[@]}"} ; do
+        if ( echo " ${r"${ALL_TOOLS[@]}"} " | grep -q " ${r"${word}"} " ) ; then
+            echo "${r"${word}"}"
+            break
+        fi
+    done
+}
+
+# Get the index of the toolname inside COMP_WORDS
+_${callerScriptOptions["callerScriptName"]}_getToolNameIndex()
+{
+    # Naively go through each word in the line until we find one that is in our list of tools:
+    local ctr=0
+    for word in ${r"${COMP_WORDS[@]}"} ; do
+        if ( echo " ${r"${ALL_TOOLS[@]}"} " | grep -q " ${r"${word}"} " ) ; then
+            echo $ctr
+            break
+        fi
+        let ctr=$ctr+1
+    done
+}
+
+# Get all possible tool names for the current command line if the current command is a
+# complete command on its own already.
+# If there is no complete command yet, then this prints nothing.
+_${callerScriptOptions["callerScriptName"]}_getAllPossibleToolNames()
+{
+# We want to return a list of possible tool names if and only if
+# the current word is a valid complete tool name
+# AND
+# the current word is also a substring in more than one tool name
+
+    local tool count matches toolList
+
+    let count=0
+    matches=false
+    toolList=()
+
+    # Go through tool names and get what matches and partial matches we have:
+    for tool in ${r"${ALL_TOOLS[@]}"} ; do
+        if [[ "${r"${COMP_WORDS[COMP_CWORD]}"}" == "${r"${tool}"}" ]] ; then
+            matches=true
+            let count=$count+1
+            ${r"toolList+=($tool)"}
+        elif [[ "${r"${tool}"}" == "${r"${COMP_WORDS[COMP_CWORD]}"}"* ]] ; then
+            ${r"toolList+=($tool)"}
+        fi
+    done
+
+    # If we have a complete match, then we print out our partial matches as a space separated string.
+    # That way we have a list of all possible full completions for this match.
+    # For instance, if there was a tool named "read" and another named "readBetter" this would get both.
+    if $matches ; then
+        echo "${r"${toolList[@]}"}"
+    fi
+}
+
+# Gets how many dependent arguments we have left to fill
+_${callerScriptOptions["callerScriptName"]}_getDependentArgumentCount()
+{
+    local depArgCount=0
+
+    for word in ${r"${COMP_LINE}"} ; do
+        for depArg in ${r"${DEPENDENT_ARGUMENTS[@]}"} ; do
+            if [[ "${r"${word}"}" == "${r"${depArg}"}" ]] ; then
+                $((depArgCount++))
+            fi
+        done
+    done
+
+    echo ${r"$depArgCount"}
+}
+
+# Resolves the given argument name to its long (normal) name
+_${callerScriptOptions["callerScriptName"]}_resolveVarName()
+{
+    local argName=$1
+    if [[ "${r"${SYNONYMOUS_ARGS[@]}"}" == *"${r"${argName}"}"* ]] ; then
+        echo "${r"${SYNONYMOUS_ARGS[@]}"}" | sed -e "s#.* \\([a-zA-Z0-9;,_\\-]*${r"${argName}"}[a-zA-Z0-9,;_\\-]*\\).*#\\1#g" -e 's#;.*##g'
+    else
+        echo "${r"${argName}"}"
+    fi
+}
+
+# Checks if we need to complete the VALUE for an argument.
+# Prints the index in the given argument list of the corresponding argument whose value we must complete.
+# Takes as input 1 positional argument: the name of the last argument given to this script
+# Otherwise prints -1
+_${callerScriptOptions["callerScriptName"]}_needToCompleteArgValue()
+{
+    if [[ "${r"${prev}"}" != "--" ]] ; then
+        local resolved=$( _${callerScriptOptions["callerScriptName"]}_resolveVarName ${r"${prev}"} )
+
+        ${r"for (( i=0 ; i < ${#ALL_LEGAL_ARGUMENTS[@]} ; i++ )) ; do"}
+            if [[ "${r"${resolved}"}" == "${r"${ALL_LEGAL_ARGUMENTS[i]}"}" ]] ; then
+
+                # Make sure the argument isn't one that takes no additional value
+                # such as a flag.
+                if [[ "${r"${ALL_ARGUMENT_VALUE_TYPES[i]}"}" != "null" ]] ; then
+                    echo "$i"
+                else
+                    echo "-1"
+                fi
+                return 0
+            fi
+        done
+    fi
+
+    echo "-1"
+}
+
+# Get the completion word list for the given argument type.
+# Prints the completion string to the screen
+_${callerScriptOptions["callerScriptName"]}_getCompletionWordList()
+{
+    # Normalize the type string so it's easier to deal with:
+    local argType=$( echo $1 | tr '[A-Z]' '[a-z]')
+
+    local isNumeric=false
+    local isFloating=false
+
+    local completionType=""
+
+    [[ "${r"${argType}"}" == *"file"* ]]      && completionType='-A file'
+    [[ "${r"${argType}"}" == *"folder"* ]]    && completionType='-A directory'
+    [[ "${r"${argType}"}" == *"directory"* ]] && completionType='-A directory'
+    [[ "${r"${argType}"}" == *"boolean"* ]]   && completionType='-W true false'
+
+    [[ "${r"${argType}"}" == "int" ]]         && completionType='-W 0 1 2 3 4 5 6 7 8 9'   && isNumeric=true
+    [[ "${r"${argType}"}" == *"[int]"* ]]     && completionType='-W 0 1 2 3 4 5 6 7 8 9'   && isNumeric=true
+    [[ "${r"${argType}"}" == "long" ]]        && completionType='-W 0 1 2 3 4 5 6 7 8 9'   && isNumeric=true
+    [[ "${r"${argType}"}" == *"[long]"* ]]    && completionType='-W 0 1 2 3 4 5 6 7 8 9'   && isNumeric=true
+
+    [[ "${r"${argType}"}" == "double" ]]      && completionType='-W . 0 1 2 3 4 5 6 7 8 9' && isNumeric=true && isFloating=true
+    [[ "${r"${argType}"}" == *"[double]"* ]]  && completionType='-W . 0 1 2 3 4 5 6 7 8 9' && isNumeric=true && isFloating=true
+    [[ "${r"${argType}"}" == "float" ]]       && completionType='-W . 0 1 2 3 4 5 6 7 8 9' && isNumeric=true && isFloating=true
+    [[ "${r"${argType}"}" == *"[float]"* ]]   && completionType='-W . 0 1 2 3 4 5 6 7 8 9' && isNumeric=true && isFloating=true
+
+    # If we have a number, we need to prepend the current completion to it so that we can continue to tab complete:
+    if $isNumeric ; then
+        completionType=$( echo ${r"${completionType}"} | sed -e "s#\([0-9]\)#$cur\1#g" )
+
+        # If we're floating point, we need to make sure we don't complete a `.` character
+        # if one already exists in our number:
+        if $isFloating ; then
+            echo "$cur" | grep -o '\.' &> /dev/null
+            local r=$?
+
+            [[ $r -eq 0 ]] && completionType=$( echo ${r"${completionType}"} | awk '{$2="" ; print}' )
+        fi
+    fi
+
+    echo "${r"${completionType}"}"
+}
+
+# Function to handle the completion tasks once we have populated our arg variables
+# When passed an argument handles the case for the caller script.
+_${callerScriptOptions["callerScriptName"]}_handleArgs()
+{
+    # Argument offset index is used in the special case where we are past the " -- " delimiter.
+    local argOffsetIndex=0
+
+    # We handle the beginning differently if this function was called with an argument
+    if [[ $# -eq 0 ]] ; then
+        # Get the number of arguments we have input so far:
+        local toolNameIndex=$(_${callerScriptOptions["callerScriptName"]}_getToolNameIndex)
+        local numArgs=$((COMP_CWORD-toolNameIndex-1))
+
+        # Now we check to see what kind of argument we are on right now
+        # We handle each type separately by order of precedence:
+        ${r"if [[ ${numArgs} -lt ${NUM_POSITIONAL_ARGUMENTS} ]] ; then"}
+            # We must complete a positional argument.
+            # Assume that positional arguments are all FILES:
+            COMPREPLY=( ${r"$(compgen -A file -- $cur"}) )
+            return 0
+        fi
+
+        # Dependent arguments must come right after positional arguments
+        # We must check to see how many dependent arguments we've gotten so far:
+        local numDepArgs=${r"$"}( _${callerScriptOptions["callerScriptName"]}_getDependentArgumentCount )
+
+        ${r"if [[ $numDepArgs -lt ${#DEPENDENT_ARGUMENTS[@]} ]] ; then"}
+            # We must complete a dependent argument next.
+            COMPREPLY=( ${r"$(compgen -W '${DEPENDENT_ARGUMENTS[@]}' -- $cur"}) )
+            return 0
+        fi
+    elif [[ "${r"${1}"}" == "POSTFIX_OPTIONS" ]] ; then
+        # Get the index of the special delimiter.
+        # we ignore everything up to and including it.
+        for (( i=0; i < COMP_CWORD ; i++ )) ; do
+            if [[ "${r"${COMP_WORDS[i]}"}" == "--" ]] ; then
+                let argOffsetIndex=$i+1
+            fi
+        done
+    fi
+    # NOTE: We don't need to worry about the prefix options case.
+    #       The caller will specify it and it skips the two special cases above.
+
+    # First we must resolve all arguments to their full names
+    # This is necessary to save time later because of short argument names / synonyms
+    local resolvedArgList=()
+    for (( i=argOffsetIndex ; i < COMP_CWORD ; i++ )) ; do
+        prevArg=${r"${COMP_WORDS[i]}"}
+
+        # Skip the current word to be completed:
+        [[ "${r"${prevArg}"}" == "${r"${cur}"}" ]] && continue
+
+        # Check if this has synonyms:
+        if [[ "${r"${SYNONYMOUS_ARGS[@]}"}" == *"${r"${prevArg}"}"* ]] ; then
+
+            local resolvedArg=$( _${callerScriptOptions["callerScriptName"]}_resolveVarName "${r"${prevArg}"}" )
+            ${r"resolvedArgList+=($resolvedArg)"}
+
+        # Make sure this is an argument:
+        elif [[ "${r"${ALL_LEGAL_ARGUMENTS[@]}"}" == *"${r"${prevArg}"}"* ]] ; then
+            ${r"resolvedArgList+=($prevArg)"}
+        fi
+    done
+
+    # Check to see if the last thing we typed was a complete argument.
+    # If so, we must complete the VALUE for the argument, not the
+    # argument itself:
+    # Note: This is shorthand for last element in the array:
+    local argToComplete=$( _${callerScriptOptions["callerScriptName"]}_needToCompleteArgValue )
+
+    if [[ $argToComplete -ne -1 ]] ; then
+        # We must complete the VALUE for an argument.
+
+        # Get the argument type.
+        local valueType=${r"${ALL_ARGUMENT_VALUE_TYPES[argToComplete]}"}
+
+        # Get the correct completion string for the type:
+        local completionString=$( _${callerScriptOptions["callerScriptName"]}_getCompletionWordList "${r"${valueType}"}" )
+
+        if [[ ${r"${#completionString}"} -eq 0 ]] ; then
+            # We don't have any information on the type to complete.
+            # We use the default SHELL behavior:
+            COMPREPLY=()
+        else
+            # We have a completion option.  Let's plug it in:
+            local compOperator=$( echo "${r"${completionString}"}" | awk '{print $1}' )
+            local compOptions=$( echo "${r"${completionString}"}" | awk '{$1="" ; print}' )
+
+            case ${r"${compOperator}"} in
+                -A) COMPREPLY=( ${r"$(compgen -A ${compOptions} -- $cur"}) ) ;;
+                -W) COMPREPLY=( ${r"$(compgen -W '${compOptions}' -- $cur"}) ) ;;
+                 *) COMPREPLY=() ;;
+            esac
+
+        fi
+        return 0
+    fi
+
+    # We must create a list of the valid remaining arguments:
+
+    # Create a list of all arguments that are
+    # mutually exclusive with arguments we have already specified
+    local mutex_list=""
+    for prevArg in ${r"${resolvedArgList[@]}"} ; do
+        if [[ "${r"${MUTUALLY_EXCLUSIVE_ARGS[@]}"}" == *"${r"${prevArg}"};"* ]] ; then
+            local mutexArgs=$( echo "${r"${MUTUALLY_EXCLUSIVE_ARGS[@]}"}" | sed -e "s#.*${r"${prevArg}"};\([a-zA-Z0-9_,\-]*\) .*#\1#g" -e "s#,# --#g" -e "s#^#--#g" )
+            mutex_list="${r"${mutex_list}${mutexArgs}"}"
+        fi
+    done
+
+    local remaining_legal_arguments=()
+    for (( i=0; i < ${r"${#NORMAL_COMPLETION_ARGUMENTS[@]}"} ; i++ )) ; do
+        local legalArg=${r"${NORMAL_COMPLETION_ARGUMENTS[i]}"}
+        local okToAdd=true
+
+        # Get the number of times this has occurred in the arguments already:
+        local numPrevOccurred=$( grep -o -- "${r"${legalArg}"}" <<< "${r"${resolvedArgList[@]}"}" | wc -l | awk '{print $1}' )
+
+        if [[ $numPrevOccurred -lt "${r"${MAX_OCCURRENCES[i]}"}" ]] ; then
+
+            # Make sure this arg isn't mutually exclusive to another argument that we've already had:
+            if [[ "${r"${mutex_list}"}" ==    "${r"${legalArg}"} "* ]] ||
+               [[ "${r"${mutex_list}"}" ==  *" ${r"${legalArg}"} "* ]] ||
+               [[ "${r"${mutex_list}"}" ==  *" ${r"${legalArg}"}"  ]] ; then
+                okToAdd=false
+            fi
+
+            # Check if we're still good to add in the argument:
+            if $okToAdd ; then
+                # Add in the argument:
+                ${r"remaining_legal_arguments+=($legalArg)"}
+
+                # Add in the synonyms of the argument:
+                if [[ "${r"${SYNONYMOUS_ARGS[@]}"}" == *"${r"${legalArg}"}"* ]] ; then
+                    local synonymString=$( echo "${r"${SYNONYMOUS_ARGS[@]}"}" | sed -e "s#.*${r"${legalArg}"};\([a-zA-Z0-9_,\-]*\).*#\1#g" -e "s#,# #g"  )
+                    ${r"remaining_legal_arguments+=($synonymString)"}
+                fi
+            fi
+        fi
+
+    done
+
+    # Add in the special option "--" which separates tool options from meta-options if they're necessary:
+    if $HAS_POSTFIX_OPTIONS ; then
+        if [[ $# -eq 0 ]] || [[ "${r"${1}"}" == "PREFIX_OPTIONS"  ]] ; then
+            remaining_legal_arguments+=("--")
+        fi
+    fi
+
+    COMPREPLY=( ${r"$(compgen -W '${remaining_legal_arguments[@]}' -- $cur"}) )
+    return 0
+}
+
+####################################################################################################
+
+_${callerScriptOptions["callerScriptName"]}_masterCompletionFunction()
+{
+    # Set up global variables for the functions that do completion:
+    prev=${r"${COMP_WORDS[COMP_CWORD-1]}"}
+    cur=${r"${COMP_WORDS[COMP_CWORD]}"}
+
+    # How many positional arguments a tool will have.
+    # These positional arguments must come directly after a tool name.
+    NUM_POSITIONAL_ARGUMENTS=0
+
+    # The types of the positional arguments, in the order in which they must be specified
+    # on the command-line.
+    POSITIONAL_ARGUMENT_TYPE=()
+
+    # The set of legal arguments that aren't dependent arguments.
+    # (A dependent argument is an argument that must occur immediately after
+    # all positional arguments.)
+    NORMAL_COMPLETION_ARGUMENTS=()
+
+    # The set of ALL legal arguments
+    # Corresponds by index to the type of those arguments in ALL_ARGUMENT_VALUE_TYPES
+    ALL_LEGAL_ARGUMENTS=()
+
+    # The types of ALL legal arguments
+    # Corresponds by index to the names of those arguments in ALL_LEGAL_ARGUMENTS
+    ALL_ARGUMENT_VALUE_TYPES=()
+
+    # Arguments that are mutually exclusive.
+    # These are listed here as arguments concatenated together with delimiters:
+    # ${r"<"}Main argument${r">"};${r"<"}Mutex Argument 1${r">"}[,${r"<"}Mutex Argument 2${r">"},...]
+    MUTUALLY_EXCLUSIVE_ARGS=()
+
+    # Alternate names of arguments.
+    # These are listed here as arguments concatenated together with delimiters.
+    # ${r"<"}Main argument${r">"};${r"<"}Synonym Argument 1${r">"}[,${r"<"}Synonym Argument 2${r">"},...]
+    SYNONYMOUS_ARGS=()
+
+    # The minimum number of times an argument can occur.
+    MIN_OCCURRENCES=()
+
+    # The maximum number of times an argument can occur.
+    MAX_OCCURRENCES=()
+
+    # Set up locals for this function:
+    local toolName=$( _${callerScriptOptions["callerScriptName"]}_getToolName )
+
+    # Get possible tool matches:
+    local possibleToolMatches=$( _${callerScriptOptions["callerScriptName"]}_getAllPossibleToolNames )
+
+    # Check if we have postfix options
+    # and if we now need to go through them:
+    if $HAS_POSTFIX_OPTIONS && [[ "${r"${COMP_WORDS[@]}"}" == *" -- "* ]] ; then
+        NUM_POSITIONAL_ARGUMENTS=0
+        POSITIONAL_ARGUMENT_TYPE=()
+        DEPENDENT_ARGUMENTS=()
+        NORMAL_COMPLETION_ARGUMENTS=("${r"${CS_POSTFIX_OPTIONS_NORMAL_COMPLETION_ARGUMENTS[@]}"}")
+        MUTUALLY_EXCLUSIVE_ARGS=("${r"${CS_POSTFIX_OPTIONS_MUTUALLY_EXCLUSIVE_ARGS[@]}"}")
+        SYNONYMOUS_ARGS=("${r"${CS_POSTFIX_OPTIONS_SYNONYMOUS_ARGS[@]}"}")
+        MIN_OCCURRENCES=("${r"${CS_POSTFIX_OPTIONS_MIN_OCCURRENCES[@]}"}")
+        MAX_OCCURRENCES=("${r"${CS_POSTFIX_OPTIONS_MAX_OCCURRENCES[@]}"}")
+        ALL_LEGAL_ARGUMENTS=("${r"${CS_POSTFIX_OPTIONS_ALL_LEGAL_ARGUMENTS[@]}"}")
+        ALL_ARGUMENT_VALUE_TYPES=("${r"${CS_POSTFIX_OPTIONS_ALL_ARGUMENT_VALUE_TYPES[@]}"}")
+
+        # Complete the arguments for the base script:
+        # Strictly speaking, what the argument to this function is doesn't matter.
+        _${callerScriptOptions["callerScriptName"]}_handleArgs POSTFIX_OPTIONS
+
+    # Check if we have a complete tool match that may match more than one tool:
+    elif [[ ${r"${#possibleToolMatches}"} -ne 0 ]] ; then
+
+        # Set our reply as a list of the possible tool matches:
+        COMPREPLY=( ${r"$(compgen -W '${possibleToolMatches[@]}' -- $cur"}) )
+
+<@emitGroupToolCheckConditional tools=tools/>
+
+    # We have no postfix options or tool options.
+    # We now must complete any prefix options and the tools themselves.
+    # These are defined at the top.
+    else
+        NUM_POSITIONAL_ARGUMENTS=0
+        POSITIONAL_ARGUMENT_TYPE=()
+        DEPENDENT_ARGUMENTS=()
+        NORMAL_COMPLETION_ARGUMENTS=("${r"${CS_PREFIX_OPTIONS_NORMAL_COMPLETION_ARGUMENTS[@]}"}")
+        MUTUALLY_EXCLUSIVE_ARGS=("${r"${CS_PREFIX_OPTIONS_MUTUALLY_EXCLUSIVE_ARGS[@]}"}")
+        SYNONYMOUS_ARGS=("${r"${CS_PREFIX_OPTIONS_SYNONYMOUS_ARGS[@]}"}")
+        MIN_OCCURRENCES=("${r"${CS_PREFIX_OPTIONS_MIN_OCCURRENCES[@]}"}")
+        MAX_OCCURRENCES=("${r"${CS_PREFIX_OPTIONS_MAX_OCCURRENCES[@]}"}")
+        ALL_LEGAL_ARGUMENTS=("${r"${CS_PREFIX_OPTIONS_ALL_LEGAL_ARGUMENTS[@]}"}")
+        ALL_ARGUMENT_VALUE_TYPES=("${r"${CS_PREFIX_OPTIONS_ALL_ARGUMENT_VALUE_TYPES[@]}"}")
+
+        # Complete the arguments for the prefix arguments and tools:
+        _${callerScriptOptions["callerScriptName"]}_handleArgs PREFIX_OPTIONS
+    fi
+}
+
+${r"complete -o default -F _"}${callerScriptOptions["callerScriptName"]}${r"_masterCompletionFunction ${CALLER_SCRIPT_NAME}"}
+
+
+
diff --git a/src/main/resources/org/broadinstitute/barclay/helpTemplates/bash-completion.macros.ftl b/src/main/resources/org/broadinstitute/barclay/helpTemplates/bash-completion.macros.ftl
new file mode 100644
index 0000000..7f5ea67
--- /dev/null
+++ b/src/main/resources/org/broadinstitute/barclay/helpTemplates/bash-completion.macros.ftl
@@ -0,0 +1,119 @@
+<#-- Removes all occurrences of delim between the two nested tags -->
+<#macro removeDelimiter delim><#local captured><#nested></#local>${ captured?replace(delim, "", "rm") }</#macro>
+
+<#-- Compress all text between opening and closing compress_single_line tags into a single line. -->
+<#macro compress_single_line><#local captured><#nested></#local>${ captured?replace("\\n|\\r", "", "rm") }</#macro>
+
+<#-- Print out the names of all arguments of the given type from the given argument map. -->
+<#macro printArgNames argumentMap argType >
+    <#list argumentMap[argType]?sort_by("name") as args>
+${args["name"]} <#nt>
+    </#list>
+</#macro>
+
+<#-- Print out the types of all arguments of the given type from the given argument map. -->
+<#macro printArgTypes argumentMap argType >
+    <#list argumentMap[argType]?sort_by("name") as args>
+"${args["type"]}" <#nt>
+    </#list>
+</#macro>
+
+<#-- Print out the given field from the list of all argument types in the given argument map. -->
+<#macro printArgFieldList argumentMap fieldName>
+    <#list ["required", "common", "optional", "advanced", "deprecated"] as argType >
+        <#if argumentMap[argType]?size gt 0>
+            <#list argumentMap[argType]?sort_by("name") as args>
+                <#if args[fieldName]?length gt 0 >
+${args[fieldName]} <#nt>
+                </#if>
+            </#list>
+        </#if>
+    </#list>
+</#macro>
+
+<#-- Print out a string of of argument sets that are mutually exclusive.
+ Argument sets are separated by spaces.
+ The argument itself is first in the list and separated between the mutex
+ arguments with a semicolon.
+ The mutex arguments are comma separated.-->
+<#macro printDelimitedArgSet argumentMap argName>
+    <#list ["required", "common", "optional", "advanced", "deprecated"] as argType >
+        <#if argumentMap[argType]?size gt 0>
+            <#list argumentMap[argType]?sort_by("name") as args>
+                <#if args[argName]?length gt 0 >
+                    <#if args[argName] != "NA" >
+"${args["name"]};${args[argName]?replace(" ", "")}" <#nt>
+                    </#if>
+                </#if>
+            </#list>
+        </#if>
+    </#list>
+</#macro>
+
+<#-- Print the minimum occurrences of the arguments in the given map -->
+<#macro printMinOccurrences argumentMap>
+    <@printArgFieldList argumentMap "minElements" />
+</#macro>
+
+<#macro emitGroupToolCheckConditional tools>
+    <#list tools?keys as toolName>
+    elif ${r"[[ ${toolName}"} == "${toolName}" ${r"]]"} ; then
+
+        <#assign arguments = tools[toolName].arguments>
+        # Set up the completion information for this tool:
+        <#if arguments["positional"]?size gt 0 >
+        <#-- We know that there will only be one positional argument in the list because of how they are declared: -->
+        NUM_POSITIONAL_ARGUMENTS=${arguments["positional"]?first["minElements"]}
+        POSITIONAL_ARGUMENT_TYPE=("${arguments["positional"]?first["type"]}")
+        </#if>
+        <@compress_single_line>
+        DEPENDENT_ARGUMENTS=(<@printArgNames arguments "dependent" />)
+        </@compress_single_line>
+
+        <@compress_single_line>
+        NORMAL_COMPLETION_ARGUMENTS=(<@printArgFieldList arguments "name"/>)
+        </@compress_single_line>
+
+        <@compress_single_line>
+        MUTUALLY_EXCLUSIVE_ARGS=(<@printDelimitedArgSet arguments "exclusiveOf"/>)
+        </@compress_single_line>
+
+        <@compress_single_line>
+        SYNONYMOUS_ARGS=(<@printDelimitedArgSet arguments "synonyms"/>)
+        </@compress_single_line>
+
+        <@compress_single_line>
+        MIN_OCCURRENCES=(<@removeDelimiter ","><@printArgFieldList arguments "minElements"/></@removeDelimiter>)
+        </@compress_single_line>
+
+        <@compress_single_line>
+        MAX_OCCURRENCES=(<@removeDelimiter ","><@printArgFieldList arguments "maxElements"/></@removeDelimiter>)
+        </@compress_single_line>
+
+        <@compress_single_line>
+        ALL_LEGAL_ARGUMENTS=(<#list ["required", "common", "optional", "dependent", "advanced", "deprecated"] as argType><@printArgNames arguments argType/></#list>)
+        </@compress_single_line>
+
+        <@compress_single_line>
+        ALL_ARGUMENT_VALUE_TYPES=(<#list ["required", "common", "optional", "dependent", "advanced", "deprecated"] as argType><@printArgTypes arguments argType/></#list>)
+        </@compress_single_line>
+
+
+        # Complete the arguments for this tool:
+        _${callerScriptOptions["callerScriptName"]}_handleArgs
+    </#list>
+</#macro>
+
+<#-- Print out the list of all tools. -->
+<#macro emitToolListForTopLevelComplete tools>
+    <#list tools?keys as toolName>
+${toolName} <#nt>
+    </#list>
+</#macro>
+
+<#-- Print out repeatString for each tool in the list of all tools. -->
+<#macro emitStringsForToolList tools repeatString>
+    <#list tools?keys as toolName>
+${repeatString} <#nt>
+    </#list>
+</#macro>
\ No newline at end of file
diff --git a/src/main/resources/org/broadinstitute/barclay/helpTemplates/common.html.ftl b/src/main/resources/org/broadinstitute/barclay/helpTemplates/common.html.ftl
new file mode 100644
index 0000000..676f5c5
--- /dev/null
+++ b/src/main/resources/org/broadinstitute/barclay/helpTemplates/common.html.ftl
@@ -0,0 +1,47 @@
+<#--
+        This file contains part of the theming used to present Barclay docs on a website. Styling is separated
+        out, so pages will be minimalistic html unless replacement styling is provided.
+        -->
+
+    <#macro footerInfo>
+        <hr>
+        <p><a href='#top'><i class='fa fa-chevron-up'></i> Return to top</a></p>
+        <hr>
+        <p class="version">Barclay version ${version} built at ${timestamp}.
+        <#-- closing P tag in next macro -->
+    </#macro>
+    
+    <#macro footerClose>
+    	<#-- ugly little hack to enable adding tool-specific info inline -->
+        </p>
+    </#macro>
+
+    <#macro getCategories groups>
+        <style>
+            #sidenav .accordion-body a {
+                color : gray;
+            }
+
+            .accordion-body li {
+                list-style : none;
+            }
+        </style>
+        <ul class="nav nav-pills nav-stacked" id="sidenav">
+			<#list groups?sort_by("name") as group>
+				<li><a data-toggle="collapse" data-parent="#sidenav" href="#${group.id}">${group.name}</a>
+					<div id="${group.id}"
+					<?php echo ($group == '${group.name}')? 'class="accordion-body collapse in"'.chr(62) : 'class="accordion-body collapse"'.chr(62);?>
+					<ul>
+						<#list data as datum>
+							<#if datum.group == group.name>
+								<li>
+									<a href="${datum.filename}">${datum.name}</a>
+								</li>
+							</#if>
+						</#list>
+					</ul>
+					</div>
+				</li>
+			</#list>
+        </ul>
+    </#macro>
\ No newline at end of file
diff --git a/src/main/resources/org/broadinstitute/barclay/helpTemplates/generic.html.ftl b/src/main/resources/org/broadinstitute/barclay/helpTemplates/generic.html.ftl
new file mode 100644
index 0000000..11db3b1
--- /dev/null
+++ b/src/main/resources/org/broadinstitute/barclay/helpTemplates/generic.html.ftl
@@ -0,0 +1,186 @@
+<?php
+    include '../../../common/include/common.php';
+?>
+
+<div class='row-fluid' id="top">
+
+	<#include "common.html.ftl"/>
+
+	<#macro argumentlist name myargs>
+		<#if myargs?size != 0>
+			<tr>
+				<th colspan="4" id="row-divider">${name}</th>
+			</tr>
+			<#list myargs as arg>
+				<tr>
+					<td><a href="#${arg.name}">${arg.name}</a><br />
+						<#if arg.synonyms != "NA">
+							<#if arg.name[2..] != arg.synonyms[1..]>
+								 <em>${arg.synonyms}</em>
+							</#if>
+						</#if>
+					</td>
+					<!--<td>${arg.type}</td> -->
+					<td>${arg.defaultValue!"NA"}</td>
+					<td>${arg.summary}</td>
+				</tr>
+			</#list>
+		</#if>
+	</#macro>
+
+	<#macro argumentDetails arg>
+		<hr style="border-bottom: dotted 1px #C0C0C0;" />
+		<h3><a name="${arg.name}">${arg.name} </a>
+			<#if arg.synonyms != "NA"> / <small>${arg.synonyms}</small></#if>
+		</h3>
+		<p class="args">
+			<b>${arg.summary}</b><br />
+			${arg.fulltext}
+		</p>
+		<#if arg.otherArgumentRequired != "NA">
+			<p><b>Dependency:</b> This argument requires that you also specify <code>${arg.otherArgumentRequired}</code>.</p>
+		</#if>
+		<#if arg.exclusiveOf != "NA">
+			<p><b>Exclusion:</b> This argument cannot be used at the same time as <code>${arg.exclusiveOf}</code>.</p>
+		</#if>
+		<#if arg.options?has_content>
+			<p>
+				The ${arg.name} argument is an enumerated type (${arg.type}), which can have one of the following values:
+			<dl class="enum">
+				<#list arg.options as option>
+					<dt class="enum">${option.name}</dt>
+					<dd class="enum">${option.summary}</dd>
+				</#list>
+			</dl>
+			</p>
+		</#if>
+		<p><#if arg.required != "NA">
+			<#if arg.required == "yes">
+				<span class="badge badge-important">R</span>
+			</#if>
+		</#if>
+			<span class="label label-info ">${arg.type}</span>
+			<#if arg.defaultValue?has_content>
+				 <span class="label">${arg.defaultValue}</span>
+			</#if>
+			<#if arg.minValue?is_number>
+				 <span class="label label-warning">[ [ ${arg.minValue}</span>
+			</#if>
+			<#if arg.minRecValue?is_number>
+				 <span class="label label-success">[ ${arg.minRecValue}</span>
+			</#if>
+			<#if arg.maxRecValue?is_number>
+				 <span class="label label-success">${arg.maxRecValue} ]</span>
+			</#if>
+			<#if arg.maxValue?is_number>
+				 <span class="label label-warning">${arg.maxValue} ] ]</span>
+			</#if>
+		</p>
+	</#macro>
+
+	<#macro relatedByType name type>
+		<#list relatedDocs as relatedDoc>
+			<#if relatedDoc.relation == type>
+				<h3>${name}</h3>
+				<ul>
+					<#list relatedDocs as relatedDoc>
+						<#if relatedDoc.relation == type>
+							<li><a href="${relatedDoc.filename}">${relatedDoc.name}</a> is a ${relatedDoc.relation}</li>
+						</#if>
+					</#list>
+				</ul>
+				<#break>
+			</#if>
+		</#list>
+	</#macro>
+
+	<?php $group = '${group}'; ?>
+
+	<section class="span4">
+		<aside class="well">
+			<a href="index"><h4><i class='fa fa-chevron-left'></i> Back to Tool Docs Index</h4></a>
+		</aside>
+		<aside class="well">
+			<h2>Categories</h2>
+			<@getCategories groups=groups />
+		</aside>
+		<?php getForumPosts( '${name}' ) ?>
+
+	</section>
+
+	<div class="span8">
+
+		<#if beta??>
+			<h1>${name} **BETA**</h1>
+		<#else>
+			<h1>${name}</h1>
+		</#if>
+
+		<p class="lead">${summary}</p>
+
+		<#if group?? >
+			<h3>Category
+				<small> ${group}</small>
+			</h3>
+		</#if>
+		<hr>
+		<h2>Overview</h2>
+		${description}
+
+		<#-- Create references to additional capabilities if appropriate -->
+			<#if extradocs?size != 0 || arguments.all?size != 0>
+				<hr>
+				<h2>Command-line Arguments</h2>
+				<p></p>
+			</#if>
+			<#if extradocs?size != 0>
+				<h3>Additional References</h3>
+				<p>See these additional references for more information.</p>
+				<ul>
+					<#list extradocs as extradoc>
+						<li><a href="${extradoc.filename}">${extradoc.name}</a></li>
+					</#list>
+				</ul>
+			</#if>
+
+			<#-- Create the argument summary -->
+			<#if arguments.all?size != 0>
+				<h3>${name} specific arguments</h3>
+				<p>This table summarizes the command-line arguments that are specific to this tool. For more details on each argument, see the list further down below the table or click on an argument name to jump directly to that entry in the list.</p>
+				<table class="table table-striped table-bordered table-condensed">
+					<thead>
+					<tr>
+						<th>Argument name(s)</th>
+						<th>Default value</th>
+						<th>Summary</th>
+					</tr>
+					</thead>
+					<tbody>
+					<@argumentlist name="Positional Arguments" myargs=arguments.positional/>
+					<@argumentlist name="Required Arguments" myargs=arguments.required/>
+					<@argumentlist name="Optional Tool Arguments" myargs=arguments.optional/>
+					<@argumentlist name="Optional Common Arguments" myargs=arguments.common/>
+					<@argumentlist name="Dependent Arguments" myargs=arguments.dependent/>
+					<@argumentlist name="Advanced Arguments" myargs=arguments.advanced/>
+					<@argumentlist name="Hidden Arguments" myargs=arguments.hidden/>
+					<@argumentlist name="Deprecated Arguments" myargs=arguments.deprecated/>
+					</tbody>
+				</table>
+			</#if>
+
+			<#-- List all of the things -->
+			<#if arguments.all?size != 0>
+				<#-- Create the argument details -->
+					<h3>Argument details</h3>
+					<p>Arguments in this list are specific to this tool. Keep in mind that other arguments are available that are shared with other tools (e.g. command-line GATK arguments); see Inherited arguments above.</p>
+					<#list arguments.all as arg>
+						<@argumentDetails arg=arg/>
+					</#list>
+			</#if>
+
+			<@footerInfo />
+			<@footerClose />
+
+	</div>
+
+	<?php printFooter($module); ?>
\ No newline at end of file
diff --git a/src/main/resources/org/broadinstitute/barclay/helpTemplates/generic.index.html.ftl b/src/main/resources/org/broadinstitute/barclay/helpTemplates/generic.index.html.ftl
new file mode 100644
index 0000000..7ae6d2e
--- /dev/null
+++ b/src/main/resources/org/broadinstitute/barclay/helpTemplates/generic.index.html.ftl
@@ -0,0 +1,65 @@
+<?php
+
+    include '../../../common/include/common.php';
+    include_once '../../config.php';
+    printHeader($module, "Tool Documentation Index", "Guide");
+?>
+
+<div class='row-fluid'>
+
+<div class='span9'>
+
+<#include "common.html.ftl"/>
+
+<#macro emitGroup group>
+    <div class="accordion-group">
+        <div class="accordion-heading">
+            <a class="accordion-toggle" data-toggle="collapse" data-parent="#index" href="#${group.id}">
+                <h4>${group.name}</h4>
+            </a>
+        </div>
+        <div class="accordion-body collapse" id="${group.id}">
+            <div class="accordion-inner">
+                <p class="lead">${group.summary}</p>
+                <table class="table table-striped table-bordered table-condensed">
+                    <tr>
+                        <th>Name</th>
+                        <th>Summary</th>
+                    </tr>
+                    <#list data as datum>
+                        <#if datum.group == group.name>
+                            <tr>
+                                <#if datum.beta??>
+                                    <td><a href="${datum.filename}">${datum.name} **BETA**</a></td>
+                                <#else>
+                                    <td><a href="${datum.filename}">${datum.name}</a></td>
+                                </#if>
+                                <td>${datum.summary}</td>
+                            </tr>
+                        </#if>
+                    </#list>
+                </table>
+            </div>
+        </div>
+    </div>
+</#macro>
+
+<h1 id="top">Tool Documentation Index
+    <small>${version}</small>
+</h1>
+<div class="accordion" id="index">
+    <#list groups?sort_by("name") as group>
+        <@emitGroup group=group/>
+    </#list>
+</div>
+
+<@footerInfo />
+<@footerClose />
+
+</div></div>
+
+<?php
+
+    printFooter($module);
+
+?>
\ No newline at end of file
diff --git a/src/test/java/org/broadinstitute/barclay/argparser/CollectionArgumentUnitTests.java b/src/test/java/org/broadinstitute/barclay/argparser/CollectionArgumentUnitTests.java
new file mode 100644
index 0000000..56d23d6
--- /dev/null
+++ b/src/test/java/org/broadinstitute/barclay/argparser/CollectionArgumentUnitTests.java
@@ -0,0 +1,298 @@
+package org.broadinstitute.barclay.argparser;
+
+import org.testng.Assert;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.*;
+
+/**
+ * Tests for arguments that are collections (not to be confused with ArgumentCollection).
+ */
+public class CollectionArgumentUnitTests {
+
+    class UninitializedCollections {
+        @Argument
+        public List<String> LIST;
+        @Argument
+        public ArrayList<String> ARRAY_LIST;
+        @Argument
+        public HashSet<String> HASH_SET;
+        @PositionalArguments
+        public Collection<File> COLLECTION;
+    }
+
+    @DataProvider(name="uninitializedCollections")
+    public Object[][] uninitializedCollections() {
+        String[] inputArgs = new String[] {"--LIST", "L1", "--LIST", "L2", "--ARRAY_LIST", "S1", "--HASH_SET", "HS1", "P1", "P2"};
+
+        List<File> expectedFileList = new ArrayList<>();
+        expectedFileList.add(new File("P1"));
+        expectedFileList.add(new File("P2"));
+
+        return new Object[][] {
+                // for these two tests, we expect the same results since both append and replace modes have
+                // the same behavior on collections with no initial values
+                {
+                        inputArgs,
+                        Collections.EMPTY_SET,  // replace mode
+                        makeList("L1", "L2"),
+                        makeList("S1"),
+                        makeList("HS1"),
+                        expectedFileList
+                },
+                {
+                        inputArgs,
+                        Collections.singleton(CommandLineParserOptions.APPEND_TO_COLLECTIONS), // append mode
+                        makeList("L1", "L2"),
+                        makeList("S1"),
+                        makeList("HS1"),
+                        expectedFileList
+                }
+        };
+    }
+
+    @Test(dataProvider="uninitializedCollections")
+    public void testUninitializedCollections(
+            final String[] args,
+            final Set<CommandLineParserOptions> parserOptions,
+            final List<String> expectedList,
+            final List<String> expectedArrayList,
+            final List<String> expectedHashSet,
+            final List<File> expectedCollection)
+    {
+        final UninitializedCollections o = new UninitializedCollections();
+        final CommandLineArgumentParser clp = new CommandLineArgumentParser(o, Collections.emptyList(), parserOptions);
+        Assert.assertTrue(clp.parseArguments(System.err, args));
+        Assert.assertEquals(o.LIST, expectedList);
+        Assert.assertEquals(o.ARRAY_LIST, expectedArrayList);
+        Assert.assertEquals(o.HASH_SET, expectedHashSet);
+        Assert.assertEquals(o.COLLECTION, expectedCollection);
+    }
+
+    class InitializedCollections {
+        @Argument
+        public List<String> LIST = makeList("foo", "bar");
+    }
+
+    @DataProvider(name="initializedCollections")
+    public Object[][] initializedCollections() {
+        final String[] inputArgs = new String[] {"--LIST", "baz", "--LIST", "frob"};
+        final String[] inputArgsWithNullAtStart = new String[] {"--LIST", "null", "--LIST", "baz", "--LIST", "frob"};
+        final String[] inputArgsWithNullMidStream = new String[] {"--LIST", "baz", "--LIST", "null", "--LIST", "frob"};
+
+        return new Object[][]{
+                {
+                    inputArgs,
+                    Collections.singleton(CommandLineParserOptions.APPEND_TO_COLLECTIONS),
+                    makeList("foo", "bar", "baz", "frob")       // original values retained
+                },
+                {
+                    inputArgs,
+                    Collections.emptySet(),
+                    makeList("baz", "frob")                     // original values replaced
+                },
+                {
+                    inputArgsWithNullAtStart,
+                    Collections.singleton(CommandLineParserOptions.APPEND_TO_COLLECTIONS),
+                    makeList("baz", "frob")                     // original values replaced
+                },
+                {
+                    inputArgsWithNullAtStart,
+                    Collections.emptySet(),
+                    makeList("baz", "frob")                     // original values replaced
+                },
+                {
+                    inputArgsWithNullMidStream,
+                    Collections.singleton(CommandLineParserOptions.APPEND_TO_COLLECTIONS),
+                    makeList("frob")                            // reset mid-stream via midstream null
+                },
+                {
+                    inputArgsWithNullMidStream,
+                    Collections.emptySet(),
+                    makeList("frob")                            // reset mid-stream via midstream null
+                },
+                {
+                    new String[]{},
+                    Collections.singleton(CommandLineParserOptions.APPEND_TO_COLLECTIONS),
+                    makeList("foo", "bar")
+                },
+                {
+                    new String[]{},
+                    Collections.singleton(Collections.emptySet()),
+                    makeList("foo", "bar")
+                }
+        };
+    }
+
+    @Test(dataProvider="initializedCollections")
+    public void testInitializedCollections(
+            final String[] args,
+            final Set<CommandLineParserOptions> parserOptions,
+            final List<String> expectedResult) {
+        final InitializedCollections o = new InitializedCollections();
+        final CommandLineParser clp = new CommandLineArgumentParser(
+                o,
+                Collections.emptyList(),
+                parserOptions
+        );
+        Assert.assertTrue(clp.parseArguments(System.err, args));
+        Assert.assertEquals(o.LIST, expectedResult);
+    }
+
+    //////////////////////////////////////////////////////////////////
+    // tests for .list files
+
+    class CollectionForListFileArguments {
+        @Argument
+        public List<String> LIST = makeList("foo", "bar");
+
+        @Argument
+        public List<String> LIST2 = makeList("baz");
+    }
+
+    @DataProvider(name="listFileArguments")
+    public Object[][] listFileArguments() {
+        final String[] inputArgs = new String[] { "shmiggle0", "shmiggle1", "shmiggle2" };
+        return new Object[][] {
+                {
+                        // replace mode
+                        Collections.EMPTY_SET,                                                  // parser options
+                        inputArgs,                                                              // args
+                        new String[] {"shmiggle0", "shmiggle1", "shmiggle2"},                   // expected result
+                },
+                {
+                        // append mode
+                        Collections.singleton(CommandLineParserOptions.APPEND_TO_COLLECTIONS),  // parser options
+                        inputArgs,                                                              // args
+                        new String[] {"foo", "bar", "shmiggle0", "shmiggle1", "shmiggle2"},     // expected result
+                },
+        };
+    }
+
+    // Test that .list files populate collections with file contents, both mpdes
+    @Test(dataProvider="listFileArguments")
+    public void testCollectionFromListFile(
+            final Set<CommandLineParserOptions> parserOptions,
+            final String [] argList,
+            final String[] expectedList) throws IOException
+    {
+        final File argListFile = createListArgumentFile("argListFile", argList);
+
+        // use a single file argument
+        final CollectionForListFileArguments o = new CollectionForListFileArguments();
+        final CommandLineParser clp = new CommandLineArgumentParser(
+                o,
+                Collections.emptyList(),
+                parserOptions
+        );
+
+        final String[] args = {"--LIST", argListFile.getAbsolutePath()};
+        Assert.assertTrue(clp.parseArguments(System.err, args));
+
+        Assert.assertEquals(o.LIST, makeList(expectedList));
+    }
+
+    @DataProvider(name="mixedListFileArguments")
+    public Object[][] mixedListFileArguments() {
+        final String[] inputArgList1 = { "shmiggle0", "shmiggle1", "shmiggle2" };
+        final String[] inputArgList2 = { "test2_shmiggle0", "test2_shmiggle1", "test2_shmiggle2" };
+        return new Object[][] {
+                {
+                        // replace mode
+                        Collections.EMPTY_SET,                                  // parser options
+                        inputArgList1,                                          // args list 1
+                        inputArgList2,                                          // args list 2
+                        new String[] {"shmiggle0", "shmiggle1", "shmiggle2"},   // expected result list 1
+                        new String[] {                                          // expected result list 2
+                                "commandLineValue",
+                                "test2_shmiggle0",
+                                "test2_shmiggle1",
+                                "test2_shmiggle2",
+                                "anotherCommandLineValue"
+                        },
+                },
+                {
+                        // append mode
+                        Collections.singleton(CommandLineParserOptions.APPEND_TO_COLLECTIONS),  // parser options
+                        inputArgList1,                                                          // args list 1
+                        inputArgList2,                                                          // args list 2
+                        new String[] {"foo", "bar", "shmiggle0", "shmiggle1", "shmiggle2"},     // expected result list 1
+                        new String[] {                                                          // expected result list 2
+                                "baz",
+                                "commandLineValue",
+                                "test2_shmiggle0",
+                                "test2_shmiggle1",
+                                "test2_shmiggle2",
+                                "anotherCommandLineValue"
+                        },
+                },
+        };
+    }
+
+    // Test that .list files intermixed with explicit command line values populate collections correctly, both mpdes
+    @Test(dataProvider="mixedListFileArguments")
+    public void testCollectionFromListFileMixed(
+            final Set<CommandLineParserOptions> parserOptions,
+            final String [] argList1,
+            final String [] argList2,
+            final String[] expectedList1,
+            final String[] expectedList2
+    ) throws IOException {
+
+        // use two file arguments
+        File listFile = createListArgumentFile("testFile1", argList1);
+        File listFile2 = createListArgumentFile("testFile2", argList2);
+        final CollectionForListFileArguments o = new CollectionForListFileArguments();
+        final CommandLineParser clp = new CommandLineArgumentParser(
+                o,
+                Collections.emptyList(),
+                parserOptions
+        );
+
+        // mix command line values and file values
+        final String[] args = new String[]{
+                "--LIST2", "commandLineValue",
+                "--LIST", listFile.getAbsolutePath(),
+                "--LIST2", listFile2.getAbsolutePath(),
+                "--LIST2", "anotherCommandLineValue"};
+
+        Assert.assertTrue(clp.parseArguments(System.err, args));
+        Assert.assertEquals(o.LIST, makeList(expectedList1));
+        Assert.assertEquals(o.LIST2, makeList(expectedList2));
+    }
+
+    class UninitializedCollectionThatCannotBeAutoInitializedArguments {
+        @Argument
+        public Set<String> SET;
+    }
+
+    @Test(expectedExceptions = CommandLineException.CommandLineParserInternalException.class)
+    public void testCollectionThatCannotBeAutoInitialized() {
+        final UninitializedCollectionThatCannotBeAutoInitializedArguments o =
+                new UninitializedCollectionThatCannotBeAutoInitializedArguments();
+        new CommandLineArgumentParser(o);
+    }
+
+    //////////////////////////////////////////////////////////////////
+    // Helper methods
+
+    private File createListArgumentFile(final String fileName, final String[] argList) throws IOException {
+        final File listFile = File.createTempFile(fileName, ".list");
+        listFile.deleteOnExit();
+        try (final PrintWriter writer = new PrintWriter(listFile)) {
+            Arrays.stream(argList).forEach(arg -> writer.println(arg));
+        }
+        return listFile;
+    }
+
+    public static List<String> makeList(final String... list) {
+        final List<String> result = new ArrayList<>();
+        Collections.addAll(result, list);
+        return result;
+    }
+
+}
diff --git a/src/test/java/org/broadinstitute/barclay/argparser/CommandLineArgumentParserTest.java b/src/test/java/org/broadinstitute/barclay/argparser/CommandLineArgumentParserTest.java
new file mode 100644
index 0000000..2b075d1
--- /dev/null
+++ b/src/test/java/org/broadinstitute/barclay/argparser/CommandLineArgumentParserTest.java
@@ -0,0 +1,1193 @@
+package org.broadinstitute.barclay.argparser;
+
+import org.apache.commons.lang3.tuple.Pair;
+
+import org.testng.Assert;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.PrintStream;
+import java.io.PrintWriter;
+import java.lang.reflect.Field;
+import java.util.*;
+import java.util.function.Consumer;
+
+public final class CommandLineArgumentParserTest {
+    enum FrobnicationFlavor {
+        FOO, BAR, BAZ
+    }
+
+    @CommandLineProgramProperties(
+            summary = "Usage: frobnicate [arguments] input-file output-file\n\nRead input-file, frobnicate it, and write frobnicated results to output-file\n",
+            oneLineSummary = "Read input-file, frobnicate it, and write frobnicated results to output-file",
+            programGroup = TestProgramGroup.class
+    )
+    class FrobnicateArguments {
+        @ArgumentCollection
+        SpecialArgumentsCollection specialArgs = new SpecialArgumentsCollection();
+
+        @PositionalArguments(minElements=2, maxElements=2)
+        public List<File> positionalArguments = new ArrayList<>();
+
+        @Argument(shortName="T", doc="Frobnication threshold setting.")
+        public Integer FROBNICATION_THRESHOLD = 20;
+
+        @Argument
+        public FrobnicationFlavor FROBNICATION_FLAVOR;
+
+        @Argument(doc="Allowed shmiggle types.", optional = false)
+        public List<String> SHMIGGLE_TYPE = new ArrayList<>();
+
+        @Argument
+        public Boolean TRUTHINESS = false;
+    }
+
+    @CommandLineProgramProperties(
+            summary = "Usage: framistat [arguments]\n\nCompute the plebnick of the freebozzle.\n",
+            oneLineSummary = "ompute the plebnick of the freebozzle",
+            programGroup = TestProgramGroup.class
+    )
+    class ArgumentsWithoutPositional {
+        public static final int DEFAULT_FROBNICATION_THRESHOLD = 20;
+        @Argument(shortName="T", doc="Frobnication threshold setting.")
+        public Integer FROBNICATION_THRESHOLD = DEFAULT_FROBNICATION_THRESHOLD;
+
+        @Argument
+        public FrobnicationFlavor FROBNICATION_FLAVOR;
+
+        @Argument(doc="Allowed shmiggle types.", optional = false)
+        public List<String> SHMIGGLE_TYPE = new ArrayList<>();
+
+        @Argument
+        public Boolean TRUTHINESS;
+    }
+
+    class MutexArguments {
+        @Argument(mutex={"M", "N", "Y", "Z"})
+        public String A;
+        @Argument(mutex={"M", "N", "Y", "Z"})
+        public String B;
+        @Argument(mutex={"A", "B", "Y", "Z"})
+        public String M;
+        @Argument(mutex={"A", "B", "Y", "Z"})
+        public String N;
+        @Argument(mutex={"A", "B", "M", "N"})
+        public String Y;
+        @Argument(mutex={"A", "B", "M", "N"})
+        public String Z;
+
+    }
+
+    class MixedCardinalityMutexArguments {
+        @Argument(optional=false, mutex={"scalar"})
+        public List<String> collection;
+        @Argument(optional=false, mutex={"collection"})
+        public String scalar;
+    }
+
+    @CommandLineProgramProperties(
+            summary = "[oscillation_frequency]\n\nResets oscillation frequency.\n",
+            oneLineSummary = "Reset oscillation frequency.",
+            programGroup = TestProgramGroup.class
+    )
+    public class RequiredOnlyArguments {
+        @Argument(doc="Oscillation frequency.", optional = false)
+        public String OSCILLATION_FREQUENCY;
+    }
+
+    @Test
+    public void testRequiredOnlyUsage() {
+        final RequiredOnlyArguments nr = new RequiredOnlyArguments();
+        final CommandLineArgumentParser clp = new CommandLineArgumentParser(nr);
+        final String out = clp.usage(false, false); // without common/hidden args
+        final int reqIndex = out.indexOf("Required Arguments:");
+        Assert.assertTrue(reqIndex > 0);
+        Assert.assertTrue(out.indexOf("Optional Arguments:", reqIndex) < 0);
+        Assert.assertTrue(out.indexOf("Advanced Arguments:", reqIndex) < 0);
+    }
+
+    @CommandLineProgramProperties(
+            summary = "[oscillation_frequency]\n\nResets oscillation frequency.\n",
+            oneLineSummary = "Reset oscillation frequency.",
+            programGroup = TestProgramGroup.class
+    )
+    @BetaFeature
+    public class BetaTool {
+    }
+
+    @Test
+    public void testBetaFeatureUsage() {
+        final BetaTool eo = new BetaTool();
+        final CommandLineArgumentParser clp = new CommandLineArgumentParser(eo);
+        final String out = clp.usage(false, false); // without common/hidden args
+        final int reqIndex = out.indexOf(CommandLineArgumentParser.BETA_PREFIX);
+        Assert.assertEquals(reqIndex, 0);
+    }
+
+    class AbbreviatableArgument{
+        public static final String ARGUMENT_NAME = "longNameArgument";
+        @Argument(fullName= ARGUMENT_NAME)
+        public boolean longNameArgument;
+    }
+
+    @Test(expectedExceptions = CommandLineException.class)
+    public void testAbbreviationsAreRejected() {
+        final AbbreviatableArgument abrv = new AbbreviatableArgument();
+        final CommandLineArgumentParser clp = new CommandLineArgumentParser(abrv);
+        //argument name is valid when it isn't abbreviated
+        Assert.assertTrue(clp.parseArguments(System.err, new String[]{"--" + AbbreviatableArgument.ARGUMENT_NAME}));
+
+        //should throw when the abbreviated name is used
+        clp.parseArguments(System.err, new String[]{"--" + AbbreviatableArgument.ARGUMENT_NAME.substring(0,5)});
+    }
+
+    @CommandLineProgramProperties(
+            summary = "[oscillation_frequency]\n\nRecalibrates overthruster oscillation. \n",
+            oneLineSummary = "Recalibrates the overthruster.",
+            programGroup = TestProgramGroup.class
+    )
+    public class OptionalOnlyArguments {
+        @Argument(doc="Oscillation frequency.", optional = true)
+        public String OSCILLATION_FREQUENCY = "20";
+    }
+
+    @Test
+    public void testOptionalOnlyUsage() {
+        final OptionalOnlyArguments oo = new OptionalOnlyArguments();
+        final CommandLineArgumentParser clp = new CommandLineArgumentParser(oo);
+        final String out = clp.usage(false, false); // without common/hidden args
+        final int reqIndex = out.indexOf("Required Arguments:");
+        Assert.assertTrue(reqIndex < 0);
+        Assert.assertTrue(out.indexOf("Optional Arguments:", reqIndex) > 0);
+        Assert.assertEquals(out.indexOf("Conditional Arguments:", reqIndex), -1);
+        Assert.assertEquals(out.indexOf("Advanced Arguments:", reqIndex), -1);
+    }
+
+    /**
+     * Validate the text emitted by a call to usage by ensuring that required arguments are
+     * emitted before optional ones.
+     */
+    private void validateRequiredOptionalUsage(final CommandLineArgumentParser clp, final boolean withDefault, final boolean hasAdvanced) {
+        final String out = clp.usage(withDefault, false); // with common args, without hidden args
+        // Required arguments should appear before optional ones
+        final int reqIndex = out.indexOf("Required Arguments:");
+        Assert.assertTrue(reqIndex > 0);
+        Assert.assertTrue(out.indexOf("Optional Arguments:", reqIndex) > 0);
+        Assert.assertEquals(out.indexOf("Conditional Arguments:", reqIndex), -1);
+        Assert.assertEquals(out.indexOf("Advanced Arguments:", reqIndex) != -1, hasAdvanced);
+    }
+
+    @Test
+    public void testRequiredOptionalWithDefaultUsage() {
+        final FrobnicateArguments fo = new FrobnicateArguments();
+        final CommandLineArgumentParser clp = new CommandLineArgumentParser(fo);
+        // FrobnicateArguments has a SpecialArgumentsCollection that contains an @Advanced argument ("called showHidden")
+        validateRequiredOptionalUsage(clp, true, true); // with common args
+    }
+
+    @Test
+    public void testRequiredOptionalWithoutDefaultUsage() {
+        final FrobnicateArguments fo = new FrobnicateArguments();
+        final CommandLineArgumentParser clp = new CommandLineArgumentParser(fo);
+        // FrobnicateArguments has a SpecialArgumentsCollection that contains an @Advanced argument ("called showHidden")
+        validateRequiredOptionalUsage(clp, false, true); // without common args
+    }
+
+    @Test
+    public void testWithoutPositionalWithDefaultUsage() {
+        final ArgumentsWithoutPositional fo = new ArgumentsWithoutPositional();
+        final CommandLineArgumentParser clp = new CommandLineArgumentParser(fo);
+        // does not have the special showHidden advanced argument
+        validateRequiredOptionalUsage(clp, true, false); // with common args
+    }
+
+    @Test
+    public void testWithoutPositionalWithoutDefaultUsage() {
+        final ArgumentsWithoutPositional fo = new ArgumentsWithoutPositional();
+        final CommandLineArgumentParser clp = new CommandLineArgumentParser(fo);
+        // does not have the special showHidden advanced argument
+        validateRequiredOptionalUsage(clp, false, false); // without common args
+    }
+
+    @Test
+    public void testPositive() {
+        final String[] args = {
+                "-T","17",
+                "-FROBNICATION_FLAVOR","BAR",
+                "-TRUTHINESS",
+                "-SHMIGGLE_TYPE","shmiggle1",
+                "-SHMIGGLE_TYPE","shmiggle2",
+                "positional1",
+                "positional2",
+        };
+        final FrobnicateArguments fo = new FrobnicateArguments();
+        final CommandLineArgumentParser clp = new CommandLineArgumentParser(fo);
+        Assert.assertTrue(clp.parseArguments(System.err, args));
+        Assert.assertEquals(fo.positionalArguments.size(), 2);
+        final File[] expectedPositionalArguments = { new File("positional1"), new File("positional2")};
+        Assert.assertEquals(fo.positionalArguments.toArray(), expectedPositionalArguments);
+        Assert.assertEquals(fo.FROBNICATION_THRESHOLD.intValue(), 17);
+        Assert.assertEquals(fo.FROBNICATION_FLAVOR, FrobnicationFlavor.BAR);
+        Assert.assertEquals(fo.SHMIGGLE_TYPE.size(), 2);
+        final String[] expectedShmiggleTypes = {"shmiggle1", "shmiggle2"};
+        Assert.assertEquals(fo.SHMIGGLE_TYPE.toArray(), expectedShmiggleTypes);
+        Assert.assertTrue(fo.TRUTHINESS);
+    }
+
+    @Test
+    public void testGetCommandLine() {
+        final String[] args = {
+                "-T","17",
+                "-FROBNICATION_FLAVOR","BAR",
+                "-TRUTHINESS",
+                "-SHMIGGLE_TYPE","shmiggle1",
+                "-SHMIGGLE_TYPE","shmiggle2",
+                "positional1",
+                "positional2",
+        };
+        final FrobnicateArguments fo = new FrobnicateArguments();
+        final CommandLineArgumentParser clp = new CommandLineArgumentParser(fo);
+        Assert.assertTrue(clp.parseArguments(System.err, args));
+        Assert.assertEquals(clp.getCommandLine(),
+                "FrobnicateArguments  " +
+                        "positional1 positional2 --FROBNICATION_THRESHOLD 17 --FROBNICATION_FLAVOR BAR " +
+                        "--SHMIGGLE_TYPE shmiggle1 --SHMIGGLE_TYPE shmiggle2 --TRUTHINESS true  --help false " +
+                        "--version false --showHidden false");
+    }
+
+    private static class WithSensitiveValues {
+
+        @Argument(sensitive = true)
+        public String secretValue;
+
+        @Argument
+        public String openValue;
+    }
+
+    @Test
+    public void testGetCommandLineWithSensitiveArgument(){
+        final String supersecret = "supersecret";
+        final String unclassified = "unclassified";
+        final String[] args = {
+                "--secretValue", supersecret,
+                "--openValue", unclassified
+        };
+        final WithSensitiveValues sv = new WithSensitiveValues();
+        final CommandLineArgumentParser clp = new CommandLineArgumentParser(sv);
+        Assert.assertTrue(clp.parseArguments(System.err, args));
+
+        final String commandLine = clp.getCommandLine();
+
+        Assert.assertTrue(commandLine.contains(unclassified));
+        Assert.assertFalse(commandLine.contains(supersecret));
+
+        Assert.assertEquals(sv.openValue, unclassified);
+        Assert.assertEquals(sv.secretValue, supersecret);
+    }
+
+    @Test
+    public void testDefault() {
+        final String[] args = {
+                "--FROBNICATION_FLAVOR","BAR",
+                "--TRUTHINESS",
+                "--SHMIGGLE_TYPE","shmiggle1",
+                "--SHMIGGLE_TYPE","shmiggle2",
+                "positional1",
+                "positional2",
+        };
+        final FrobnicateArguments fo = new FrobnicateArguments();
+        final CommandLineArgumentParser clp = new CommandLineArgumentParser(fo);
+        Assert.assertTrue(clp.parseArguments(System.err, args));
+        Assert.assertEquals(fo.FROBNICATION_THRESHOLD.intValue(), 20);
+    }
+
+    @Test(expectedExceptions = CommandLineException.MissingArgument.class)
+    public void testMissingRequiredArgument() {
+        final String[] args = {
+                "--TRUTHINESS","False",
+                "--SHMIGGLE_TYPE","shmiggle1",
+                "--SHMIGGLE_TYPE","shmiggle2",
+                "positional1",
+                "positional2",
+        };
+        final FrobnicateArguments fo = new FrobnicateArguments();
+        final CommandLineArgumentParser clp = new CommandLineArgumentParser(fo);
+        clp.parseArguments(System.err, args);
+    }
+
+    class CollectionRequired{
+        @Argument(optional = false)
+        List<Integer> ints;
+    }
+
+    @Test(expectedExceptions = CommandLineException.MissingArgument.class)
+    public void testMissingRequiredCollectionArgument(){
+        final String[] args = {};
+        final CollectionRequired cr = new CollectionRequired();
+        final CommandLineArgumentParser clp = new CommandLineArgumentParser(cr);
+        clp.parseArguments(System.err, args);
+    }
+
+    @Test( expectedExceptions = CommandLineException.BadArgumentValue.class)
+    public void testBadValue() {
+        final String[] args = {
+                "--FROBNICATION_THRESHOLD","ABC",
+                "--FROBNICATION_FLAVOR","BAR",
+                "--TRUTHINESS","False",
+                "--SHMIGGLE_TYPE","shmiggle1",
+                "--SHMIGGLE_TYPE","shmiggle2",
+                "positional1",
+                "positional2",
+        };
+        final FrobnicateArguments fo = new FrobnicateArguments();
+        final CommandLineArgumentParser clp = new CommandLineArgumentParser(fo);
+        clp.parseArguments(System.err, args);
+    }
+
+    @Test(expectedExceptions = CommandLineException.BadArgumentValue.class)
+    public void testBadEnumValue() {
+        final String[] args = {
+                "--FROBNICATION_FLAVOR","HiMom",
+                "--TRUTHINESS","False",
+                "--SHMIGGLE_TYPE","shmiggle1",
+                "--SHMIGGLE_TYPE","shmiggle2",
+                "positional1",
+                "positional2",
+        };
+        final FrobnicateArguments fo = new FrobnicateArguments();
+        final CommandLineArgumentParser clp = new CommandLineArgumentParser(fo);
+        clp.parseArguments(System.err, args);
+    }
+
+    @Test(expectedExceptions = CommandLineException.MissingArgument.class)
+    public void testNotEnoughOfListArgument() {
+        final String[] args = {
+                "--FROBNICATION_FLAVOR","BAR",
+                "--TRUTHINESS","False",
+                "positional1",
+                "positional2",
+        };
+        final FrobnicateArguments fo = new FrobnicateArguments();
+        final CommandLineArgumentParser clp = new CommandLineArgumentParser(fo);
+        clp.parseArguments(System.err, args);
+    }
+
+    @Test(expectedExceptions = CommandLineException.class)
+    public void testTooManyPositional() {
+        final String[] args = {
+                "--FROBNICATION_FLAVOR","BAR",
+                "--TRUTHINESS","False",
+                "--SHMIGGLE_TYPE","shmiggle1",
+                "--SHMIGGLE_TYPE","shmiggle2",
+                "positional1",
+                "positional2",
+                "positional3",
+        };
+        final FrobnicateArguments fo = new FrobnicateArguments();
+        final CommandLineArgumentParser clp = new CommandLineArgumentParser(fo);
+        clp.parseArguments(System.err, args);
+    }
+
+    @Test(expectedExceptions = CommandLineException.MissingArgument.class)
+    public void testNotEnoughPositional() {
+        final String[] args = {
+                "--FROBNICATION_FLAVOR","BAR",
+                "--TRUTHINESS","False",
+                "--SHMIGGLE_TYPE","shmiggle1",
+                "--SHMIGGLE_TYPE","shmiggle2",
+        };
+        final FrobnicateArguments fo = new FrobnicateArguments();
+        final CommandLineArgumentParser clp = new CommandLineArgumentParser(fo);
+        clp.parseArguments(System.err, args);
+    }
+
+    @Test( expectedExceptions = CommandLineException.class)
+    public void testUnexpectedPositional() {
+        final String[] args = {
+                "--T","17",
+                "--FROBNICATION_FLAVOR","BAR",
+                "--TRUTHINESS","False",
+                "--SHMIGGLE_TYPE","shmiggle1",
+                "--SHMIGGLE_TYPE","shmiggle2",
+                "positional"
+        };
+        final ArgumentsWithoutPositional fo = new ArgumentsWithoutPositional();
+        final CommandLineArgumentParser clp = new CommandLineArgumentParser(fo);
+        clp.parseArguments(System.err, args);
+    }
+
+    @Test(expectedExceptions = CommandLineException.class)
+    public void testArgumentUseClash() {
+        final String[] args = {
+                "--FROBNICATION_FLAVOR", "BAR",
+                "--FROBNICATION_FLAVOR", "BAZ",
+                "--SHMIGGLE_TYPE", "shmiggle1",
+                "positional1",
+                "positional2",
+        };
+        final FrobnicateArguments fo = new FrobnicateArguments();
+        final CommandLineArgumentParser clp = new CommandLineArgumentParser(fo);
+        clp.parseArguments(System.err, args);
+    }
+
+    @Test
+    public void testArgumentsFile() throws Exception {
+        final File argumentsFile = File.createTempFile("clp.", ".arguments");
+        argumentsFile.deleteOnExit();
+        try (final PrintWriter writer = new PrintWriter(argumentsFile)) {
+            writer.println("-T 18");
+            writer.println("--TRUTHINESS");
+            writer.println("--SHMIGGLE_TYPE shmiggle0");
+            writer.println("--" + SpecialArgumentsCollection.ARGUMENTS_FILE_FULLNAME + " " + argumentsFile.getPath());
+            //writer.println("--STRANGE_ARGUMENT shmiggle0");
+        }
+        final String[] args = {
+                "--"+SpecialArgumentsCollection.ARGUMENTS_FILE_FULLNAME, argumentsFile.getPath(),
+                // Multiple arguments files are allowed
+                "--"+SpecialArgumentsCollection.ARGUMENTS_FILE_FULLNAME, argumentsFile.getPath(),
+                "--FROBNICATION_FLAVOR","BAR",
+                "--TRUTHINESS",
+                "--SHMIGGLE_TYPE","shmiggle0",
+                "--SHMIGGLE_TYPE","shmiggle1",
+                "positional1",
+                "positional2",
+        };
+        final FrobnicateArguments fo = new FrobnicateArguments();
+        final CommandLineArgumentParser clp = new CommandLineArgumentParser(fo);
+        Assert.assertTrue(clp.parseArguments(System.err, args));
+        Assert.assertEquals(fo.positionalArguments.size(), 2);
+        final File[] expectedPositionalArguments = { new File("positional1"), new File("positional2")};
+        Assert.assertEquals(fo.positionalArguments.toArray(), expectedPositionalArguments);
+        Assert.assertEquals(fo.FROBNICATION_THRESHOLD.intValue(), 18);
+        Assert.assertEquals(fo.FROBNICATION_FLAVOR, FrobnicationFlavor.BAR);
+        Assert.assertEquals(fo.SHMIGGLE_TYPE.size(), 3);
+        final String[] expectedShmiggleTypes = {"shmiggle0", "shmiggle0", "shmiggle1"};
+        Assert.assertEquals(fo.SHMIGGLE_TYPE.toArray(), expectedShmiggleTypes);
+        Assert.assertTrue(fo.TRUTHINESS);
+    }
+
+
+    /**
+     * In an arguments file, should not be allowed to override an argument set on the command line
+     * @throws Exception
+     */
+    @Test( expectedExceptions = CommandLineException.class)
+    public void testArgumentsFileWithDisallowedOverride() throws Exception {
+        final File argumentsFile = File.createTempFile("clp.", ".arguments");
+        argumentsFile.deleteOnExit();
+        try (final PrintWriter writer = new PrintWriter(argumentsFile)) {
+            writer.println("--T 18");
+        }
+        final String[] args = {
+                "--T","17",
+                "--"+SpecialArgumentsCollection.ARGUMENTS_FILE_FULLNAME ,argumentsFile.getPath()
+        };
+        final FrobnicateArguments fo = new FrobnicateArguments();
+        final CommandLineArgumentParser clp = new CommandLineArgumentParser(fo);
+        clp.parseArguments(System.err, args);
+    }
+
+    @DataProvider(name="failingMutexScenarios")
+    public Object[][] failingMutexScenarios() {
+        return new Object[][] {
+                { "no args", new MutexArguments(), new String[0] },
+                { "1 of group required", new MutexArguments(), new String[] {"-A","1"} },
+                { "mutex", new MutexArguments(), new String[]  {"-A","1", "-Y","3"} },
+                { "mega mutex", new MutexArguments(), new String[]  {"-A","1", "-B","2", "-Y","3", "-Z","1", "-M","2", "-N","3"} },
+                { "collection and scalar mutex mix", new MixedCardinalityMutexArguments(), new String[] { "-collection", "s1", "-scalar", "s2"}}
+        };
+    }
+
+    @DataProvider(name="passingMutexScenarios")
+    public Object[][] passingMutexScenarios() {
+        return new Object[][] {
+                { "simple mutex", new MutexArguments(), new String[] { "-A", "1", "-B", "2"} },
+                { "collection mixed", new MixedCardinalityMutexArguments(), new String[] { "-collection", "s1"} },
+                { "collection multiple mixed", new MixedCardinalityMutexArguments(), new String[] { "-collection", "s1", "-collection", "s2"} },
+                { "scalar mixed", new MixedCardinalityMutexArguments(), new String[] { "-scalar", "s1"} }
+        };
+    }
+
+    @Test(dataProvider="passingMutexScenarios")
+    public void passingMutexCheck(final String testName, final Object o, final String[] args){
+        final CommandLineArgumentParser clp = new CommandLineArgumentParser(o);
+        Assert.assertTrue(clp.parseArguments(System.err, args));
+    }
+
+    @Test(dataProvider="failingMutexScenarios", expectedExceptions = CommandLineException.class)
+    public void testFailingMutex(final String testName, final Object o, final String[] args) {
+        final CommandLineArgumentParser clp = new CommandLineArgumentParser(o);
+        clp.parseArguments(System.err, args);
+    }
+
+    @Test
+       public void testFlagNoArgument(){
+        final BooleanFlags o = new BooleanFlags();
+        final CommandLineArgumentParser clp = new CommandLineArgumentParser(o);
+        Assert.assertTrue(clp.parseArguments(System.err, new String[]{"--flag1"}));
+        Assert.assertTrue(o.flag1);
+    }
+
+    @Test
+    public void testFlagsWithArguments(){
+        final BooleanFlags o = new BooleanFlags();
+        final CommandLineArgumentParser clp = new CommandLineArgumentParser(o);
+        Assert.assertTrue(clp.parseArguments(System.err, new String[]{"--flag1", "false", "--flag2", "false"}));
+        Assert.assertFalse(o.flag1);
+        Assert.assertFalse(o.flag2);
+    }
+
+    class ArgsCollection {
+        @Argument(fullName = "arg1")
+        public int Arg1;
+    }
+
+    class ArgsCollectionHaver{
+
+        public ArgsCollectionHaver(){}
+
+        @ArgumentCollection
+        public ArgsCollection default_args = new ArgsCollection();
+
+        @Argument(fullName = "somenumber",shortName = "n")
+        public int someNumber = 0;
+    }
+
+    @Test
+    public void testArgumentCollection(){
+        final ArgsCollectionHaver o = new ArgsCollectionHaver();
+        final CommandLineArgumentParser clp = new CommandLineArgumentParser(o);
+
+        String[] args = {"--arg1", "42", "--somenumber", "12"};
+        Assert.assertTrue(clp.parseArguments(System.err, args));
+        Assert.assertEquals(o.someNumber, 12);
+        Assert.assertEquals(o.default_args.Arg1, 42);
+
+    }
+
+    class BooleanFlags{
+        @Argument
+        public Boolean flag1 = false;
+
+        @Argument
+        public boolean flag2 = true;
+
+        @Argument
+        public boolean flag3 = false;
+
+        @Argument(mutex="flag1")
+        public boolean antiflag1 = false;
+
+        @ArgumentCollection
+        SpecialArgumentsCollection special = new SpecialArgumentsCollection();
+    }
+
+    @Test
+    public void testCombinationOfFlags(){
+        final BooleanFlags o = new BooleanFlags();
+        final CommandLineArgumentParser clp = new CommandLineArgumentParser(o);
+
+        clp.parseArguments(System.err, new String[]{"--flag1", "false", "--flag2"});
+        Assert.assertFalse(o.flag1);
+        Assert.assertTrue(o.flag2);
+        Assert.assertFalse(o.flag3);
+    }
+
+    class WithBadField{
+        @Argument
+        @ArgumentCollection
+        public boolean badfield;
+    }
+
+    @Test(expectedExceptions = CommandLineException.CommandLineParserInternalException.class)
+    public void testBadFieldCausesException(){
+        WithBadField o = new WithBadField();
+        final CommandLineArgumentParser clp = new CommandLineArgumentParser(o);
+    }
+
+    class PrivateArgument{
+        @Argument
+        private boolean privateArgument = false;
+
+        @Argument(optional = true)
+        private List<Integer> privateCollection = new ArrayList<>();
+
+        @ArgumentCollection
+        private BooleanFlags booleanFlags= new BooleanFlags();
+
+        @PositionalArguments()
+        List<Integer> positionals = new ArrayList<>();
+    }
+
+    @Test
+    public void testFlagWithPositionalFollowing(){
+        PrivateArgument o = new PrivateArgument();
+        final CommandLineArgumentParser clp = new CommandLineArgumentParser(o);
+        Assert.assertTrue(clp.parseArguments(System.err, new String[]{"--flag1","1","2" }));
+        Assert.assertTrue(o.booleanFlags.flag1);
+        Assert.assertEquals(o.positionals, Arrays.asList(1, 2));
+    }
+
+    @Test
+    public void testPrivateArgument(){
+        PrivateArgument o = new PrivateArgument();
+        final CommandLineArgumentParser clp = new CommandLineArgumentParser(o);
+        Assert.assertTrue(clp.parseArguments(System.err, new String[]{"--privateArgument",
+                "--privateCollection", "1", "--privateCollection", "2", "--flag1"}));
+        Assert.assertTrue(o.privateArgument);
+        Assert.assertEquals(o.privateCollection, Arrays.asList(1,2));
+        Assert.assertTrue(o.booleanFlags.flag1);
+    }
+
+    /**
+     * Test that the special flag --version is handled correctly
+     * (no blowup)
+     */
+    @Test
+    public void testVersionSpecialFlag(){
+        final BooleanFlags o = new BooleanFlags();
+        final CommandLineArgumentParser clp = new CommandLineArgumentParser(o);
+
+        final String[] versionArgs = {"--" + SpecialArgumentsCollection.VERSION_FULLNAME};
+        String out = captureStderr(() -> {
+                    Assert.assertFalse(clp.parseArguments(System.err, versionArgs));
+            });
+        Assert.assertTrue(out.contains("Version:"));
+
+        Assert.assertTrue(clp.parseArguments(System.err, new String[]{"--version","false"}));
+        Assert.assertFalse(clp.parseArguments(System.err, new String[]{"--version", "true"}));
+    }
+
+    /**
+     * Test that the special flag --help is handled correctly
+     * (no blowup)
+     */
+    @Test
+    public void testHelp(){
+        final BooleanFlags o = new BooleanFlags();
+        final CommandLineArgumentParser clp = new CommandLineArgumentParser(o);
+
+        final String[] versionArgs = {"--" + SpecialArgumentsCollection.HELP_FULLNAME};
+        String out = captureStderr(() -> {
+            Assert.assertFalse(clp.parseArguments(System.err, versionArgs));
+        });
+        Assert.assertTrue(out.contains("USAGE:"));
+
+        Assert.assertTrue(clp.parseArguments(System.err, new String[]{"--help","false"}));
+        Assert.assertFalse(clp.parseArguments(System.err, new String[]{"--help", "true"}));
+    }
+
+    class NameCollision{
+        @ArgumentCollection
+        public ArgsCollection argsCollection = new ArgsCollection();
+
+        //this arg name collides with one in ArgsCollection
+        @Argument(fullName = "arg1")
+        public int anArg;
+    }
+
+    @Test(expectedExceptions = CommandLineException.CommandLineParserInternalException.class)
+    public void testArgumentNameCollision(){
+        final NameCollision collides = new NameCollision();
+        final CommandLineArgumentParser clp = new CommandLineArgumentParser(collides);
+
+        clp.parseArguments(System.err, new String[]{"--arg1", "101"});
+    }
+    /**
+     * captures {@link System#err} while runnable is executing
+     * @param runnable a code block to execute
+     * @return everything written to {@link System#err} by runnable
+     */
+    public static String captureStderr(Runnable runnable){
+        return captureSystemStream(runnable, System.err, System::setErr);
+    }
+
+    private static String captureSystemStream(Runnable runnable, PrintStream stream, Consumer<? super PrintStream> setter){
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        setter.accept(new PrintStream(out));
+        try {
+            runnable.run();
+        } finally{
+            setter.accept(stream);
+        }
+        return out.toString();
+    }
+
+
+    @CommandLineProgramProperties(
+            summary = "tool with nullable arguments",
+            oneLineSummary = "tools with nullable arguments",
+            programGroup = TestProgramGroup.class
+    )
+    public class WithNullableArguments {
+        // Integer without boundaries and null should be allowed
+        @Argument(doc = "Integer with null value allowed", optional = true)
+        public Integer nullInteger = null;
+        // Double without boundaries and null should be allowed
+        @Argument(doc = "Double with null value allowed", optional = true)
+        public Double nullDouble= null;
+    }
+
+    @DataProvider(name = "nullableArgs")
+    public Object[][] getNullableArguments() {
+        return new Object[][] {
+                // null values
+                {new String[]{}, null, null},
+                {new String[]{"--nullInteger", "null"}, null, null},
+                {new String[]{"--nullDouble", "null"}, null, null},
+                {new String[]{"--nullInteger", "null", "--nullDouble", "null"}, null, null},
+                // with values
+                {new String[]{"--nullInteger", "1"}, 1, null},
+                {new String[]{"--nullDouble", "2"}, null, 2d},
+                {new String[]{"--nullInteger", "1", "--nullDouble", "2"}, 1, 2.d},
+        };
+    }
+
+    @Test(dataProvider = "nullableArgs")
+    public void testWithinBoundariesArguments(final String[] argv, final Integer expectedInteger, final Double expectedDouble) throws Exception {
+        final WithNullableArguments o = new WithNullableArguments();
+        final CommandLineArgumentParser clp = new CommandLineArgumentParser(o);
+        Assert.assertTrue(clp.parseArguments(System.err, argv));
+        Assert.assertEquals(o.nullInteger, expectedInteger);
+        Assert.assertEquals(o.nullDouble, expectedDouble);
+    }
+
+    @Test(expectedExceptions = CommandLineException.CommandLineParserInternalException.class)
+    public void testWithBoundariesArgumentsForNoNumeric() {
+        @CommandLineProgramProperties(summary = "broken tool",
+                oneLineSummary = "broken tool",
+                programGroup = TestProgramGroup.class)
+        class WithBoundariesArgumentsForString {
+            @Argument(doc = "String argument", optional = true, minValue = 0, maxValue = 30)
+            public String stringArg = "string";
+        }
+        final CommandLineArgumentParser clp = new CommandLineArgumentParser(new WithBoundariesArgumentsForString());
+    }
+
+    @Test(expectedExceptions = CommandLineException.CommandLineParserInternalException.class)
+    public void testWithDoubleBoundariesArgumentsForInteger() {
+        @CommandLineProgramProperties(summary = "broken tool",
+                oneLineSummary = "broken tool",
+                programGroup = TestProgramGroup.class)
+        class WithDoubleBoundariesArgumentsForInteger {
+            @Argument(doc = "Integer argument", minValue = 0.1, maxValue = 0.5)
+            public Integer integerArg;
+        }
+        final CommandLineArgumentParser clp = new CommandLineArgumentParser(new WithDoubleBoundariesArgumentsForInteger());
+    }
+
+    @CommandLineProgramProperties(
+            summary = "tool with boundaries",
+            oneLineSummary = "tools with boundaries",
+            programGroup = TestProgramGroup.class
+    )
+    public class WithBoundariesArguments {
+        // recommended values are not explicitly verified by the tests, but do force the code through the warning code paths
+        @Argument(doc = "Double argument in the range [10, 20]", optional = true, minValue = 10, minRecommendedValue = 16, maxRecommendedValue = 17, maxValue = 20)
+        public Double doubleArg = 15d;
+        // recommended values are not explicitly verified by the tests, but do force the code through the warning code paths
+        @Argument(doc = "Integer in the range [0, 30]", optional = true, minValue = 0, minRecommendedValue = 10, maxRecommendedValue = 15, maxValue = 30)
+        public int integerArg = 20;
+    }
+
+    @DataProvider
+    public Object[][] withinBoundariesArgs() {
+        return new Object[][]{
+            {new String[]{"--integerArg", "0"}, 15, 0},
+            {new String[]{"--integerArg", "10"}, 15, 10},
+            {new String[]{"--integerArg", "30"}, 15, 30},
+            {new String[]{"--doubleArg", "10"}, 10, 20},
+            {new String[]{"--doubleArg", "12"}, 12, 20},
+            {new String[]{"--doubleArg", "16"}, 16, 20},
+            {new String[]{"--doubleArg", "18"}, 18, 20},
+            {new String[]{"--doubleArg", "20"}, 20, 20}
+        };
+    }
+
+    @Test(dataProvider = "withinBoundariesArgs")
+    public void testWithinBoundariesArguments(final String[] argv, final double expectedDouble, final int expectedInteger) throws Exception {
+        final WithBoundariesArguments o = new WithBoundariesArguments();
+        final CommandLineArgumentParser clp = new CommandLineArgumentParser(o);
+        Assert.assertTrue(clp.parseArguments(System.err, argv));
+        Assert.assertEquals(o.doubleArg, expectedDouble);
+        Assert.assertEquals(o.integerArg, expectedInteger);
+    }
+
+    @DataProvider
+    public Object[][] outOfRangeArgs() {
+        return new Object[][]{
+                {new String[]{"--integerArg", "-45"}},
+                {new String[]{"--integerArg", "-1"}},
+                {new String[]{"--integerArg", "31"}},
+                {new String[]{"--integerArg", "106"}},
+                {new String[]{"--integerArg", "null"}},
+                {new String[]{"--doubleArg", "-1"}},
+                {new String[]{"--doubleArg", "0"}},
+                {new String[]{"--doubleArg", "21"}}
+        };
+    }
+
+    @Test(dataProvider = "outOfRangeArgs", expectedExceptions = CommandLineException.OutOfRangeArgumentValue.class)
+    public void testOutOfRangesArguments(final String[] argv) throws Exception {
+        final WithBoundariesArguments o = new WithBoundariesArguments();
+        final CommandLineArgumentParser clp = new CommandLineArgumentParser(o);
+        clp.parseArguments(System.err, argv);
+    }
+
+    @Test(expectedExceptions = CommandLineException.CommandLineParserInternalException.class)
+    public void testHiddenRequiredArgumentThrowException() throws Exception {
+        @CommandLineProgramProperties(summary = "tool with required and hidden argument",
+                oneLineSummary = "tool with required and hidden argument",
+                programGroup = TestProgramGroup.class)
+        final class ToolWithRequiredHiddenArgument {
+            @Argument(fullName = "hiddenTestArgument", shortName = "hiddenTestArgument", doc = "Hidden argument", optional = false)
+            @Hidden
+            public Integer hidden;
+        }
+        new CommandLineArgumentParser(new ToolWithRequiredHiddenArgument());
+    }
+
+    @Test
+    public void testHiddenArguments() throws Exception {
+        @CommandLineProgramProperties(summary = "test",
+                oneLineSummary = "test",
+                programGroup = TestProgramGroup.class)
+        final class ToolWithHiddenArgument {
+            @Argument(fullName = "hiddenTestArgument", shortName = "hiddenTestArgument", doc = "Hidden argument", optional = true)
+            @Hidden
+            public Integer hidden = 0;
+        }
+
+        final ToolWithHiddenArgument tool = new ToolWithHiddenArgument();
+        final CommandLineArgumentParser clp = new CommandLineArgumentParser(tool);
+        // test that it is not printed in the usage
+        String out = clp.usage(true, false); // with common args, without hidden
+        Assert.assertEquals(out.indexOf("hiddenTestArgument"), -1, out);
+        out = clp.usage(true, true); // with common and hidden args
+        Assert.assertNotEquals(out.indexOf("hiddenTestArgument"), -1, out);
+        // test that it is parsed from the command line if specified
+        clp.parseArguments(System.err, new String[]{"--hiddenTestArgument", "10"});
+        Assert.assertEquals(tool.hidden.intValue(), 10);
+    }
+
+    @Test
+    public void testAdvancedArguments() throws Exception {
+        @CommandLineProgramProperties(summary = "test",
+                oneLineSummary = "test",
+                programGroup = TestProgramGroup.class)
+        final class ToolWithAdvancedArgument {
+            @Argument(fullName = "advancedTestArgument", shortName = "advancedTestArgument", doc = "Advanced argument", optional = true)
+            @Advanced
+            public Integer advanced = 0;
+        }
+
+        final CommandLineParser clp = new CommandLineArgumentParser(new ToolWithAdvancedArgument());
+        // test that it is printed in the usage
+        final String out = clp.usage(true, false); // with common args, without hidden
+        Assert.assertTrue(out.contains("advancedTestArgument"), out);
+    }
+
+    @Test(expectedExceptions = CommandLineException.CommandLineParserInternalException.class)
+    public void testRequiredAdvancedNotAllowed() throws Exception {
+        @CommandLineProgramProperties(summary = "test",
+                oneLineSummary = "test",
+                programGroup = TestProgramGroup.class)
+        final class ToolWithRequiredAdvancedArgument {
+            @Advanced
+            @Argument(optional = false)
+            public String invalid;
+        }
+        new CommandLineArgumentParser(new ToolWithRequiredAdvancedArgument());
+    }
+
+    /***************************************************************************************
+     * Start of tests and helper classes for CommandLineParser.gatherArgumentValuesOfType()
+     ***************************************************************************************/
+
+    /**
+     * Classes and argument collections for use with CommandLineParser.gatherArgumentValuesOfType() tests below.
+     *
+     * Structured to ensure that we test support for:
+     *
+     * -distinguishing between arguments of the target type, and arguments not of the target type
+     * -distinguishing between annotated and unannotated fields of the target type
+     * -gathering arguments that are a subtype of the target type
+     * -gathering multi-valued arguments of the target type within Collection types
+     * -gathering arguments of the target type that are not specified on the command line
+     * -gathering arguments of the target type from superclasses of our tool
+     * -gathering arguments of the target type from argument collections
+     * -gathering arguments when the target type is itself a parameterized type (eg., FeatureInput<VariantContext>)
+     */
+
+    private static class GatherArgumentValuesTestSourceParent {
+        @Argument(fullName = "parentSuperTypeTarget", shortName = "parentSuperTypeTarget", doc = "")
+        private GatherArgumentValuesTargetSuperType parentSuperTypeTarget;
+
+        @Argument(fullName = "parentSubTypeTarget", shortName = "parentSubTypeTarget", doc = "")
+        private GatherArgumentValuesTargetSubType parentSubTypeTarget;
+
+        @Argument(fullName = "parentListSuperTypeTarget", shortName = "parentListSuperTypeTarget", doc = "")
+        private List<GatherArgumentValuesTargetSuperType> parentListSuperTypeTarget;
+
+        @Argument(fullName = "parentListSubTypeTarget", shortName = "parentListSubTypeTarget", doc = "")
+        private List<GatherArgumentValuesTargetSubType> parentListSubTypeTarget;
+
+        @Argument(fullName = "uninitializedParentTarget", shortName = "uninitializedParentTarget", optional = true, doc = "")
+        private GatherArgumentValuesTargetSuperType uninitializedParentTarget;
+
+        @Argument(fullName = "parentNonTargetArgument", shortName = "parentNonTargetArgument", doc = "")
+        private int parentNonTargetArgument;
+
+        private GatherArgumentValuesTargetSuperType parentUnannotatedTarget;
+
+        @ArgumentCollection
+        private GatherArgumentValuesTestSourceParentCollection parentCollection = new GatherArgumentValuesTestSourceParentCollection();
+    }
+
+    private static class GatherArgumentValuesTestSourceChild extends GatherArgumentValuesTestSourceParent {
+        @Argument(fullName = "childSuperTypeTarget", shortName = "childSuperTypeTarget", doc = "")
+        private GatherArgumentValuesTargetSuperType childSuperTypeTarget;
+
+        @Argument(fullName = "childSubTypeTarget", shortName = "childSubTypeTarget", doc = "")
+        private GatherArgumentValuesTargetSubType childSubTypeTarget;
+
+        @Argument(fullName = "childListSuperTypeTarget", shortName = "childListSuperTypeTarget", doc = "")
+        private List<GatherArgumentValuesTargetSuperType> childListSuperTypeTarget;
+
+        @Argument(fullName = "childListSubTypeTarget", shortName = "childListSubTypeTarget", doc = "")
+        private List<GatherArgumentValuesTargetSubType> childListSubTypeTarget;
+
+        @Argument(fullName = "uninitializedChildTarget", shortName = "uninitializedChildTarget", optional = true, doc = "")
+        private GatherArgumentValuesTargetSuperType uninitializedChildTarget;
+
+        @Argument(fullName = "uninitializedChildListTarget", shortName = "uninitializedChildListTarget", optional = true, doc = "")
+        private List<GatherArgumentValuesTargetSuperType> uninitializedChildListTarget;
+
+        @Argument(fullName = "childNonTargetArgument", shortName = "childNonTargetArgument", doc = "")
+        private int childNonTargetArgument;
+
+        @Argument(fullName = "childNonTargetListArgument", shortName = "childNonTargetListArgument", doc = "")
+        private List<Integer> childNonTargetListArgument;
+
+        private GatherArgumentValuesTargetSuperType childUnannotatedTarget;
+
+        @ArgumentCollection
+        private GatherArgumentValuesTestSourceChildCollection childCollection = new GatherArgumentValuesTestSourceChildCollection();
+    }
+
+    private static class GatherArgumentValuesTestSourceParentCollection {
+        private static final long serialVersionUID = 1L;
+
+        @Argument(fullName = "parentCollectionSuperTypeTarget", shortName = "parentCollectionSuperTypeTarget", doc = "")
+        private GatherArgumentValuesTargetSuperType parentCollectionSuperTypeTarget;
+
+        @Argument(fullName = "parentCollectionSubTypeTarget", shortName = "parentCollectionSubTypeTarget", doc = "")
+        private GatherArgumentValuesTargetSubType parentCollectionSubTypeTarget;
+
+        @Argument(fullName = "uninitializedParentCollectionTarget", shortName = "uninitializedParentCollectionTarget", optional = true, doc = "")
+        private GatherArgumentValuesTargetSuperType uninitializedParentCollectionTarget;
+
+        @Argument(fullName = "parentCollectionNonTargetArgument", shortName = "parentCollectionNonTargetArgument", doc = "")
+        private int parentCollectionNonTargetArgument;
+
+        private GatherArgumentValuesTargetSuperType parentCollectionUnannotatedTarget;
+    }
+
+    private static class GatherArgumentValuesTestSourceChildCollection {
+        private static final long serialVersionUID = 1L;
+
+        @Argument(fullName = "childCollectionSuperTypeTarget", shortName = "childCollectionSuperTypeTarget", doc = "")
+        private GatherArgumentValuesTargetSuperType childCollectionSuperTypeTarget;
+
+        @Argument(fullName = "childCollectionSubTypeTarget", shortName = "childCollectionSubTypeTarget", doc = "")
+        private GatherArgumentValuesTargetSubType childCollectionSubTypeTarget;
+
+        @Argument(fullName = "childCollectionListSuperTypeTarget", shortName = "childCollectionListSuperTypeTarget", doc = "")
+        private List<GatherArgumentValuesTargetSuperType> childCollectionListSuperTypeTarget;
+
+        @Argument(fullName = "uninitializedChildCollectionTarget", shortName = "uninitializedChildCollectionTarget", optional = true, doc = "")
+        private GatherArgumentValuesTargetSuperType uninitializedChildCollectionTarget;
+
+        @Argument(fullName = "childCollectionNonTargetArgument", shortName = "childCollectionNonTargetArgument", doc = "")
+        private int childCollectionNonTargetArgument;
+
+        private GatherArgumentValuesTargetSuperType childCollectionUnannotatedTarget;
+    }
+
+    /**
+     * Our tests will search for argument values of this type, subtypes of this type, and Collections of
+     * this type or its subtypes. Has a String constructor so that the argument parsing system can correctly
+     * initialize it.
+     */
+    private static class GatherArgumentValuesTargetSuperType {
+        private String value;
+
+        public GatherArgumentValuesTargetSuperType( String s ) {
+            value = s;
+        }
+
+        public String getValue() {
+            return value;
+        }
+    }
+
+    private static class GatherArgumentValuesTargetSubType extends GatherArgumentValuesTargetSuperType {
+        public GatherArgumentValuesTargetSubType( String s ) {
+            super(s);
+        }
+    }
+
+    @DataProvider(name = "gatherArgumentValuesOfTypeDataProvider")
+    public Object[][] gatherArgumentValuesOfTypeDataProvider() {
+        // Non-Collection arguments of the target type
+        final List<String> targetScalarArguments = Arrays.asList("childSuperTypeTarget", "childSubTypeTarget",
+                                                                 "parentSuperTypeTarget", "parentSubTypeTarget",
+                                                                 "childCollectionSuperTypeTarget", "childCollectionSubTypeTarget",
+                                                                 "parentCollectionSuperTypeTarget", "parentCollectionSubTypeTarget");
+        // Collection arguments of the target type
+        final List<String> targetListArguments = Arrays.asList("childListSuperTypeTarget", "childListSubTypeTarget",
+                                                               "parentListSuperTypeTarget", "parentListSubTypeTarget",
+                                                               "childCollectionListSuperTypeTarget");
+        // Arguments of the target type that we won't specify on our command line
+        final List<String> uninitializedTargetArguments = Arrays.asList("uninitializedChildTarget", "uninitializedChildListTarget",
+                                                                        "uninitializedParentTarget", "uninitializedChildCollectionTarget",
+                                                                        "uninitializedParentCollectionTarget");
+        // Arguments not of the target type
+        final List<String> nonTargetArguments = Arrays.asList("childNonTargetArgument", "parentNonTargetArgument",
+                                                              "childCollectionNonTargetArgument", "parentCollectionNonTargetArgument",
+                                                              "childNonTargetListArgument");
+
+        List<String> commandLineArguments = new ArrayList<>();
+        List<Pair<String, String>> sortedExpectedGatheredValues = new ArrayList<>();
+
+        for ( String targetScalarArgument : targetScalarArguments ) {
+            final String argumentValue = targetScalarArgument + "Value";
+
+            commandLineArguments.add("--" + targetScalarArgument);
+            commandLineArguments.add(argumentValue);
+            sortedExpectedGatheredValues.add(Pair.of(targetScalarArgument, argumentValue));
+        }
+
+        // Give each list argument multiple values
+        for ( String targetListArgument : targetListArguments ) {
+            for ( int argumentNum = 1; argumentNum <= 3; ++argumentNum ) {
+                final String argumentValue = targetListArgument + "Value" + argumentNum;
+
+                commandLineArguments.add("--" + targetListArgument);
+                commandLineArguments.add(argumentValue);
+                sortedExpectedGatheredValues.add(Pair.of(targetListArgument, argumentValue));
+            }
+        }
+
+        // Make sure the uninitialized args of the target type not included on the command line are
+        // represented in the expected output
+        for ( String uninitializedTargetArgument : uninitializedTargetArguments ) {
+            sortedExpectedGatheredValues.add(Pair.of(uninitializedTargetArgument, null));
+        }
+
+        // The non-target args are all of type int, so give them an arbitrary int value on the command line.
+        // These should not be gathered at all, so are not added to the expected output.
+        for ( String nonTargetArgument : nonTargetArguments ) {
+            commandLineArguments.add("--" + nonTargetArgument);
+            commandLineArguments.add("1");
+        }
+
+        Collections.sort(sortedExpectedGatheredValues);
+
+        return new Object[][] {{
+            commandLineArguments, sortedExpectedGatheredValues
+        }};
+    }
+
+    @Test(dataProvider = "gatherArgumentValuesOfTypeDataProvider")
+    public void testGatherArgumentValuesOfType( final List<String> commandLineArguments, final List<Pair<String, String>> sortedExpectedGatheredValues ) {
+        GatherArgumentValuesTestSourceChild argumentSource = new GatherArgumentValuesTestSourceChild();
+
+        // Parse the command line, and inject values into our test instance
+        CommandLineArgumentParser clp = new CommandLineArgumentParser(argumentSource);
+        clp.parseArguments(System.err, commandLineArguments.toArray(new String[commandLineArguments.size()]));
+
+        // Gather all argument values of type GatherArgumentValuesTargetSuperType (or Collection<GatherArgumentValuesTargetSuperType>),
+        // including subtypes.
+        List<Pair<Field, GatherArgumentValuesTargetSuperType>> gatheredArguments =
+                CommandLineParser.gatherArgumentValuesOfType(GatherArgumentValuesTargetSuperType.class, argumentSource);
+
+        // Make sure we gathered the expected number of argument values
+        Assert.assertEquals(gatheredArguments.size(), sortedExpectedGatheredValues.size(), "Gathered the wrong number of arguments");
+
+        // Make sure actual gathered argument values match expected values
+        List<Pair<String, String>> sortedActualGatheredArgumentValues = new ArrayList<>();
+        for ( Pair<Field, GatherArgumentValuesTargetSuperType> gatheredArgument : gatheredArguments ) {
+            Assert.assertNotNull(gatheredArgument.getKey().getAnnotation(Argument.class), "Gathered argument is not annotated with an @Argument annotation");
+
+            String argumentName = gatheredArgument.getKey().getAnnotation(Argument.class).fullName();
+            GatherArgumentValuesTargetSuperType argumentValue = gatheredArgument.getValue();
+
+            sortedActualGatheredArgumentValues.add(Pair.of(argumentName, argumentValue != null ? argumentValue.getValue() : null));
+        }
+        Collections.sort(sortedActualGatheredArgumentValues);
+
+        Assert.assertEquals(sortedActualGatheredArgumentValues, sortedExpectedGatheredValues,
+                            "One or more gathered argument values not correct");
+
+    }
+
+    /**
+     * Nonsensical parameterized class, just to ensure that CommandLineParser.gatherArgumentValuesOfType()
+     * can gather argument values of a generic type
+     *
+     * @param <T> meaningless type parameter
+     */
+    private static class GatherArgumentValuesParameterizedTargetType<T> {
+        private String value;
+        private T foo;
+
+        public GatherArgumentValuesParameterizedTargetType( String s ) {
+            value = s;
+            foo = null;
+        }
+
+        public String getValue() {
+            return value;
+        }
+    }
+
+    private static class GatherArgumentValuesParameterizedTypeSource {
+        @Argument(fullName = "parameterizedTypeArgument", shortName = "parameterizedTypeArgument", doc = "")
+        private GatherArgumentValuesParameterizedTargetType<Integer> parameterizedTypeArgument;
+
+        @Argument(fullName = "parameterizedTypeListArgument", shortName = "parameterizedTypeListArgument", doc = "")
+        private List<GatherArgumentValuesParameterizedTargetType<Integer>> parameterizedTypeListArgument;
+    }
+
+    @Test
+    @SuppressWarnings("rawtypes")
+    public void testGatherArgumentValuesOfTypeWithParameterizedType() {
+        GatherArgumentValuesParameterizedTypeSource argumentSource = new GatherArgumentValuesParameterizedTypeSource();
+
+        // Parse the command line, and inject values into our test instance
+        CommandLineArgumentParser clp = new CommandLineArgumentParser(argumentSource);
+        clp.parseArguments(System.err, new String[]{"--parameterizedTypeArgument", "parameterizedTypeArgumentValue",
+                                                    "--parameterizedTypeListArgument", "parameterizedTypeListArgumentValue"});
+
+        // Gather argument values of the raw type GatherArgumentValuesParameterizedTargetType, and make
+        // sure that we match fully-parameterized declarations
+        List<Pair<Field, GatherArgumentValuesParameterizedTargetType>> gatheredArguments =
+                CommandLineParser.gatherArgumentValuesOfType(GatherArgumentValuesParameterizedTargetType.class, argumentSource);
+
+        Assert.assertEquals(gatheredArguments.size(), 2, "Wrong number of arguments gathered");
+
+        Assert.assertNotNull(gatheredArguments.get(0).getKey().getAnnotation(Argument.class), "Gathered argument is not annotated with an @Argument annotation");
+        Assert.assertEquals(gatheredArguments.get(0).getKey().getAnnotation(Argument.class).fullName(), "parameterizedTypeArgument", "Wrong argument gathered");
+        Assert.assertEquals(gatheredArguments.get(0).getValue().getValue(), "parameterizedTypeArgumentValue", "Wrong value for gathered argument");
+        Assert.assertNotNull(gatheredArguments.get(1).getKey().getAnnotation(Argument.class), "Gathered argument is not annotated with an @Argument annotation");
+        Assert.assertEquals(gatheredArguments.get(1).getKey().getAnnotation(Argument.class).fullName(), "parameterizedTypeListArgument", "Wrong argument gathered");
+        Assert.assertEquals(gatheredArguments.get(1).getValue().getValue(), "parameterizedTypeListArgumentValue", "Wrong value for gathered argument");
+    }
+
+    /***************************************************************************************
+     * End of tests and helper classes for CommandLineParser.gatherArgumentValuesOfType()
+     ***************************************************************************************/
+}
diff --git a/src/test/java/org/broadinstitute/barclay/argparser/CommandLinePluginUnitTest.java b/src/test/java/org/broadinstitute/barclay/argparser/CommandLinePluginUnitTest.java
new file mode 100644
index 0000000..c7a7c68
--- /dev/null
+++ b/src/test/java/org/broadinstitute/barclay/argparser/CommandLinePluginUnitTest.java
@@ -0,0 +1,457 @@
+package org.broadinstitute.barclay.argparser;
+
+import org.testng.Assert;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import java.util.*;
+import java.util.function.Predicate;
+
+/**
+ * Test command line parser plugin functionality.
+ */
+public class CommandLinePluginUnitTest {
+
+    public static class TestPluginBase {
+    }
+
+    public static class TestPluginWithOptionalArg extends TestPluginBase {
+        public static final String optionalArgName = "optionalStringArgForPlugin";
+
+        @Argument(fullName=optionalArgName,
+                optional=true)
+        String optionalArg;
+    }
+
+    public static class TestPluginWithRequiredArg extends TestPluginBase {
+        public static final String requiredArgName = "requiredStringArgForPlugin";
+
+        @Argument(fullName=requiredArgName, optional=false)
+        String requiredArg;
+    }
+
+    public static class TestPlugin extends TestPluginBase {
+        final static String argumentName = "optionalIntegerArgForPlugin";
+        @Argument(fullName = argumentName,
+                shortName="optionalStringShortName",
+                optional=true)
+        Integer argumentForTestPlugin;
+    }
+
+    public static class TestDefaultPlugin extends TestPluginBase {
+        final static String argumentName = "optionalIntegerArgForTestDefaultPlugin";
+        @Argument(fullName = argumentName, optional=true)
+        Integer argumentForDefaultTestPlugin;
+    }
+
+    public static class TestPluginDescriptor extends CommandLinePluginDescriptor<TestPluginBase> {
+
+        private static final String pluginPackageName = "org.broadinstitute.barclay.argparser";
+        private final Class<?> pluginBaseClass = TestPluginBase.class;
+
+        public static final String testPluginArgumentName = "testPlugin";
+
+        @Argument(fullName = testPluginArgumentName, optional=true)
+        public final List<String> userPluginNames = new ArrayList<>(); // preserve order
+
+        private List<TestPluginBase> defaultPlugins = new ArrayList<>();
+
+        // Map of plugin (simple) class names to the corresponding discovered plugin instance
+        private Map<String, TestPluginBase> testPlugins = new HashMap<>();
+
+        // Set of dependent args for which we've seen values (requires predecessor)
+        private Set<String> requiredPredecessors = new HashSet<>();
+
+        public TestPluginDescriptor(final List<TestPluginBase> defaultPlugins) {
+            this.defaultPlugins = defaultPlugins;
+        }
+
+        /////////////////////////////////////////////////////////
+        // TestCommandLinePluginDescriptor implementation methods
+
+        /**
+         * Return a display name to identify this plugin to the user
+         * @return A short user-friendly name for this plugin.
+         */
+        @Override
+        public String getDisplayName() { return "testPlugin"; }
+
+        /**
+         * @return the class object for the base class of all plugins managed by this descriptor
+         */
+        @Override
+        public Class<?> getPluginClass() {return pluginBaseClass;}
+
+        /**
+         * A list of package names which will be searched for plugins managed by the descriptor.
+         * @return
+         */
+        @Override
+        public List<String> getPackageNames() {return Collections.singletonList(pluginPackageName);};
+
+        @Override
+        public Predicate<Class<?>> getClassFilter() {
+            return c -> { // don't use the Plugin base class
+                return !c.getName().equals(this.getPluginClass().getName());
+            };
+        }
+
+        // Instantiate a new ReadFilter derived object and save it in the list
+        @Override
+        public Object getInstance(final Class<?> pluggableClass) throws IllegalAccessException, InstantiationException {
+            TestPluginBase testPluginBase = null;
+            final String simpleName = pluggableClass.getSimpleName();
+
+            if (testPlugins.containsKey(simpleName)) {
+                // we found a plugin class with a name that collides with an existing class;
+                // plugin names must be unique even across packages
+                throw new IllegalArgumentException(
+                        String.format("A plugin class name collision was detected (%s/%s). " +
+                                        "Simple names of plugin classes must be unique across packages.",
+                                pluggableClass.getName(),
+                                testPlugins.get(simpleName).getClass().getName())
+                );
+            } else {
+                testPluginBase = (TestPluginBase) pluggableClass.newInstance();
+                testPlugins.put(simpleName, testPluginBase);
+            }
+            return testPluginBase;
+        }
+
+        @Override
+        public boolean isDependentArgumentAllowed(final Class<?> dependentClass) {
+            // make sure the predecessor for this dependent class was either specified
+            // on the command line or is a tool default, otherwise reject it
+            String predecessorName = dependentClass.getSimpleName();
+            boolean isAllowed = userPluginNames.contains(predecessorName);
+            if (isAllowed) {
+                // keep track of the ones we allow so we can validate later that they
+                // weren't subsequently disabled
+                requiredPredecessors.add(predecessorName);
+            }
+            return isAllowed;
+        }
+
+        /**
+         * Get the list of default plugins that were passed to this instance.
+         * @return
+         */
+        public List<Object> getDefaultInstances() {
+            ArrayList<Object> defaultList = new ArrayList<>(defaultPlugins.size());
+            defaultList.addAll(defaultPlugins);
+            return defaultList;
+        }
+
+        /**
+         * Pass back the list of ReadFilter instances that were actually seen on the
+         * command line in the same order they were specified. This list does not
+         * include the tool defaults.
+         */
+        @Override
+        public List<TestPluginBase> getAllInstances() {
+            // Add the instances in the order they were specified on the command line
+            //
+            final ArrayList<TestPluginBase> filters = new ArrayList<>(userPluginNames.size());
+            userPluginNames.forEach(s -> filters.add(testPlugins.get(s)));
+            return filters;
+        }
+
+        public Class<?> getClassForInstance(final String pluginName) { return testPlugins.get(pluginName).getClass();};
+
+        // Return the allowable values for readFilterNames/disableReadFilter
+        @Override
+        public Set<String> getAllowedValuesForDescriptorArgument(final String longArgName) {
+            if (longArgName.equals(testPluginArgumentName)) {
+                return testPlugins.keySet();
+            }
+            throw new IllegalArgumentException("Allowed values request for unrecognized string argument: " + longArgName);
+        }
+
+        /**
+         * Validate the list of arguments and reduce the list of plugins to those
+         * actually seen on the command line. This is called by the command line parser
+         * after all arguments have been parsed.
+         */
+        @Override
+        public void validateArguments() {
+            Set<String> seenNames = new HashSet<>();
+            seenNames.addAll(userPluginNames);
+
+            Set<String> validNames = new HashSet<>();
+            validNames.add(org.broadinstitute.barclay.argparser.CommandLinePluginUnitTest.TestPluginWithRequiredArg.class.getSimpleName());
+            validNames.add(org.broadinstitute.barclay.argparser.CommandLinePluginUnitTest.TestPluginWithOptionalArg.class.getSimpleName());
+            validNames.add(org.broadinstitute.barclay.argparser.CommandLinePluginUnitTest.TestPlugin.class.getSimpleName());
+
+            if (seenNames.retainAll(validNames)) {
+                throw new CommandLineException.BadArgumentValue("Illegal command line plugin specified");
+            }
+            userPluginNames.retainAll(seenNames);
+        }
+
+    }
+
+    @CommandLineProgramProperties(
+            summary = "Plugin Test",
+            oneLineSummary = "Plugin test",
+            programGroup = TestProgramGroup.class
+    )
+    public class PlugInTestObject {
+    }
+
+    @DataProvider(name="pluginTests")
+    public Object[][] pluginTests() {
+        return new Object[][]{
+                {new String[0], 0},
+                {new String[]{"--" + TestPluginDescriptor.testPluginArgumentName, TestPlugin.class.getSimpleName()}, 1}
+        };
+    }
+
+    @Test(dataProvider = "pluginTests")
+    public void testBasicPlugin(final String[] args, final int expectedInstanceCount){
+
+        PlugInTestObject plugInTest = new PlugInTestObject();
+        final CommandLineArgumentParser clp = new CommandLineArgumentParser(
+                plugInTest,
+                Collections.singletonList(new TestPluginDescriptor(Collections.singletonList(new TestDefaultPlugin()))),
+                Collections.emptySet());
+
+        Assert.assertTrue(clp.parseArguments(System.err, args));
+
+        TestPluginDescriptor pid = clp.getPluginDescriptor(TestPluginDescriptor.class);
+        Assert.assertNotNull(pid);
+
+        List<TestPluginBase> pluginBases = pid.getAllInstances();
+
+        Assert.assertEquals(pluginBases.size(), expectedInstanceCount);
+    }
+
+    @Test
+    public void testPluginUsage() {
+        PlugInTestObject plugInTest = new PlugInTestObject();
+        final CommandLineArgumentParser clp = new CommandLineArgumentParser(
+                plugInTest,
+                Collections.singletonList(new TestPluginDescriptor(Collections.singletonList(new TestDefaultPlugin()))),
+                Collections.emptySet());
+        final String out = clp.usage(true, false); // with common args, without hidden
+
+        TestPluginDescriptor pid = clp.getPluginDescriptor(TestPluginDescriptor.class);
+        Assert.assertNotNull(pid);
+
+        // Make sure TestPlugin.argumentName is listed as conditional
+        final int condIndex = out.indexOf("Conditional Arguments");
+        Assert.assertTrue(condIndex > 0);
+        final int argIndex = out.indexOf(TestPlugin.argumentName);
+        Assert.assertTrue(argIndex > condIndex);
+    }
+
+
+    @DataProvider(name="pluginsWithRequiredArguments")
+    public Object[][] pluginsWithRequiredArguments(){
+        return new Object[][]{
+                { TestPluginWithRequiredArg.class.getSimpleName(), TestPluginWithRequiredArg.requiredArgName, "fakeArgValue" }
+        };
+    }
+
+    // fail if a plugin with required arguments is specified without the corresponding required arguments
+    @Test(dataProvider = "pluginsWithRequiredArguments", expectedExceptions = CommandLineException.MissingArgument.class)
+    public void testRequiredDependentArguments(
+            final String plugin,
+            final String argName,   //unused
+            final String argValue)  //unused
+    {
+        CommandLineParser clp = new CommandLineArgumentParser(new Object(),
+                Collections.singletonList(new TestPluginDescriptor(Collections.singletonList(new TestDefaultPlugin()))),
+                Collections.emptySet());
+        String[] args = {
+                "--" + TestPluginDescriptor.testPluginArgumentName, plugin  // no args, just enable plugin
+        };
+
+        clp.parseArguments(System.out, args);
+    }
+
+    @DataProvider(name="pluginsWithArguments")
+    public Object[][] pluginsWithArguments(){
+        return new Object[][]{
+                { TestPluginWithRequiredArg.class.getSimpleName(), TestPluginWithRequiredArg.requiredArgName, "fakeArgValue" },
+                { TestPluginWithOptionalArg.class.getSimpleName(), TestPluginWithOptionalArg.optionalArgName, "fakeArgValue" }
+        };
+    }
+
+    // fail if a plugin's arguments are passed but the plugin itself is not specified
+    @Test(dataProvider = "pluginsWithArguments", expectedExceptions = CommandLineException.class)
+    public void testDanglingFilterArguments(
+            final String filter, // unused
+            final String argName,
+            final String argValue)
+    {
+        CommandLineParser clp = new CommandLineArgumentParser(new Object(),
+                Collections.singletonList(new TestPluginDescriptor(Collections.singletonList(new TestDefaultPlugin()))),
+                Collections.emptySet());
+
+        String[] args = { argName, argValue }; // plugin args are specified but no plugin actually specified
+
+        clp.parseArguments(System.out, args);
+    }
+
+    @Test
+    public void testNoPluginsSpecified() {
+        CommandLineParser clp = new CommandLineArgumentParser(new Object(),
+                Collections.singletonList(new TestPluginDescriptor(Collections.singletonList(new TestDefaultPlugin()))),
+                Collections.emptySet());
+        clp.parseArguments(System.out, new String[]{});
+
+        // get the command line read plugins
+        final TestPluginDescriptor pluginDescriptor = clp.getPluginDescriptor(TestPluginDescriptor.class);
+        final List<org.broadinstitute.barclay.argparser.CommandLinePluginUnitTest.TestPluginBase> plugins = pluginDescriptor.getAllInstances();
+        Assert.assertEquals(plugins.size(), 0);
+    }
+
+    @Test
+    public void testEnableMultiplePlugins() {
+        CommandLineParser clp = new CommandLineArgumentParser(new Object(),
+                Collections.singletonList(new TestPluginDescriptor(Collections.singletonList(new TestDefaultPlugin()))),
+                Collections.emptySet());
+        String[] args = {
+                "--" + TestPluginDescriptor.testPluginArgumentName, TestPluginWithRequiredArg.class.getSimpleName(),
+                "--" + TestPluginWithRequiredArg.requiredArgName, "fake",
+                "--" + TestPluginDescriptor.testPluginArgumentName, TestPluginWithOptionalArg.class.getSimpleName(),
+                "--" + TestPluginWithOptionalArg.optionalArgName, "alsofake"
+        };
+        clp.parseArguments(System.out, args);
+
+        // get the command line plugins
+        final TestPluginDescriptor pluginDescriptor = clp.getPluginDescriptor(TestPluginDescriptor.class);
+        final List<org.broadinstitute.barclay.argparser.CommandLinePluginUnitTest.TestPluginBase> plugins = pluginDescriptor.getAllInstances();
+        Assert.assertEquals(plugins.size(), 2);
+        Assert.assertEquals(plugins.get(0).getClass().getSimpleName(), TestPluginWithRequiredArg.class.getSimpleName());
+        Assert.assertEquals(plugins.get(1).getClass().getSimpleName(), TestPluginWithOptionalArg.class.getSimpleName());
+    }
+
+    @Test(expectedExceptions = CommandLineException.class)
+    public void testEnableNonExistentPlugin() {
+        CommandLineParser clp = new CommandLineArgumentParser(new Object(),
+                Collections.singletonList(new TestPluginDescriptor(Collections.singletonList(new TestDefaultPlugin()))),
+                Collections.emptySet());
+        clp.parseArguments(System.out, new String[] {"--" + TestPluginDescriptor.testPluginArgumentName, "nonExistentPlugin"});
+    }
+
+    ////////////////////////////////////////////
+    //Begin plugin argument name collision tests
+
+    public static class TestPluginArgCollisionBase {
+    }
+
+    public static class TestPluginArgCollision1 extends TestPluginArgCollisionBase {
+        public final static String argumentName = "argumentForTestCollisionPlugin";
+        @Argument(fullName = argumentName, optional=true)
+        Integer argumentForTestPlugin;
+    }
+
+    // This class isn't explicitly referenced anywhere, but it needs to be here so the command line parser
+    // will find it on behalf of the TestPluginArgCollisionDescriptor when running the collision test. This
+    // will result in an argument namespace collision, which is what we're testing.
+    public static class TestPluginArgCollision2 extends TestPluginArgCollisionBase {
+
+        //deliberately create an arg name collision with TestPluginArgCollision1
+        @Argument(fullName = TestPluginArgCollision1.argumentName, optional=true)
+        Integer argumentForTestPlugin;
+    }
+
+    // This descriptor should only be used for the namespace collision tests since it has a...namespace collision
+    public static class TestPluginArgCollisionDescriptor extends CommandLinePluginDescriptor<TestPluginArgCollisionBase> {
+
+        final String collisionPluginArgName = "collisionPluginName";
+
+        @Argument(fullName=collisionPluginArgName, optional = true)
+        Set<String> pluginNames = new HashSet<>();
+
+        // Map of plugin names to the corresponding instance
+        public Map<String, TestPluginArgCollisionBase> pluginInstances = new HashMap<>();
+
+        public TestPluginArgCollisionDescriptor() {}
+
+        @Override
+        public Class<?> getPluginClass() {
+            return TestPluginArgCollisionBase.class;
+        }
+
+        @Override
+        public List<String> getPackageNames() {
+            return Collections.singletonList("org.broadinstitute.barclay.argparser");
+        }
+
+        @Override
+        public Predicate<Class<?>> getClassFilter() {
+            return c -> {
+                // don't use the TestPlugin base class
+                return !c.getName().equals(this.getPluginClass().getName());
+            };
+        }
+
+        @Override
+        public Object getInstance(Class<?> pluggableClass) throws IllegalAccessException, InstantiationException {
+            final TestPluginArgCollisionBase plugin = (TestPluginArgCollisionBase) pluggableClass.newInstance();
+            pluginInstances.put(pluggableClass.getSimpleName(), plugin);
+            return plugin;
+        }
+
+        @Override
+        public Set<String> getAllowedValuesForDescriptorArgument(String longArgName) {
+            if (longArgName.equals(collisionPluginArgName) ){
+                return pluginInstances.keySet();
+            }
+            throw new IllegalArgumentException("Allowed values request for unrecognized string argument: " + longArgName);
+
+        }
+        @Override
+        public boolean isDependentArgumentAllowed(Class<?> targetPluginClass) {
+            return true;
+        }
+
+        @Override
+        public void validateArguments() {
+            // remove the un-specified plugin instances
+            Map<String, TestPluginArgCollisionBase> requestedPlugins = new HashMap<>();
+            pluginNames.forEach(s -> {
+                TestPluginArgCollisionBase trf = pluginInstances.get(s);
+                if (null == trf) {
+                    throw new CommandLineException("Unrecognized test plugin name: " + s);
+                }
+                else {
+                    requestedPlugins.put(s, trf);
+                }
+            });
+            pluginInstances = requestedPlugins;
+
+            // now validate that each plugin specified is valid (has a corresponding instance)
+            Assert.assertEquals(pluginNames.size(), pluginInstances.size());
+        }
+
+        /**
+         * Get the list of default plugins that were passed to this instance.
+         * @return
+         */
+        @Override
+        public List<Object> getDefaultInstances() { return null; }
+
+        @Override
+        public List<TestPluginArgCollisionBase> getAllInstances() {
+            List<TestPluginArgCollisionBase> pluginList = new ArrayList<>();
+            pluginList.addAll(pluginInstances.values());
+            return pluginList;
+        }
+
+        public Class<?> getClassForInstance(final String pluginName) { return pluginInstances.get(pluginName).getClass();};
+    }
+
+    @Test(expectedExceptions=CommandLineException.CommandLineParserInternalException.class)
+    public void testPluginArgumentNameCollision(){
+        PlugInTestObject PlugInTestObject = new PlugInTestObject();
+        // just the act of passing this descriptor to the parser should cause the collision
+        new CommandLineArgumentParser(
+                PlugInTestObject,
+                Collections.singletonList(new TestPluginArgCollisionDescriptor()),
+                Collections.emptySet());
+    }
+
+}
diff --git a/src/test/java/org/broadinstitute/barclay/argparser/LegacyCommandLineArgumentParserTest.java b/src/test/java/org/broadinstitute/barclay/argparser/LegacyCommandLineArgumentParserTest.java
new file mode 100644
index 0000000..12e3ac0
--- /dev/null
+++ b/src/test/java/org/broadinstitute/barclay/argparser/LegacyCommandLineArgumentParserTest.java
@@ -0,0 +1,933 @@
+/*
+ * The MIT License
+ *
+ * Copyright (c) 2009 The Broad Institute
+ *
+ * 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 org.broadinstitute.barclay.argparser;
+
+import org.testng.Assert;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import java.io.*;
+import java.util.*;
+
+public class LegacyCommandLineArgumentParserTest {
+
+    enum FrobnicationFlavor {
+        FOO, BAR, BAZ
+    }
+
+    @CommandLineProgramProperties(
+            summary = "Usage: frobnicate [options] input-file output-file\n\nRead input-file, frobnicate it, and write frobnicated results to output-file\n",
+            oneLineSummary = "Read input-file, frobnicate it, and write frobnicated results to output-file",
+            programGroup = TestProgramGroup.class
+    )
+    class FrobnicateOptions {
+
+        @PositionalArguments(minElements = 2, maxElements = 2)
+        public List<File> positionalArguments = new ArrayList<File>();
+
+        @Argument(shortName = "T", doc = "Frobnication threshold setting.")
+        public Integer FROBNICATION_THRESHOLD = 20;
+
+        @Argument
+        public FrobnicationFlavor FROBNICATION_FLAVOR;
+
+        @Argument(doc = "Allowed shmiggle types.", minElements = 1, maxElements = 3)
+        public List<String> SHMIGGLE_TYPE = new ArrayList<String>();
+
+        @Argument
+        public Boolean TRUTHINESS;
+    }
+
+    @CommandLineProgramProperties(
+            summary = "Usage: frobnicate [options] input-file output-file\n\nRead input-file, frobnicate it, and write frobnicated results to output-file\n",
+            oneLineSummary = "Read input-file, frobnicate it, and write frobnicated results to output-file",
+            programGroup = TestProgramGroup.class
+    )
+    class FrobnicateOptionsWithNullList {
+
+        @PositionalArguments(minElements = 2, maxElements = 2)
+        public List<File> positionalArguments = new ArrayList<File>();
+
+        @Argument(shortName = "T", doc = "Frobnication threshold setting.")
+        public Integer FROBNICATION_THRESHOLD = 20;
+
+        @Argument
+        public FrobnicationFlavor FROBNICATION_FLAVOR;
+
+        @Argument(doc = "Allowed shmiggle types.", minElements = 0, maxElements = 3)
+        public List<String> SHMIGGLE_TYPE = new ArrayList<String>();
+
+        @Argument
+        public Boolean TRUTHINESS;
+    }
+
+    @CommandLineProgramProperties(
+            summary = "Usage: framistat [options]\n\nCompute the plebnick of the freebozzle.\n",
+            oneLineSummary = "ompute the plebnick of the freebozzle",
+            programGroup = TestProgramGroup.class
+    )
+    class OptionsWithoutPositional {
+        public static final int DEFAULT_FROBNICATION_THRESHOLD = 20;
+        @Argument(shortName = "T", doc = "Frobnication threshold setting.")
+        public Integer FROBNICATION_THRESHOLD = DEFAULT_FROBNICATION_THRESHOLD;
+
+        @Argument
+        public FrobnicationFlavor FROBNICATION_FLAVOR;
+
+        @Argument(doc = "Allowed shmiggle types.", minElements = 1, maxElements = 3)
+        public List<String> SHMIGGLE_TYPE = new ArrayList<String>();
+
+        @Argument
+        public Boolean TRUTHINESS;
+    }
+
+    class OptionsWithCaseClash {
+        @Argument
+        public String FROB;
+        @Argument
+        public String frob;
+    }
+
+    class OptionsWithSameShortName {
+        @Argument(shortName = "SAME_SHORT_NAME", optional = true)
+        public String SAME_SHORT_NAME;
+        @Argument(shortName = "SOMETHING_ELSE", optional = true)
+        public String DIFF_SHORT_NAME;
+    }
+
+    class MutexOptions {
+        @Argument(mutex = {"M", "N", "Y", "Z"})
+        public String A;
+        @Argument(mutex = {"M", "N", "Y", "Z"})
+        public String B;
+        @Argument(mutex = {"A", "B", "Y", "Z"})
+        public String M;
+        @Argument(mutex = {"A", "B", "Y", "Z"})
+        public String N;
+        @Argument(mutex = {"A", "B", "M", "N"})
+        public String Y;
+        @Argument(mutex = {"A", "B", "M", "N"})
+        public String Z;
+
+    }
+
+    @Test
+    public void testUsage() {
+        final FrobnicateOptions fo = new FrobnicateOptions();
+        final LegacyCommandLineArgumentParser clp = new LegacyCommandLineArgumentParser(fo);
+        clp.usage(false, true);
+    }
+
+    @Test
+    public void testUsageWithDefault() {
+        final FrobnicateOptions fo = new FrobnicateOptions();
+        final LegacyCommandLineArgumentParser clp = new LegacyCommandLineArgumentParser(fo);
+        clp.usage(true, true);
+    }
+
+    @Test
+    public void testUsageWithoutPositional() {
+        final OptionsWithoutPositional fo = new OptionsWithoutPositional();
+        final LegacyCommandLineArgumentParser clp = new LegacyCommandLineArgumentParser(fo);
+        clp.usage(false, true);
+    }
+
+    @Test
+    public void testUsageWithoutPositionalWithDefault() {
+        final OptionsWithoutPositional fo = new OptionsWithoutPositional();
+        final LegacyCommandLineArgumentParser clp = new LegacyCommandLineArgumentParser(fo);
+        clp.usage(true, true);
+    }
+
+    /**
+     * If the short name is set to be the same as the long name we still want the argument to appear in the commandLine.
+     */
+    @Test
+    public void testForIdenticalShortName() {
+        final String[] args = {
+                "SAME_SHORT_NAME=FOO",
+                "SOMETHING_ELSE=BAR"
+        };
+        final OptionsWithSameShortName fo = new OptionsWithSameShortName();
+        final LegacyCommandLineArgumentParser clp = new LegacyCommandLineArgumentParser(fo);
+        clp.parseArguments(System.err, args);
+        final String commandLine = clp.getCommandLine();
+        Assert.assertTrue(commandLine.contains("DIFF_SHORT_NAME"));
+        Assert.assertTrue(commandLine.contains("SAME_SHORT_NAME"));
+    }
+
+
+    @Test
+    public void testPositive() {
+        final String[] args = {
+                "T=17",
+                "FROBNICATION_FLAVOR=BAR",
+                "TRUTHINESS=False",
+                "SHMIGGLE_TYPE=shmiggle1",
+                "SHMIGGLE_TYPE=shmiggle2",
+                "positional1",
+                "positional2",
+        };
+        final FrobnicateOptions fo = new FrobnicateOptions();
+        final LegacyCommandLineArgumentParser clp = new LegacyCommandLineArgumentParser(fo);
+        Assert.assertTrue(clp.parseArguments(System.err, args));
+        Assert.assertEquals(fo.positionalArguments.size(), 2);
+        final File[] expectedPositionalArguments = {new File("positional1"), new File("positional2")};
+        Assert.assertEquals(fo.positionalArguments.toArray(), expectedPositionalArguments);
+        Assert.assertEquals(fo.FROBNICATION_THRESHOLD.intValue(), 17);
+        Assert.assertEquals(fo.FROBNICATION_FLAVOR, FrobnicationFlavor.BAR);
+        Assert.assertEquals(fo.SHMIGGLE_TYPE.size(), 2);
+        final String[] expectedShmiggleTypes = {"shmiggle1", "shmiggle2"};
+        Assert.assertEquals(fo.SHMIGGLE_TYPE.toArray(), expectedShmiggleTypes);
+        Assert.assertFalse(fo.TRUTHINESS);
+    }
+
+    /**
+     * Allow a whitespace btw equal sign and option value.
+     */
+    @Test
+    public void testPositiveWithSpaces() {
+        final String[] args = {
+                "T=", "17",
+                "FROBNICATION_FLAVOR=", "BAR",
+                "TRUTHINESS=", "False",
+                "SHMIGGLE_TYPE=", "shmiggle1",
+                "SHMIGGLE_TYPE=", "shmiggle2",
+                "positional1",
+                "positional2",
+        };
+        final FrobnicateOptions fo = new FrobnicateOptions();
+        final LegacyCommandLineArgumentParser clp = new LegacyCommandLineArgumentParser(fo);
+        Assert.assertTrue(clp.parseArguments(System.err, args));
+        Assert.assertEquals(fo.positionalArguments.size(), 2);
+        final File[] expectedPositionalArguments = {new File("positional1"), new File("positional2")};
+        Assert.assertEquals(fo.positionalArguments.toArray(), expectedPositionalArguments);
+        Assert.assertEquals(fo.FROBNICATION_THRESHOLD.intValue(), 17);
+        Assert.assertEquals(fo.FROBNICATION_FLAVOR, FrobnicationFlavor.BAR);
+        Assert.assertEquals(fo.SHMIGGLE_TYPE.size(), 2);
+        final String[] expectedShmiggleTypes = {"shmiggle1", "shmiggle2"};
+        Assert.assertEquals(fo.SHMIGGLE_TYPE.toArray(), expectedShmiggleTypes);
+        Assert.assertFalse(fo.TRUTHINESS);
+    }
+
+    @Test
+    public void testPositiveWithoutPositional() {
+        final String[] args = {
+                "T=17",
+                "FROBNICATION_FLAVOR=BAR",
+                "TRUTHINESS=False",
+                "SHMIGGLE_TYPE=shmiggle1",
+                "SHMIGGLE_TYPE=shmiggle2",
+        };
+        final OptionsWithoutPositional fo = new OptionsWithoutPositional();
+        final LegacyCommandLineArgumentParser clp = new LegacyCommandLineArgumentParser(fo);
+        Assert.assertTrue(clp.parseArguments(System.err, args));
+        Assert.assertEquals(fo.FROBNICATION_THRESHOLD.intValue(), 17);
+        Assert.assertEquals(fo.FROBNICATION_FLAVOR, FrobnicationFlavor.BAR);
+        Assert.assertEquals(fo.SHMIGGLE_TYPE.size(), 2);
+        final String[] expectedShmiggleTypes = {"shmiggle1", "shmiggle2"};
+        Assert.assertEquals(fo.SHMIGGLE_TYPE.toArray(), expectedShmiggleTypes);
+        Assert.assertFalse(fo.TRUTHINESS);
+    }
+
+    /**
+     * If last character of command line is the equal sign in an option=value pair,
+     * make sure no crash, and that the value is empty string.
+     */
+    @Test
+    public void testPositiveTerminalEqualSign() {
+        final String[] args = {
+                "T=17",
+                "FROBNICATION_FLAVOR=BAR",
+                "TRUTHINESS=False",
+                "SHMIGGLE_TYPE=shmiggle1",
+                "SHMIGGLE_TYPE=",
+        };
+        final OptionsWithoutPositional fo = new OptionsWithoutPositional();
+        final LegacyCommandLineArgumentParser clp = new LegacyCommandLineArgumentParser(fo);
+        Assert.assertTrue(clp.parseArguments(System.err, args));
+        Assert.assertEquals(fo.FROBNICATION_THRESHOLD.intValue(), 17);
+        Assert.assertEquals(fo.FROBNICATION_FLAVOR, FrobnicationFlavor.BAR);
+        Assert.assertEquals(fo.SHMIGGLE_TYPE.size(), 2);
+        final String[] expectedShmiggleTypes = {"shmiggle1", ""};
+        Assert.assertEquals(fo.SHMIGGLE_TYPE.toArray(), expectedShmiggleTypes);
+        Assert.assertFalse(fo.TRUTHINESS);
+    }
+
+    @Test
+    public void testDefault() {
+        final String[] args = {
+                "FROBNICATION_FLAVOR=BAR",
+                "TRUTHINESS=False",
+                "SHMIGGLE_TYPE=shmiggle1",
+                "SHMIGGLE_TYPE=shmiggle2",
+                "positional1",
+                "positional2",
+        };
+        final FrobnicateOptions fo = new FrobnicateOptions();
+        final LegacyCommandLineArgumentParser clp = new LegacyCommandLineArgumentParser(fo);
+        Assert.assertTrue(clp.parseArguments(System.err, args));
+        Assert.assertEquals(fo.FROBNICATION_THRESHOLD.intValue(), 20);
+    }
+
+    @Test
+    public void testMissingRequiredArgument() {
+        final String[] args = {
+                "TRUTHINESS=False",
+                "SHMIGGLE_TYPE=shmiggle1",
+                "SHMIGGLE_TYPE=shmiggle2",
+                "positional1",
+                "positional2",
+        };
+        final FrobnicateOptions fo = new FrobnicateOptions();
+        final LegacyCommandLineArgumentParser clp = new LegacyCommandLineArgumentParser(fo);
+        Assert.assertFalse(clp.parseArguments(System.err, args));
+    }
+
+    @Test
+    public void testBadValue() {
+        final String[] args = {
+                "FROBNICATION_THRESHOLD=ABC",
+                "FROBNICATION_FLAVOR=BAR",
+                "TRUTHINESS=False",
+                "SHMIGGLE_TYPE=shmiggle1",
+                "SHMIGGLE_TYPE=shmiggle2",
+                "positional1",
+                "positional2",
+        };
+        final FrobnicateOptions fo = new FrobnicateOptions();
+        final LegacyCommandLineArgumentParser clp = new LegacyCommandLineArgumentParser(fo);
+        Assert.assertFalse(clp.parseArguments(System.err, args));
+    }
+
+    @Test
+    public void testBadEnumValue() {
+        final String[] args = {
+                "FROBNICATION_FLAVOR=HiMom",
+                "TRUTHINESS=False",
+                "SHMIGGLE_TYPE=shmiggle1",
+                "SHMIGGLE_TYPE=shmiggle2",
+                "positional1",
+                "positional2",
+        };
+        final FrobnicateOptions fo = new FrobnicateOptions();
+        final LegacyCommandLineArgumentParser clp = new LegacyCommandLineArgumentParser(fo);
+        Assert.assertFalse(clp.parseArguments(System.err, args));
+    }
+
+    @Test
+    public void testNotEnoughOfListOption() {
+        final String[] args = {
+                "FROBNICATION_FLAVOR=BAR",
+                "TRUTHINESS=False",
+                "positional1",
+                "positional2",
+        };
+        final FrobnicateOptions fo = new FrobnicateOptions();
+        final LegacyCommandLineArgumentParser clp = new LegacyCommandLineArgumentParser(fo);
+        Assert.assertFalse(clp.parseArguments(System.err, args));
+    }
+
+    @Test
+    public void testTooManyListOption() {
+        final String[] args = {
+                "FROBNICATION_FLAVOR=BAR",
+                "TRUTHINESS=False",
+                "SHMIGGLE_TYPE=shmiggle1",
+                "SHMIGGLE_TYPE=shmiggle2",
+                "SHMIGGLE_TYPE=shmiggle3",
+                "SHMIGGLE_TYPE=shmiggle4",
+                "positional1",
+                "positional2",
+        };
+        final FrobnicateOptions fo = new FrobnicateOptions();
+        final LegacyCommandLineArgumentParser clp = new LegacyCommandLineArgumentParser(fo);
+        Assert.assertFalse(clp.parseArguments(System.err, args));
+    }
+
+    @Test
+    public void testTooManyPositional() {
+        final String[] args = {
+                "FROBNICATION_FLAVOR=BAR",
+                "TRUTHINESS=False",
+                "SHMIGGLE_TYPE=shmiggle1",
+                "SHMIGGLE_TYPE=shmiggle2",
+                "positional1",
+                "positional2",
+                "positional3",
+        };
+        final FrobnicateOptions fo = new FrobnicateOptions();
+        final LegacyCommandLineArgumentParser clp = new LegacyCommandLineArgumentParser(fo);
+        Assert.assertFalse(clp.parseArguments(System.err, args));
+    }
+
+    @Test
+    public void testNotEnoughPositional() {
+        final String[] args = {
+                "FROBNICATION_FLAVOR=BAR",
+                "TRUTHINESS=False",
+                "SHMIGGLE_TYPE=shmiggle1",
+                "SHMIGGLE_TYPE=shmiggle2",
+        };
+        final FrobnicateOptions fo = new FrobnicateOptions();
+        final LegacyCommandLineArgumentParser clp = new LegacyCommandLineArgumentParser(fo);
+        Assert.assertFalse(clp.parseArguments(System.err, args));
+    }
+
+    @Test
+    public void testUnexpectedPositional() {
+        final String[] args = {
+                "T=17",
+                "FROBNICATION_FLAVOR=BAR",
+                "TRUTHINESS=False",
+                "SHMIGGLE_TYPE=shmiggle1",
+                "SHMIGGLE_TYPE=shmiggle2",
+                "positional"
+        };
+        final OptionsWithoutPositional fo = new OptionsWithoutPositional();
+        final LegacyCommandLineArgumentParser clp = new LegacyCommandLineArgumentParser(fo);
+        Assert.assertFalse(clp.parseArguments(System.err, args));
+    }
+
+    @Test(expectedExceptions = CommandLineException.CommandLineParserInternalException.class)
+    public void testOptionDefinitionCaseClash() {
+        final OptionsWithCaseClash options = new OptionsWithCaseClash();
+        new LegacyCommandLineArgumentParser(options);
+        Assert.fail("Should not be reached.");
+    }
+
+    @Test
+    public void testOptionUseCaseClash() {
+        final String[] args = {
+                "FROBNICATION_FLAVOR=BAR",
+                "FrOBNICATION_fLAVOR=BAR",
+        };
+        final FrobnicateOptions fo = new FrobnicateOptions();
+        final LegacyCommandLineArgumentParser clp = new LegacyCommandLineArgumentParser(fo);
+        Assert.assertFalse(clp.parseArguments(System.err, args));
+    }
+
+    @Test
+    public void testNullValue() {
+        final String[] args = {
+                "FROBNICATION_THRESHOLD=null",
+                "FROBNICATION_FLAVOR=BAR",
+                "TRUTHINESS=False",
+                "SHMIGGLE_TYPE=null",
+                "positional1",
+                "positional2",
+        };
+
+        final FrobnicateOptionsWithNullList fownl = new FrobnicateOptionsWithNullList();
+        fownl.SHMIGGLE_TYPE.add("shmiggle1"); //providing null value should clear this list
+
+        final LegacyCommandLineArgumentParser clp = new LegacyCommandLineArgumentParser(fownl);
+        Assert.assertTrue(clp.parseArguments(System.err, args));
+        Assert.assertEquals(fownl.positionalArguments.size(), 2);
+        final File[] expectedPositionalArguments = {new File("positional1"), new File("positional2")};
+        Assert.assertEquals(fownl.positionalArguments.toArray(), expectedPositionalArguments);
+        Assert.assertEquals(fownl.FROBNICATION_THRESHOLD, null); //test null value
+        Assert.assertEquals(fownl.SHMIGGLE_TYPE.size(), 0); //test null value for list
+        Assert.assertFalse(fownl.TRUTHINESS);
+
+        //verify that required arg can't be set to null
+        args[2] = "TRUTHINESS=null";
+        final LegacyCommandLineArgumentParser clp2 = new LegacyCommandLineArgumentParser(fownl);
+        Assert.assertFalse(clp2.parseArguments(System.err, args));
+
+        //verify that positional arg can't be set to null
+        args[2] = "TRUTHINESS=False";
+        args[4] = "null";
+        final LegacyCommandLineArgumentParser clp3 = new LegacyCommandLineArgumentParser(fownl);
+        Assert.assertFalse(clp3.parseArguments(System.err, args));
+
+    }
+
+
+    @Test
+    public void testOptionsFile() throws Exception {
+        final File optionsFile = File.createTempFile("clp.", ".options");
+        optionsFile.deleteOnExit();
+        final PrintWriter writer = new PrintWriter(optionsFile);
+        writer.println("T=18");
+        writer.println("TRUTHINESS=True");
+        writer.println("SHMIGGLE_TYPE=shmiggle0");
+        writer.println("STRANGE_OPTION=shmiggle0");
+        writer.close();
+        final String[] args = {
+                "OPTIONS_FILE=" + optionsFile.getPath(),
+                // Multiple options files are allowed
+                "OPTIONS_FILE=" + optionsFile.getPath(),
+                "T=17",
+                "FROBNICATION_FLAVOR=BAR",
+                "TRUTHINESS=False",
+                "SHMIGGLE_TYPE=shmiggle1",
+                "positional1",
+                "positional2",
+        };
+        final FrobnicateOptions fo = new FrobnicateOptions();
+        final LegacyCommandLineArgumentParser clp = new LegacyCommandLineArgumentParser(fo);
+        Assert.assertTrue(clp.parseArguments(System.err, args));
+        Assert.assertEquals(fo.positionalArguments.size(), 2);
+        final File[] expectedPositionalArguments = {new File("positional1"), new File("positional2")};
+        Assert.assertEquals(fo.positionalArguments.toArray(), expectedPositionalArguments);
+        Assert.assertEquals(fo.FROBNICATION_THRESHOLD.intValue(), 17);
+        Assert.assertEquals(fo.FROBNICATION_FLAVOR, FrobnicationFlavor.BAR);
+        Assert.assertEquals(fo.SHMIGGLE_TYPE.size(), 3);
+        final String[] expectedShmiggleTypes = {"shmiggle0", "shmiggle0", "shmiggle1"};
+        Assert.assertEquals(fo.SHMIGGLE_TYPE.toArray(), expectedShmiggleTypes);
+        Assert.assertFalse(fo.TRUTHINESS);
+    }
+
+
+    /**
+     * In an options file, should not be allowed to override an option set on the command line
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testOptionsFileWithDisallowedOverride() throws Exception {
+        final File optionsFile = File.createTempFile("clp.", ".options");
+        optionsFile.deleteOnExit();
+        final PrintWriter writer = new PrintWriter(optionsFile);
+        writer.println("T=18");
+        writer.close();
+        final String[] args = {
+                "T=17",
+                "OPTIONS_FILE=" + optionsFile.getPath()
+        };
+        final FrobnicateOptions fo = new FrobnicateOptions();
+        final LegacyCommandLineArgumentParser clp = new LegacyCommandLineArgumentParser(fo);
+        Assert.assertFalse(clp.parseArguments(System.err, args));
+    }
+
+    @DataProvider(name = "mutexScenarios")
+    public Object[][] mutexScenarios() {
+        return new Object[][]{
+                {"pass", new String[]{"A=1", "B=2"}, true},
+                {"no args", new String[0], false},
+                {"1 of group required", new String[]{"A=1"}, false},
+                {"mutex", new String[]{"A=1", "Y=3"}, false},
+                {"mega mutex", new String[]{"A=1", "B=2", "Y=3", "Z=1", "M=2", "N=3"}, false}
+        };
+    }
+
+    @Test(dataProvider = "mutexScenarios")
+    public void testMutex(final String testName, final String[] args, final boolean expected) {
+        final MutexOptions o = new MutexOptions();
+        final LegacyCommandLineArgumentParser clp = new LegacyCommandLineArgumentParser(o);
+        Assert.assertEquals(clp.parseArguments(System.err, args), expected);
+    }
+
+    class UninitializedCollectionOptions {
+        @Argument
+        public List<String> LIST;
+        @Argument
+        public ArrayList<String> ARRAY_LIST;
+        @Argument
+        public HashSet<String> HASH_SET;
+        @PositionalArguments
+        public Collection<File> COLLECTION;
+
+    }
+
+    @Test
+    public void testUninitializedCollections() {
+        final UninitializedCollectionOptions o = new UninitializedCollectionOptions();
+        final LegacyCommandLineArgumentParser clp = new LegacyCommandLineArgumentParser(o);
+        final String[] args = {"LIST=L1", "LIST=L2", "ARRAY_LIST=S1", "HASH_SET=HS1", "P1", "P2"};
+        Assert.assertTrue(clp.parseArguments(System.err, args));
+        Assert.assertEquals(o.LIST.size(), 2);
+        Assert.assertEquals(o.ARRAY_LIST.size(), 1);
+        Assert.assertEquals(o.HASH_SET.size(), 1);
+        Assert.assertEquals(o.COLLECTION.size(), 2);
+    }
+
+    class UninitializedCollectionThatCannotBeAutoInitializedOptions {
+        @Argument
+        public Set<String> SET;
+    }
+
+    @Test(expectedExceptions = CommandLineException.CommandLineParserInternalException.class)
+    public void testCollectionThatCannotBeAutoInitialized() {
+        final UninitializedCollectionThatCannotBeAutoInitializedOptions o = new UninitializedCollectionThatCannotBeAutoInitializedOptions();
+        new LegacyCommandLineArgumentParser(o);
+        Assert.fail("Exception should have been thrown");
+    }
+
+    class CollectionWithDefaultValuesOptions {
+        @Argument
+        public List<String> LIST = makeList("foo", "bar");
+    }
+
+    @Test
+    public void testClearDefaultValuesFromListOption() {
+        final CollectionWithDefaultValuesOptions o = new CollectionWithDefaultValuesOptions();
+        final LegacyCommandLineArgumentParser clp = new LegacyCommandLineArgumentParser(o);
+        final String[] args = {"LIST=null"};
+        Assert.assertTrue(clp.parseArguments(System.err, args));
+        Assert.assertEquals(o.LIST.size(), 0);
+    }
+
+    @Test
+    public void testClearDefaultValuesFromListOptionAndAddNew() {
+        final CollectionWithDefaultValuesOptions o = new CollectionWithDefaultValuesOptions();
+        final LegacyCommandLineArgumentParser clp = new LegacyCommandLineArgumentParser(o);
+        final String[] args = {"LIST=null", "LIST=baz", "LIST=frob"};
+        Assert.assertTrue(clp.parseArguments(System.err, args));
+        Assert.assertEquals(o.LIST, makeList("baz", "frob"));
+    }
+
+    @Test
+    public void testAddToDefaultValuesListOption() {
+        final CollectionWithDefaultValuesOptions o = new CollectionWithDefaultValuesOptions();
+        final LegacyCommandLineArgumentParser clp = new LegacyCommandLineArgumentParser(o);
+        final String[] args = {"LIST=baz", "LIST=frob"};
+        Assert.assertTrue(clp.parseArguments(System.err, args));
+        Assert.assertEquals(o.LIST, makeList("foo", "bar", "baz", "frob"));
+    }
+
+    class ArgsCollection {
+        @Argument(fullName = "arg1")
+        public int Arg1;
+    }
+
+    class ArgsCollectionContainer {
+
+        public ArgsCollectionContainer(){}
+
+        @ArgumentCollection
+        public ArgsCollection default_args = new ArgsCollection();
+
+        @Argument(fullName = "somenumber",shortName = "n")
+        public int someNumber = 0;
+    }
+
+    @Test
+    public void testArgumentCollectionFlat() {
+        final ArgsCollectionContainer o = new ArgsCollectionContainer();
+        final LegacyCommandLineArgumentParser clp = new LegacyCommandLineArgumentParser(o);
+
+        String[] args = {"arg1=42", "somenumber=12"};
+        Assert.assertTrue(clp.parseArguments(System.err, args));
+        Assert.assertEquals(o.default_args.Arg1, 42);
+        Assert.assertEquals(o.someNumber, 12);
+    }
+
+    public abstract class ReferenceArgumentCollection {
+        public abstract File getReferenceFile();
+    }
+
+    public class OptionalReferenceArgumentCollection extends ReferenceArgumentCollection {
+
+        @Argument(doc="A reference is optional", optional=true, common=true)
+        public File REFERENCE_SEQUENCE;
+
+        public File getReferenceFile() { return REFERENCE_SEQUENCE; };
+    }
+
+    public class RequiredReferenceArgumentCollection extends ReferenceArgumentCollection {
+
+        @Argument(doc="A reference is required", optional=false, common=false)
+        public File REFERENCE_SEQUENCE;
+
+        public File getReferenceFile() { return REFERENCE_SEQUENCE; };
+    }
+
+    class ArgCollectionToolBase {
+        @ArgumentCollection
+        public ReferenceArgumentCollection referenceSequence =
+                requiresReference() ?
+                        new RequiredReferenceArgumentCollection() :
+                        new OptionalReferenceArgumentCollection();
+
+        public boolean requiresReference() { return false;}
+    }
+
+    class ToolWithRequiredReference extends ArgCollectionToolBase {
+        public boolean requiresReference() { return true; }
+    }
+
+    class ToolWithOptionalReference extends ArgCollectionToolBase {
+        public boolean requiresReference() { return false; }
+    }
+
+    // Tool with optional reference, no reference provided
+    @Test
+    public void testArgumentCollectionNoReferenceProvidedForOptional(){
+        final ToolWithOptionalReference o = new ToolWithOptionalReference();
+        final LegacyCommandLineArgumentParser clp = new LegacyCommandLineArgumentParser(o);
+
+        String[] args = {}; // no reference provided
+        Assert.assertTrue(clp.parseArguments(System.err, args));
+        Assert.assertNull(o.referenceSequence.getReferenceFile());
+    }
+
+    // Tool with optional reference, with reference provided
+    @Test
+    public void testArgumentCollectionWithReferenceProvidedForOptional() {
+        final ToolWithOptionalReference o = new ToolWithOptionalReference();
+        final LegacyCommandLineArgumentParser clp = new LegacyCommandLineArgumentParser(o);
+
+        String[] args = {"REFERENCE_SEQUENCE=ref.fasta"}; // with reference provided
+        Assert.assertTrue(clp.parseArguments(System.err, args));
+        Assert.assertEquals(o.referenceSequence.getReferenceFile().getName(), "ref.fasta");
+    }
+
+    // Tool with required reference, no reference provided (parseArguments issues error message; returns false)
+    @Test
+    public void testArgumentCollectionNoReferenceProvidedForRequired() {
+        final ToolWithRequiredReference o = new ToolWithRequiredReference();
+        final LegacyCommandLineArgumentParser clp = new LegacyCommandLineArgumentParser(o);
+
+        String[] args = {}; // no reference provided
+        Assert.assertFalse(clp.parseArguments(System.err, args));
+    }
+
+    // Tool with required reference, with reference provided
+    @Test
+    public void testArgumentCollectionWithReferenceProvidedForRequired(){
+        final ToolWithRequiredReference o = new ToolWithRequiredReference();
+        final LegacyCommandLineArgumentParser clp = new LegacyCommandLineArgumentParser(o);
+
+        String[] args = {"REFERENCE_SEQUENCE=ref.fasta"}; // with reference provided
+        Assert.assertTrue(clp.parseArguments(System.err, args));
+        Assert.assertEquals(o.referenceSequence.getReferenceFile().getName(), "ref.fasta");
+    }
+
+    //////////////////////////////
+    // Nested argument collections
+
+    class ArgumentCollectionContainerInner {
+        @ArgumentCollection
+        ArgsCollection innerContainerArg = new ArgsCollection();
+    }
+
+    class ArgumentCollectionContainerOuter {
+        @Argument(fullName = "outerContainerArg")
+        public int outerContainerArg;
+
+        @ArgumentCollection
+        ArgumentCollectionContainerInner argInner = new ArgumentCollectionContainerInner();
+    }
+
+    class ToolWithNestedArgumentCollection  {
+        @ArgumentCollection
+        public ArgumentCollectionContainerOuter argOuter =  new ArgumentCollectionContainerOuter();
+    }
+
+    @Test
+    public void testArgumentCollectionNested() {
+        final ToolWithNestedArgumentCollection o = new ToolWithNestedArgumentCollection();
+        final LegacyCommandLineArgumentParser clp = new LegacyCommandLineArgumentParser(o);
+
+        String[] args = {"outerContainerArg=17", "arg1=92"};
+        Assert.assertTrue(clp.parseArguments(System.err, args));
+        Assert.assertEquals(o.argOuter.outerContainerArg, 17);
+        Assert.assertEquals(o.argOuter.argInner.innerContainerArg.Arg1, 92);
+    }
+
+    private List<String> makeList(final String... list) {
+        final List<String> result = new ArrayList<>();
+        Collections.addAll(result, list);
+        return result;
+    }
+
+    @DataProvider(name = "testHtmlEscapeData")
+    public Object[][] testHtmlEscapeData() {
+        final List<Object[]> retval = new ArrayList<>();
+
+        retval.add(new Object[]{"<", "<"});
+        retval.add(new Object[]{"x<y", "x<y"});
+        retval.add(new Object[]{"x<y<z", "x<y<z"});
+        retval.add(new Object[]{"\n", "<p>"});
+        retval.add(new Object[]{"<html> x<y </html> y< <strong> x </strong>","<html> x<y </html> y< <strong> x </strong>"});
+
+        return retval.toArray(new Object[0][]);
+    }
+
+    @Test(dataProvider = "testHtmlEscapeData")
+    public void testHtmlUnescape(final String expected, final String html) {
+        Assert.assertEquals(LegacyCommandLineArgumentParser.htmlUnescape(html), expected, "problems");
+    }
+
+    @DataProvider(name = "testHTMLConverter")
+    public Object[][] testHTMLConverterData() {
+        final List<Object[]> retval = new ArrayList<>();
+
+        retval.add(new Object[]{"hello", "hello"});
+        retval.add(new Object[]{"", ""});
+        retval.add(new Object[]{"hi</th>bye", "hi\tbye"});
+        retval.add(new Object[]{"hi<th>bye", "hibye"});
+        retval.add(new Object[]{"hi<li>bye", "hi - bye"});
+        retval.add(new Object[]{"hi<NOT_A_REAL_TAG>bye", "hibye"});
+        retval.add(new Object[]{"</h4><pre>", "\n\n"});
+        retval.add(new Object[]{"<a href=\"http://go.here.org\"> string</ a >", " string (http://go.here.org)"});
+        retval.add(new Object[]{"<a href=\"http://go.here.org\" > string</ a>", " string (http://go.here.org)"});
+        retval.add(new Object[]{"< a href=\"http://go.here.org\"> string<a />", " string (http://go.here.org)"});
+
+
+        //for some reason, the next test seems to break intelliJ, but it works on the commandline
+        retval.add(new Object[]{"hi</li>bye", "hi\nbye"});
+
+        retval.add(new Object[]{"Using read outputs from high throughput sequencing (HTS) technologies, this tool provides " +
+                "metrics regarding the quality of read alignments to a reference sequence, as well as the proportion of the reads " +
+                "that passed machine signal-to-noise threshold quality filters (Illumina)." +
+                "<h4>Usage example:</h4>" +
+                "<pre>" +
+                "    java -jar picard.jar CollectAlignmentSummaryMetrics \\<br />" +
+                "          R=reference_sequence.fasta \\<br />" +
+                "          I=input.bam \\<br />" +
+                "          O=output.txt" +
+                "</pre>" +
+                "Please see <a href='http://broadinstitute.github.io/picard/picard-metric-definitions.html#AlignmentSummaryMetrics'>" +
+                "the AlignmentSummaryMetrics documentation</a> for detailed explanations of each metric. <br /> <br />" +
+                "Additional information about Illumina's quality filters can be found in the following documents on the Illumina website:" +
+                "<ul><li><a href=\"http://support.illumina.com/content/dam/illumina-marketing/documents/products/technotes/hiseq-x-percent-pf-technical-note-770-2014-043.pdf\">" +
+                "hiseq-x-percent-pf-technical-note</a></li>" +
+                "<li><a href=\"http://support.illumina.com/content/dam/illumina-support/documents/documentation/system_documentation/hiseqx/hiseq-x-system-guide-15050091-d.pdf\">" +
+                "hiseq-x-system-guide</a></li></ul>" +
+                "<hr />",
+
+                "Using read outputs from high throughput sequencing (HTS) technologies, this tool provides " +
+                        "metrics regarding the quality of read alignments to a reference sequence, as well as the proportion of the reads " +
+                        "that passed machine signal-to-noise threshold quality filters (Illumina)." +
+                        "\nUsage example:\n" +
+                        "\n" +
+                        "    java -jar picard.jar CollectAlignmentSummaryMetrics \\\n" +
+                        "          R=reference_sequence.fasta \\\n" +
+                        "          I=input.bam \\\n" +
+                        "          O=output.txt" +
+                        "\n" +
+                        "Please see the AlignmentSummaryMetrics documentation (http://broadinstitute.github.io/picard/picard-metric-definitions.html#AlignmentSummaryMetrics) for detailed explanations of each metric. \n \n" +
+                        "Additional information about Illumina's quality filters can be found in the following documents on the Illumina website:" +
+                        "\n" +
+                        " - hiseq-x-percent-pf-technical-note (http://support.illumina.com/content/dam/illumina-marketing/documents/products/technotes/hiseq-x-percent-pf-technical-note-770-2014-043.pdf)\n" +
+                        " - hiseq-x-system-guide (http://support.illumina.com/content/dam/illumina-support/documents/documentation/system_documentation/hiseqx/hiseq-x-system-guide-15050091-d.pdf)\n\n" +
+                        "\n"});
+
+        return retval.toArray(new Object[0][]);
+    }
+
+    @Test(dataProvider = "testHTMLConverter")
+    public void testHTMLConverter(String input, String expected) {
+        final String converted = LegacyCommandLineArgumentParser.convertFromHtml(input);
+        Assert.assertEquals(converted, expected, "common part:\"" + expected.substring(0, lengthOfCommonSubstring(converted, expected)) + "\"\n\n");
+    }
+
+    @CommandLineProgramProperties(
+            summary = TestParserFail.USAGE_SUMMARY + TestParserFail.USAGE_DETAILS,
+            oneLineSummary = TestParserFail.USAGE_SUMMARY,
+            programGroup = TestProgramGroup.class
+    )
+    protected class TestParserFail extends Object {
+
+        static public final String USAGE_DETAILS = "blah &blah; blah ";
+        static public final String USAGE_SUMMARY = "This tool offers.....";
+    }
+
+    @CommandLineProgramProperties(
+            summary = TestParserSucceed.USAGE_SUMMARY + TestParserSucceed.USAGE_DETAILS,
+            oneLineSummary = TestParserSucceed.USAGE_SUMMARY,
+            programGroup = TestProgramGroup.class
+    )
+    protected class TestParserSucceed extends Object {
+
+        static public final String USAGE_DETAILS = "This is the first row <p>And this is the second";
+        static public final String USAGE_SUMMARY = " X < Y ";
+    }
+
+    @Test(expectedExceptions = AssertionError.class)
+    public void testNonAsciiAssertion() {
+        LegacyCommandLineArgumentParser clp = new LegacyCommandLineArgumentParser(new TestParserFail());
+
+        PrintStream stream = new PrintStream(new NullOutputStream());
+        clp.parseArguments(stream, new String[]{});
+        clp.usage(true, true);
+    }
+
+    @Test
+    public void testNonAsciiConverted() {
+        LegacyCommandLineArgumentParser clp = new LegacyCommandLineArgumentParser(new TestParserSucceed());
+
+        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+        PrintStream stream = new PrintStream(byteArrayOutputStream);
+        clp.parseArguments(stream, new String[]{});
+        stream.append(clp.usage(true, true));
+
+        String expected = "USAGE: TestParserSucceed [options]\n" +
+                "\n" +
+                " X < Y This is the first row \n" +
+                "And this is the second";
+        String result = byteArrayOutputStream.toString();
+        Assert.assertEquals(byteArrayOutputStream.toString().substring(0, expected.length()), expected);
+    }
+
+    @Test
+    public void testNonASCIIAccept() {
+        LegacyCommandLineArgumentParser.checkForNonASCII("abc", "ascii passes");
+    }
+
+    @Test(expectedExceptions = AssertionError.class)
+    public void testNonASCIIReject() {
+        LegacyCommandLineArgumentParser.checkForNonASCII("\u0080", "non-ascii fails");
+    }
+
+    static private int lengthOfCommonSubstring(String lhs, String rhs) {
+        int i = 0;
+        while (i < Math.min(lhs.length(), rhs.length()) && lhs.charAt(i) == rhs.charAt(i)) i++;
+
+        return i;
+    }
+
+    private class NullOutputStream extends OutputStream {
+        @Override
+        public void write(final int b) throws IOException {
+
+        }
+    }
+
+    @CommandLineProgramProperties(
+            summary = "Tool with max/min boundaries",
+            oneLineSummary = "Tool with max/min boundaries",
+            programGroup = TestProgramGroup.class
+    )
+    private static final class WithBoundaries {
+        @Argument(minValue = -10, minRecommendedValue = -2, maxValue = 2, maxRecommendedValue = 10)
+        public int INTEGER_VALUE;
+    }
+
+    @DataProvider
+    public Object[][] valuesForWithBoundaries() {
+        return new Object[][] {{-15}, {-5}, {-2}, {1}, {0}, {2}, {5}, {10}};
+    }
+
+    @Test(dataProvider = "valuesForWithBoundaries")
+    public void testIgnoringBoundaries(final int value) {
+        final WithBoundaries withBoundaries = new WithBoundaries();
+        final LegacyCommandLineArgumentParser clp = new LegacyCommandLineArgumentParser(withBoundaries);
+        clp.parseArguments(System.err, new String[]{"INTEGER_VALUE=" + Integer.toString(value)});
+        Assert.assertEquals(withBoundaries.INTEGER_VALUE, value);
+    }
+}
diff --git a/src/test/java/org/broadinstitute/barclay/argparser/StrictBooleanConverterTest.java b/src/test/java/org/broadinstitute/barclay/argparser/StrictBooleanConverterTest.java
new file mode 100644
index 0000000..bade880
--- /dev/null
+++ b/src/test/java/org/broadinstitute/barclay/argparser/StrictBooleanConverterTest.java
@@ -0,0 +1,25 @@
+package org.broadinstitute.barclay.argparser;
+
+import joptsimple.ValueConversionException;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+
+public final class StrictBooleanConverterTest {
+    @Test
+    public void recognizedValues(){
+        StrictBooleanConverter converter = new StrictBooleanConverter();
+        Assert.assertEquals("true", converter.convert("true"));
+        Assert.assertEquals("true", converter.convert("T"));
+        Assert.assertEquals("true",converter.convert("TRUE"));
+        Assert.assertEquals("false",converter.convert("F"));
+        Assert.assertEquals("false", converter.convert("False"));
+    }
+
+    @Test(expectedExceptions = ValueConversionException.class)
+    public void unrecognizedValues(){
+        StrictBooleanConverter converter = new StrictBooleanConverter();
+        converter.convert("unprovable");
+    }
+
+}
diff --git a/src/test/java/org/broadinstitute/barclay/argparser/TaggedArgumentTest.java b/src/test/java/org/broadinstitute/barclay/argparser/TaggedArgumentTest.java
new file mode 100644
index 0000000..338eb1e
--- /dev/null
+++ b/src/test/java/org/broadinstitute/barclay/argparser/TaggedArgumentTest.java
@@ -0,0 +1,441 @@
+package org.broadinstitute.barclay.argparser;
+
+import org.testng.Assert;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import java.util.*;
+
+/**
+ * Tests for taggable arguments, i.e.:
+ *
+ *     -I:tumor my.bam
+ *     -I:tumor,key=value,key2=value2
+ */
+public class TaggedArgumentTest {
+
+    private static class TaggableArg implements TaggedArgument {
+
+        private String tagName;
+        private Map<String, String> tagAttributes = new HashMap<>();
+        public String argValue;
+
+        public TaggableArg(final String value) {
+            this.argValue = value;
+        }
+
+        public TaggableArg(final String value, final String tagName)
+        {
+            this(value);
+            this.tagName = tagName;
+        }
+
+        @Override
+        public void setTag(final String tagName) {
+            this.tagName = tagName;
+        }
+
+        @Override
+        public String getTag() {
+            return tagName;
+        }
+
+        @Override
+        public void setTagAttributes(final Map<String, String> attributes) {
+            this.tagAttributes.putAll(attributes);
+        }
+
+        @Override
+        public Map<String, String> getTagAttributes() {
+            return tagAttributes;
+        }
+
+        @Override
+        public String toString() { return argValue; }
+    }
+
+    @CommandLineProgramProperties(
+            summary = "Test tagged aruments",
+            oneLineSummary = "Test tagged aruments",
+            programGroup = TestProgramGroup.class
+    )
+    private static class TaggableArguments {
+
+        @PositionalArguments(minElements = 0, maxElements=2)
+        public List<String> positionalArgs = new ArrayList<>();
+
+        @Argument(shortName="t", fullName="tFullName", doc="Tag target arg", optional = true)
+        public List<TaggableArg> taggableArgList = new ArrayList<>();
+
+        @Argument(shortName="scalar", fullName="tScalar", doc="Tag target arg", optional = true)
+        public TaggableArg taggableArgScalar = new TaggableArg("foo", "taggableArgScalar");
+
+        @Argument(shortName="s", fullName="scalarArg", optional = true)
+        public Integer nonTaggableInteger = 17;
+
+        @Argument(shortName="n", fullName="notTaggable", optional = true)
+        public String notTaggable;
+    }
+
+    // Non-generic key-value class for easy addition to statically initialized arrays in data provider
+    private static class KeyValuePair{
+        private final String key, value;
+
+        public static KeyValuePair of(final String key, final String right) { return new KeyValuePair(key, right); }
+
+        private KeyValuePair(final String left, final String right) { this.key = left; this.value = right; }
+        public String getKey() { return this.key; }
+        public String getValue() { return this.value; }
+    }
+
+    @SuppressWarnings("unchecked")
+    @DataProvider(name = "GoodTaggedArguments")
+    public Object[][] goodTaggedArguments() {
+        return new Object[][][]{
+                //TODO: file args, tagged enum
+                {
+                        // single value in a collection, no tag, no attributes, short name
+                        new String[]{"--t", "tumor.bam"},
+                        new KeyValuePair[]{ KeyValuePair.of(null, "tumor.bam")}, // tag is long arg name
+                        new KeyValuePair[][]{{}}
+                },
+                {
+                        // same as above, with "-" instead of "--"
+                        new String[]{"-t", "tumor.bam"},
+                        new KeyValuePair[]{ KeyValuePair.of(null, "tumor.bam")}, // tag is long arg name
+                        new KeyValuePair[][]{{}}
+                },
+                {
+                        // single value in a collection, no attributes, short name
+                        new String[]{"--t:tumor", "tumor.bam"},
+                        new KeyValuePair[]{KeyValuePair.of("tumor", "tumor.bam")},
+                        new KeyValuePair[][]{{}}
+                },
+                {
+                        // single value in a collection, no attributes, short name
+                        new String[]{"--t:tumor", "/tumor.bam"},
+                        new KeyValuePair[]{KeyValuePair.of("tumor", "/tumor.bam")},
+                        new KeyValuePair[][]{{}}
+                },
+                {
+                        // single tagged value in a collection, no attributes, short name
+                        new String[]{"--t:tumor", "gcs://my/tumor.bam"},
+                        new KeyValuePair[]{KeyValuePair.of("tumor", "gcs://my/tumor.bam")},
+                        new KeyValuePair[][]{{}}
+                },
+                {
+                        // single tagged value in a collection, no attributes, short name
+                        new String[]{"--t:tumor", "gendb://mydb"},
+                        new KeyValuePair[]{KeyValuePair.of("tumor", "gendb://mydb")},
+                        new KeyValuePair[][]{{}}
+                },
+                {
+                        // same as above but with "-"
+                        new String[]{"-t:tumor", "gcs://my/tumor.bam"},
+                        new KeyValuePair[]{KeyValuePair.of("tumor", "gcs://my/tumor.bam")},
+                        new KeyValuePair[][]{{}}
+                },
+                {
+                        // single tagged value in a collection, no attributes, short name
+                        new String[]{"-t:tumor", "hostName:1234/tumor.bam"},
+                        new KeyValuePair[]{KeyValuePair.of("tumor", "hostName:1234/tumor.bam")},
+                        new KeyValuePair[][]{{}}
+                },
+                {
+                        // single tagged value in a collection, no attributes, short name
+                        new String[]{"-t:tumor", "http://hostName:1234/tumor.bam"},
+                        new KeyValuePair[]{KeyValuePair.of("tumor", "http://hostName:1234/tumor.bam")},
+                        new KeyValuePair[][]{{}}
+                },
+                {
+                        // single value in a collection, no attributes, full name
+                        new String[]{"--tFullName:tumor", "tumor.bam"},
+                        new KeyValuePair[]{KeyValuePair.of("tumor", "tumor.bam")},
+                        new KeyValuePair[][]{{}}
+                },
+                {
+                        // single tagged value in a collection, two attributes, short name
+                        new String[]{"--t:tumor,truth=true,training=false", "tumor.bam"},
+                        new KeyValuePair[]{KeyValuePair.of("tumor", "tumor.bam")},
+                        new KeyValuePair[][]{
+                                {
+                                        KeyValuePair.of("truth", "true"),
+                                        KeyValuePair.of("training", "false")
+                                }
+                        }
+                },
+                {
+                        // same as above, but with a single attribute
+                        new String[]{"--t:tumor,truth=false", "tumor.bam"},
+                        new KeyValuePair[]{KeyValuePair.of("tumor", "tumor.bam")},
+                        new KeyValuePair[][]{
+                                {
+                                        KeyValuePair.of("truth", "false"),
+                                }
+                        }
+                },
+                {
+                        // single tagged value in a collection, two attributes, short name
+                        new String[] {"--t:tumor,truth=false,training=true", "tumor.bam"},
+                        new KeyValuePair[]{KeyValuePair.of("tumor", "tumor.bam")},
+                        new KeyValuePair[][]{
+                                {
+                                        KeyValuePair.of("truth", "false"),
+                                        KeyValuePair.of("training", "true")
+                                }
+                        }
+                },
+                {
+                        // same as above, but with "-"
+                        new String[] {"-t:tumor,truth=false,training=true", "tumor.bam"},
+                        new KeyValuePair[]{KeyValuePair.of("tumor", "tumor.bam")},
+                        new KeyValuePair[][]{
+                                {
+                                        KeyValuePair.of("truth", "false"),
+                                        KeyValuePair.of("training", "true")
+                                }
+                        }
+                },
+                {
+                        // two tagged values in a collection, no attributes, short name
+                        new String[]{"--t:tumor", "tumor.bam", "--t:normal", "normal.bam"},
+                        new KeyValuePair[]{
+                                KeyValuePair.of("tumor", "tumor.bam"),
+                                KeyValuePair.of("normal", "normal.bam")
+                        },
+                        new KeyValuePair[][]{{}}
+                },
+                {
+                        // two tagged values in a collection, with attributes, short name
+                        new String[] {"--t:tumor,truth=false,training=true", "tumor.bam", "--t:normal,truth=true,training=false", "normal.bam"},
+                        new KeyValuePair[]{
+                                KeyValuePair.of("tumor", "tumor.bam"),
+                                KeyValuePair.of("normal", "normal.bam")
+                        },
+                        new KeyValuePair[][]{
+                                {
+                                        KeyValuePair.of("truth", "false"),
+                                        KeyValuePair.of("training", "true")
+                                },
+                                {
+                                        KeyValuePair.of("truth", "true"),
+                                        KeyValuePair.of("training", "false")
+                                }
+                        }
+                },
+                {
+                        // two tagged values in a collection, with attributes, one short name, one long name
+                        new String[] {"--t:tumor,truth=false,training=true", "tumor.bam", "--tFullName:normal,truth=true,training=false", "normal.bam"},
+                        new KeyValuePair[]{
+                                KeyValuePair.of("tumor", "tumor.bam"),
+                                KeyValuePair.of("normal", "normal.bam")
+                        },
+                        new KeyValuePair[][]{
+                                {
+                                        KeyValuePair.of("truth", "false"),
+                                        KeyValuePair.of("training", "true")
+                                },
+                                {
+                                        KeyValuePair.of("truth", "true"),
+                                        KeyValuePair.of("training", "false")
+                                }
+                        }
+                },
+                {
+                        // intermix positional with two tagged values in a collection, with attributes, one short name, one long name
+                        new String[] {
+                                "positional1", // note this test does not verify the positional values
+                                "positional2",
+                                "--t:tumor,truth=false,training=true",
+                                "tumor.bam",
+                                "--tFullName:normal,truth=true,training=false",
+                                "normal.bam"},
+                        new KeyValuePair[]{
+                                KeyValuePair.of("tumor", "tumor.bam"),
+                                KeyValuePair.of("normal", "normal.bam")
+                        },
+                        new KeyValuePair[][]{
+                                {
+                                        KeyValuePair.of("truth", "false"),
+                                        KeyValuePair.of("training", "true")
+                                },
+                                {
+                                        KeyValuePair.of("truth", "true"),
+                                        KeyValuePair.of("training", "false")
+                                }
+                        }
+                }
+        };
+    }
+
+    @Test(dataProvider="GoodTaggedArguments")
+    public void testGoodTaggedArguments(
+            final String argv[],
+            final KeyValuePair expectedTagNameValuePairs[],
+            final KeyValuePair expectedAttributePairArrays[][])
+    {
+        final TaggableArguments taggable = new TaggableArguments();
+        final CommandLineArgumentParser clp = new CommandLineArgumentParser(taggable);
+        clp.parseArguments(System.err, argv);
+
+        // All list entries are always populated
+        Assert.assertEquals(taggable.taggableArgList.size(), expectedTagNameValuePairs.length);
+
+        // validate that the tags match the right values
+        int i = 0;
+        for (final KeyValuePair tagNameValuePair : expectedTagNameValuePairs) {
+            TaggableArg taggedArg = taggable.taggableArgList.get(i++);
+            Assert.assertEquals(taggedArg.getTag(), tagNameValuePair.getKey());
+            Assert.assertEquals(taggedArg.argValue, tagNameValuePair.getValue());
+        }
+
+        // validate attributes
+        i = 0;
+        for (KeyValuePair attributePairArray[] : expectedAttributePairArrays) {
+            Map<String, String> attributes = taggable.taggableArgList.get(i++).getTagAttributes();
+            for (KeyValuePair attributePair : attributePairArray) {
+                Assert.assertEquals(attributes.get(attributePair.getKey()), attributePair.getValue());
+            }
+        }
+    }
+
+    @Test
+    public void testTaggedScalarArgument() {
+        final TaggableArguments taggable = new TaggableArguments();
+        final CommandLineArgumentParser clp = new CommandLineArgumentParser(taggable);
+        String argv[] = new String[] {
+                "-tScalar:ScalarTag,aScalar=27", "tumor.bam"
+        };
+        clp.parseArguments(System.err, argv);
+
+        Assert.assertEquals(taggable.taggableArgScalar.argValue, "tumor.bam");
+        Assert.assertEquals(taggable.taggableArgScalar.getTag(), "ScalarTag");
+        Assert.assertEquals(taggable.taggableArgScalar.getTagAttributes().get("aScalar"), "27");
+    }
+
+    @Test
+    public void testMixedTagCollectionArgument() {
+        final TaggableArguments taggable = new TaggableArguments();
+        final CommandLineArgumentParser clp = new CommandLineArgumentParser(taggable);
+        String argv[] = new String[] {
+                "--t:tumor,truth=false,training=true", "tumor.bam", "--t", "normal.bam"
+        };
+        clp.parseArguments(System.err, argv);
+
+        // first in collection is tagged.
+        TaggableArg ta = taggable.taggableArgList.get(0);
+        Assert.assertEquals(ta.argValue, "tumor.bam");
+
+        Assert.assertEquals(ta.getTag(), "tumor");
+        Assert.assertEquals(ta.getTagAttributes().get("truth"), "false");
+        Assert.assertEquals(ta.getTagAttributes().get("training"), "true");
+
+        // second in collection is not
+        ta = taggable.taggableArgList.get(1);
+        Assert.assertEquals(ta.argValue, "normal.bam");
+        Assert.assertTrue(ta.getTagAttributes().isEmpty());
+    }
+
+    @DataProvider(name = "BadTaggedArguments")
+    public Object[][] badTaggedArguments() {
+        return new Object[][]{
+                // short name, mix of "-" and "--"
+                {new String[]{"--t"}},                             // taggable, no tagname, but no arg
+                {new String[]{"-t"}},                              // taggable, no tagname, but no arg
+                {new String[]{"--t:"}},                            // taggable, tagname missing, no arg
+                {new String[]{"-t:"}},                             // taggable, tagname missing, no arg
+                {new String[]{"--t:", "tumor1.bam"}},              // taggable, tagname missing, with arg
+                {new String[]{"-t:", "tumor1.bam"}},               // taggable, tagname missing, with arg
+                {new String[]{"--t:tagName"}},                     // taggable, with tagname, but no arg
+                {new String[]{"-t:tagName"}},                      // taggable, with tagname, but no arg
+
+                // full name, mix of "-" and "--"
+                {new String[]{"--tFullName"}},                     // taggable, but no tagname, no arg
+                {new String[]{"-tFullName"}},                      // taggable, but no tagname, no arg
+                {new String[]{"--tFullName:"}},                    // taggable, tagname missing, no arg
+                {new String[]{"-tFullName:"}},                     // taggable, tagname missing, no arg
+                {new String[]{"--tFullName:", "tumor1.bam"}},      // taggable, tagname missing, with arg
+                {new String[]{"-tFullName:", "tumor1.bam"}},       // taggable, tagname missing, with arg
+                {new String[]{"--tFullName:tagName"}},             // taggable, with tagname, but no arg
+                {new String[]{"-tFullName:tagName"}},              // taggable, with tagname, but no arg
+
+                // not taggable, mix of "-" and "--"
+                {new String[]{"--n:tagName"}},                     // not taggable, with tagname
+                {new String[]{"-n:tagName"}},                      // not taggable, with tagname
+                {new String[]{"--n:key=value"}},                   // not taggable, with attributes
+                {new String[]{"-n:key=value"}},                    // not taggable, with attributes
+                {new String[]{"--n:tagName,key=value"}},           // not taggable, with tagname and attributes
+                {new String[]{"-n:tagName,key=value"}},            // not taggable, with tagname and attributes
+                {new String[]{"--n:tagName,key=value argValue"}},  // not taggable, with tagname, attributes and value
+                {new String[]{"-n:tagName,key=value argValue"}},   // not taggable, with tagname, attributes and value
+
+                // missing value, mix of "-" and "--"
+                {new String[]{"--t:tagName,key=value"}},            // taggable, with tagname, attributes, missing value at end
+                {new String[]{"-t:tagName,key=value"}},             // taggable, with tagname, attributes, missing value at end
+                {new String[]{"--t:tagName,key=value", "-s", "28"}},// taggable, with tagname, attributes, missing value and second argument
+                {new String[]{"-t:tagName,key=value", "-s", "28"}}, // taggable, with tagname, attributes, missing value and second argument
+                {new String[]{ "-s", "28", "--t:tagName,key=value"}},// second argument taggable with tagname, attributes, missing value
+                {new String[]{ "-s", "28", "-t:tagName,key=value"}},// second argument taggable with tagname, attributes, missing value
+
+                // Malformed attribute strings
+                {new String[]{"--t:tumor,truth", "tumor.bam"}},                    // attribute name with missing value
+                {new String[]{"--t:tumor,truth=", "tumor.bam"}},                   // attribute name with missing value
+                {new String[]{"--t:tumor,truth=true,truth=false", "tumor.bam"}},   // duplicate attribute value
+                {new String[]{"--t:tumor,", "tumor.bam"}},                         // dangling comma
+
+                // actually ok - we haven't placed any restrictions on tagnames
+                //{new String[]{"--t:tag:tag", "value"}},
+                //{new String[]{"--t::", "value"}},
+                {new String[]{"--t:tag,key=value=foo", "value"}},
+                {new String[]{"--t:tag,,", "value"}},
+                {new String[]{"--t:tag,,key=value", "value"}},
+                {new String[]{"--t:,key=value", "value"}},
+                {new String[]{"--t:,,key=value", "value"}},
+
+                {new String[]{"--,tumor:key=value", "value"}},
+                {new String[]{"--t\"", "value"}},
+                {new String[]{"--t:,", "value"}},
+                {new String[]{"--t,:", "value"}},
+                {new String[]{"--t,,", "value"}},
+                {new String[]{"--t:,,key=value", "value"}},
+                {new String[]{"--t,tumor:key=value", "value"}},
+                {new String[]{"--t,tumor:key=:value", "value"}},
+                {new String[]{"--t,tumor:key:value", "value"}},
+                {new String[]{"--t,tumor,key:value", "value"}},
+                {new String[]{"--t,tumor,=key:value", "value"}},
+                {new String[]{"--t,tumor,=key=value", "value"}},
+                {new String[]{"--t,tumor,=value", "value"}},
+                {new String[]{"--t,tumor,=:value", "value"}},
+                {new String[]{"--t,tumor:value", "value"}},
+                {new String[]{"--t,tumor:value:", "gendb://mydb"}},
+        };
+    }
+
+    @Test(dataProvider="BadTaggedArguments", expectedExceptions = CommandLineException.class)
+    public void testBadTaggedArguments(final String argv[]) {
+        final TaggableArguments taggables = new TaggableArguments();
+        final CommandLineArgumentParser clp = new CommandLineArgumentParser(taggables);
+        clp.parseArguments(System.err, argv);
+    }
+
+    @DataProvider(name = "GetCommandLineTaggedArguments")
+    public Object[][] taggedGetCommandLine() {
+        return new Object[][]{
+                {new String[]{"--t:tumor", "gcs://my/tumor.bam"},
+                        "TaggableArguments  --tFullName:tumor gcs://my/tumor.bam  --tScalar:taggableArgScalar foo --scalarArg 17"
+                },
+                {new String[]{"--t:tumor,truth=false,training=true", "tumor.bam", "--tFullName:normal,truth=true,training=false", "normal.bam"},
+                        "TaggableArguments  --tFullName:tumor,training=true,truth=false tumor.bam --tFullName:normal,training=false,truth=true normal.bam  --tScalar:taggableArgScalar foo --scalarArg 17"
+                }
+        };
+    }
+
+    @Test(dataProvider="GetCommandLineTaggedArguments")
+    public void testGetCommandLineTagged(final String[] argv, final String expectedCommandLine) {
+        final TaggableArguments taggables = new TaggableArguments();
+        final CommandLineArgumentParser clp = new CommandLineArgumentParser(taggables);
+        clp.parseArguments(System.err, argv);
+        final String commandLine = clp.getCommandLine();
+        Assert.assertEquals(commandLine, expectedCommandLine);
+    }
+}
diff --git a/src/test/java/org/broadinstitute/barclay/argparser/TestProgramGroup.java b/src/test/java/org/broadinstitute/barclay/argparser/TestProgramGroup.java
new file mode 100644
index 0000000..983d19a
--- /dev/null
+++ b/src/test/java/org/broadinstitute/barclay/argparser/TestProgramGroup.java
@@ -0,0 +1,16 @@
+package org.broadinstitute.barclay.argparser;
+
+/**
+ * only for testing
+ */
+public final class TestProgramGroup implements CommandLineProgramGroup {
+    @Override
+    public String getName() {
+        return "TestProgramGroup";
+    }
+
+    @Override
+    public String getDescription() {
+        return "Test program group used for testing";
+    }
+}
\ No newline at end of file
diff --git a/src/test/java/org/broadinstitute/barclay/help/DocumentationGenerationIntegrationTest.java b/src/test/java/org/broadinstitute/barclay/help/DocumentationGenerationIntegrationTest.java
new file mode 100644
index 0000000..e962661
--- /dev/null
+++ b/src/test/java/org/broadinstitute/barclay/help/DocumentationGenerationIntegrationTest.java
@@ -0,0 +1,275 @@
+package org.broadinstitute.barclay.help;
+
+
+import org.testng.Assert;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import java.io.File;
+import java.io.IOException;
+import java.math.BigInteger;
+import java.nio.file.Files;
+import java.security.SecureRandom;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Integration test for documentation generation.
+ */
+public class DocumentationGenerationIntegrationTest {
+
+    private static String inputResourcesDir = "src/main/resources/org/broadinstitute/barclay/";
+    private static String testResourcesDir = "src/test/resources/org/broadinstitute/barclay/";
+
+    private static final String indexFileName = "index";
+    private static final String jsonFileExtension = ".json";
+
+    // common arguments not changed for tests
+    private static final List<String> COMMON_DOC_ARG_LIST = Arrays.asList(
+            "-build-timestamp", "2016/01/01 01:01:01",      // dummy, constant timestamp
+            "-absolute-version", "11.1",                    // dummy version
+            "-docletpath", "build/libs",
+            "-sourcepath", "src/test/java",
+            "org.broadinstitute.barclay.help",
+            "org.broadinstitute.barclay.argparser",
+            "-verbose",
+            "-cp", System.getProperty("java.class.path")
+    );
+
+    private static final List<String> EXPECTED_OUTPUT_FILE_NAME_PREFIXES = Arrays.asList(
+            "org_broadinstitute_barclay_help_TestArgumentContainer",
+            "org_broadinstitute_barclay_help_TestExtraDocs"
+    );
+
+    private static List<String> docArgList(final Class<?> docletClass, final File templatesFolder, final File outputDir,
+            final String indexFileExtension, final String outputFileExtension) {
+
+        // set the common arguments
+        final List<String> docArgList = new ArrayList<>(COMMON_DOC_ARG_LIST);
+
+        // set the templates
+        if ( templatesFolder != null ) {
+            docArgList.add("-settings-dir");
+            docArgList.add(templatesFolder.getAbsolutePath());
+        }
+
+        // set the output directory
+        docArgList.add("-d");
+        docArgList.add(outputDir.getAbsolutePath());
+
+        // set the doclet class
+        docArgList.add("-doclet");
+        docArgList.add(docletClass.getName());
+
+        // set the index and output extension
+        docArgList.add("-index-file-extension");
+        docArgList.add(indexFileExtension);
+        docArgList.add("-output-file-extension");
+        docArgList.add(outputFileExtension);
+
+        return docArgList;
+    }
+
+    @DataProvider
+    public Object[][] getDocGenTestParams() {
+        return new Object[][] {
+                // default doclet and templates
+                {HelpDoclet.class,
+                        new File(inputResourcesDir + "helpTemplates/"),
+                        new File(testResourcesDir + "help/expected/HelpDoclet"),
+                        indexFileName,
+                        "html", // testIndexFileExtension
+                        "html", // testOutputFileExtension
+                        "html", // requestedIndexFileExtension
+                        "html", // requestedOutputFileExtension
+                        new String[] {}, // customDocletArgs
+                        false    // onlyTestIndex
+                },
+                // defaut doclet and templates using alternate index extension
+                {HelpDoclet.class,
+                        new File(inputResourcesDir + "helpTemplates/"),
+                        new File(testResourcesDir + "help/expected/HelpDoclet"),
+                        indexFileName,
+                        "html",  // testIndexFileExtension
+                        "html",  // testOutputFileExtension
+                        "xhtml", // requestedIndexFileExtension
+                        "html",  // requestedOutputFileExtension
+                        new String[] {}, // customDocletArgs
+                        false     // onlyTestIndex
+                },
+                // custom doclet and templates
+                {TestDoclet.class,
+                        new File(testResourcesDir + "help/templates/TestDoclet/"),
+                        new File(testResourcesDir + "help/expected/TestDoclet"),
+                        indexFileName,
+                        "html", // testIndexFileExtension
+                        "html", // testOutputFileExtension
+                        "html", // requestedIndexFileExtension
+                        "html", // requestedOutputFileExtension
+                        new String[] {}, // customDocletArgs
+                        false    // onlyTestIndex
+                },
+                // custom bash doclet and templates
+                {BashTabCompletionDoclet.class,
+                        new File(inputResourcesDir + "helpTemplates/"),
+                        new File(testResourcesDir + "help/expected/BashTabCompletionDoclet"),
+                        "bashTabCompletionDocletTestLaunch-completion",
+                        "sh", // testIndexFileExtension
+                        "sh", // testOutputFileExtension
+                        "sh", // requestedIndexFileExtension
+                        "sh", // requestedOutputFileExtension
+                        new String[] {    // customDocletArgs
+                                "-caller-script-name",         "bashTabCompletionDocletTestLaunch.sh",
+
+                                // Test with these off for now:
+                                "-caller-pre-legal-args",      "--pre-help --pre-info --pre-inputFile",
+                                "-caller-pre-arg-val-types",   "null null File",
+                                "-caller-pre-mutex-args",      "--pre-help;pre-info,pre-inputFile --pre-info;pre-help,pre-inputFile",
+                                "-caller-pre-alias-args",      "--pre-help;-prh --pre-inputFile;-prif",
+                                "-caller-pre-arg-min-occurs",  "0 0 1",
+                                "-caller-pre-arg-max-occurs",  "1 1 1",
+
+                                "-caller-post-legal-args",     "--post-help --post-info --post-inputFile",
+                                "-caller-post-arg-val-types",  "null null File",
+                                "-caller-post-mutex-args",     "--post-help;post-info,post-inputFile --post-info;post-help,post-inputFile",
+                                "-caller-post-alias-args",     "--post-help;-poh --post-inputFile;-poif",
+                                "-caller-post-arg-min-occurs", "0 0 1",
+                                "-caller-post-arg-max-occurs", "1 1 1",
+                        },
+                        true  // onlyTestIndex
+                },
+
+                //==============================================================================================================
+
+                // default doclet and templates from classpath
+                {HelpDoclet.class,
+                        null,
+                        new File(testResourcesDir + "help/expected/HelpDoclet"),
+                        indexFileName,
+                        "html", // testIndexFileExtension
+                        "html", // testOutputFileExtension
+                        "html", // requestedIndexFileExtension
+                        "html", // requestedOutputFileExtension
+                        new String[] {"-use-default-templates"}, // customDocletArgs
+                        false    // onlyTestIndex
+                },
+                // defaut doclet and templates from classpath using alternate index extension
+                {HelpDoclet.class,
+                        null,
+                        new File(testResourcesDir + "help/expected/HelpDoclet"),
+                        indexFileName,
+                        "html",  // testIndexFileExtension
+                        "html",  // testOutputFileExtension
+                        "xhtml", // requestedIndexFileExtension
+                        "html",  // requestedOutputFileExtension
+                        new String[] { "-use-default-templates" }, // customDocletArgs
+                        false     // onlyTestIndex
+                },
+                // custom bash doclet pulling templates from classpath
+                {BashTabCompletionDoclet.class,
+                        null,
+                        new File(testResourcesDir + "help/expected/BashTabCompletionDoclet"),
+                        "bashTabCompletionDocletTestLaunch-completion",
+                        "sh", // testIndexFileExtension
+                        "sh", // testOutputFileExtension
+                        "sh", // requestedIndexFileExtension
+                        "sh", // requestedOutputFileExtension
+                        new String[] {    // customDocletArgs
+                                "-caller-script-name",         "bashTabCompletionDocletTestLaunch.sh",
+
+                                "-use-default-templates",
+
+                                // Test with these off for now:
+                                "-caller-pre-legal-args",      "--pre-help --pre-info --pre-inputFile",
+                                "-caller-pre-arg-val-types",   "null null File",
+                                "-caller-pre-mutex-args",      "--pre-help;pre-info,pre-inputFile --pre-info;pre-help,pre-inputFile",
+                                "-caller-pre-alias-args",      "--pre-help;-prh --pre-inputFile;-prif",
+                                "-caller-pre-arg-min-occurs",  "0 0 1",
+                                "-caller-pre-arg-max-occurs",  "1 1 1",
+
+                                "-caller-post-legal-args",     "--post-help --post-info --post-inputFile",
+                                "-caller-post-arg-val-types",  "null null File",
+                                "-caller-post-mutex-args",     "--post-help;post-info,post-inputFile --post-info;post-help,post-inputFile",
+                                "-caller-post-alias-args",     "--post-help;-poh --post-inputFile;-poif",
+                                "-caller-post-arg-min-occurs", "0 0 1",
+                                "-caller-post-arg-max-occurs", "1 1 1",
+                        },
+                        true  // onlyTestIndex
+                },
+                // custom bash doclet pulling templates from classpath, Mostly defaults
+                {BashTabCompletionDoclet.class,
+                        null,
+                        new File(testResourcesDir + "help/expected/BashTabCompletionDoclet"),
+                        "bashTabCompletionDocletTestLaunchWithDefaults-completion",
+                        "sh", // testIndexFileExtension
+                        "sh", // testOutputFileExtension
+                        "sh", // requestedIndexFileExtension
+                        "sh", // requestedOutputFileExtension
+                        new String[] {  // customDocletArgs
+                                "-caller-script-name",         "bashTabCompletionDocletTestLaunchWithDefaults.sh",
+                                "-use-default-templates"
+                        },
+                        true  // onlyTestIndex
+                },
+        };
+    }
+
+    @Test(dataProvider = "getDocGenTestParams")
+    public void testDocGenRoundTrip(
+            final Class<?> docletClass,
+            final File inputTemplatesFolder,
+            final File expectedDir,
+            final String indexFileBaseName,
+            final String testIndexFileExtension,
+            final String testOutputFileExtension,
+            final String requestedIndexFileExtension,
+            final String requestedOutputFileExtension,
+            final String[] customDocletArgs,
+            final boolean onlyTestIndex
+    ) throws IOException
+    {
+        // creates a temp output directory
+        final File outputDir = Files.createTempDirectory(docletClass.getName()).toAbsolutePath().toFile();
+        outputDir.deleteOnExit();
+
+        // pull all our arguments together:
+        List<String> javadocArgs = docArgList(docletClass, inputTemplatesFolder, outputDir, requestedIndexFileExtension, requestedOutputFileExtension);
+        for (int i = 0 ; i < customDocletArgs.length; ++i) {
+            javadocArgs.add(customDocletArgs[i]);
+        }
+
+        // run javadoc with the custom doclet
+        com.sun.tools.javadoc.Main.execute(
+                javadocArgs.toArray(new String[] {})
+        );
+
+        // Compare index files
+        assertFileContentsIdentical(
+                new File(outputDir, indexFileBaseName + "." + requestedIndexFileExtension),
+                new File(expectedDir, indexFileBaseName + "." + testIndexFileExtension));
+
+        // Only compare other output files if we should have them:
+        if ( !onlyTestIndex ) {
+            // Compare output files (json and workunit)
+            for (final String workUnitFileNamePrefix : EXPECTED_OUTPUT_FILE_NAME_PREFIXES) {
+                //check json file
+                assertFileContentsIdentical(
+                        new File(outputDir, workUnitFileNamePrefix + jsonFileExtension),
+                        new File(expectedDir, workUnitFileNamePrefix + jsonFileExtension));
+                // check workunit output file
+                assertFileContentsIdentical(
+                        new File(outputDir, workUnitFileNamePrefix + "." + requestedOutputFileExtension),
+                        new File(expectedDir, workUnitFileNamePrefix + "." + testOutputFileExtension));
+            }
+        }
+    }
+
+    private void assertFileContentsIdentical(
+            final File actualFile,
+            final File expectedFile) throws IOException {
+        byte[] actualBytes = Files.readAllBytes(actualFile.toPath());
+        byte[] expectedBytes = Files.readAllBytes(expectedFile.toPath());
+        Assert.assertEquals(actualBytes, expectedBytes);
+    }
+}
diff --git a/src/test/java/org/broadinstitute/barclay/help/TestArgumentCollection.java b/src/test/java/org/broadinstitute/barclay/help/TestArgumentCollection.java
new file mode 100644
index 0000000..587a8ae
--- /dev/null
+++ b/src/test/java/org/broadinstitute/barclay/help/TestArgumentCollection.java
@@ -0,0 +1,36 @@
+package org.broadinstitute.barclay.help;
+
+import org.broadinstitute.barclay.argparser.Argument;
+
+import java.io.File;
+import java.util.List;
+
+/**
+ * Test class used to test argument collection documentation.
+ */
+public class TestArgumentCollection {
+
+    @Argument(fullName = "optionalStringInputFromArgCollection",
+            shortName = "optionalStringInputFromArgCollection",
+            doc = "Optional string input from argument collection",
+            optional = true)
+    public String argCollectOptionalStringInput;
+
+    @Argument(fullName = "requiredStringInputFromArgCollection",
+            shortName = "requiredStringInputFromArgCollection",
+            doc = "Required string input from argument collection",
+            optional = false)
+    public String argCollectRequiredStringInput;
+
+    @Argument(fullName = "requiredInputFilesFromArgCollection",
+            shortName = "rRequiredInputFilesFromArgCollection",
+            doc = "Required input files from argument collection",
+            optional = false)
+    public List<File> argCollectRequiredInputFiles;
+
+    @Argument(fullName = "optionalInputFilesFromArgCollection",
+            shortName = "optionalInputFilesFromArgCollection",
+            doc = "Optional input files from argument collection",
+            optional = true)
+    public List<File> argCollectOptionalInputFiles;
+}
diff --git a/src/test/java/org/broadinstitute/barclay/help/TestArgumentContainer.java b/src/test/java/org/broadinstitute/barclay/help/TestArgumentContainer.java
new file mode 100644
index 0000000..a6147a5
--- /dev/null
+++ b/src/test/java/org/broadinstitute/barclay/help/TestArgumentContainer.java
@@ -0,0 +1,201 @@
+package org.broadinstitute.barclay.help;
+
+
+import org.broadinstitute.barclay.argparser.*;
+import org.broadinstitute.barclay.argparser.CommandLinePluginProvider;
+import org.broadinstitute.barclay.argparser.CommandLinePluginUnitTest;
+
+import java.io.File;
+import java.util.*;
+
+/**
+ * Argument container class for testing documentation generation. Contains an argument
+ * for each @Argument, @ArgumentCollection, and @DocumentedFeature property that should
+ * be tested.
+ *
+ * Test custom tag:
+ * {@MyTag.Type testType}
+ *
+ * <p>
+ * The purpose of this paragraph is to test embedded html formatting.
+ * <ol>
+ *     <li>This is point number 1</li>
+ *     <li>This is point number 2</li>
+ * </ol>
+ * </p>
+ */
+ at CommandLineProgramProperties(
+        summary = "Test tool summary",
+        oneLineSummary = "Argument container class for testing documentation generation.",
+        programGroup = TestProgramGroup.class)
+ at BetaFeature
+ at DocumentedFeature(groupName = "Test feature group name", extraDocs = TestExtraDocs.class)
+public class TestArgumentContainer implements CommandLinePluginProvider {
+
+    /**
+     * Positional arguments
+     */
+    @PositionalArguments(
+            minElements = 2,
+            maxElements = 2,
+            doc = "Positional arguments, min = 2, max = 2")
+    public List<File> positionalArguments;
+
+    /**
+     * Optional file list.
+     */
+    @Argument(fullName = "optionalFileList",
+            shortName = "optFilList",
+            doc = "Optional file list",
+            optional = true)
+    public List<File> optionalFileList;
+
+    /**
+     * Required file list.
+     */
+    @Argument(fullName = "requiredFileList",
+            shortName = "reqFilList",
+            doc = "Required file list",
+            optional = false)
+    public List<File> requiredFileList;
+
+    /**
+     * Optional string list.
+     */
+    @Argument(fullName = "optionalStringList",
+            shortName = "optStrList",
+            doc = "An optional list of strings",
+            optional = true)
+    public List<String> optionalStringList = Collections.emptyList();
+
+    /**
+     * Required string list.
+     */
+    @Argument(fullName = "requiredStringList",
+            shortName = "reqStrList",
+            doc = "A required list of strings",
+            optional = false)
+    public List<String> requiredStringList = Collections.emptyList();
+
+    /**
+     * Required double
+     */
+    @Argument(fullName = "optionalDouble",
+            shortName = "optDouble",
+            doc = "Optionals double with initial value 2.15",
+            optional = true)
+     protected double optionalDouble = 2.15;
+
+    /**
+     * Optional double list.
+     */
+    @Argument(fullName = "optionalDoubleList",
+            shortName = "optDoubleList",
+            doc = "optionalDoubleList with initial values: 100.0, 99.9, 99.0, 90.0",
+            optional = true)
+    private List<Double> optionalDoubleList = new ArrayList<Double>(Arrays.asList(100.0, 99.9, 99.0, 90.0));
+
+    /**
+     * Optional flag.
+     */
+    @Argument(fullName = "optionalFlag",
+            shortName = "optFlag",
+            doc = "Optional flag, defaults to false.",
+            optional = true)
+    private boolean optionalFlag = false;
+
+    /**
+     * Hidden, optional list. This should not be displayed in normal help output.
+     */
+    @Hidden
+    @Argument(fullName = "hiddenOptionalList",
+            shortName = "hiddenOptList",
+            doc = "*******ERROR*******: this is supposed to be hidden so you shouldn't be seeing it in the doc output",
+            optional = true)
+    private ArrayList<Double> hiddenOptionalList = new ArrayList<>();
+
+    /**
+     * Advanced, Optional int
+     */
+    @Advanced
+    @Argument(fullName = "advancedOptionalInt",
+            shortName = "advancedOptInt",
+            doc = "advancedOptionalInt with initial value 1", optional = true)
+    protected int optionalInt = 1;
+
+    /**
+     * Deprecated string
+     */
+    @Deprecated
+    @Argument(fullName = "deprecatedString",
+            shortName = "depStr",
+            doc = "deprecated", optional = true)
+    protected int deprecatedString;
+
+    @Argument(doc="Use field name if no name in annotation.")
+    protected String usesFieldNameForArgName;
+
+    //////////////////////////////////////////////////////////////////////
+    // Embedded argument collection
+    @ArgumentCollection
+    private TestArgumentCollection testArugmentCollection = new TestArgumentCollection();
+
+    //////////////////////////////////////////////////////////////////////
+    // clpEnum
+    public enum TestEnum implements CommandLineParser.ClpEnum {
+
+        ENUM_VALUE_1("This is enum value 1."),
+        ENUM_VALUE_2("This is enum value 2.");
+
+        private String helpdoc;
+
+        TestEnum(final String helpdoc) {
+            this.helpdoc = helpdoc;
+        }
+
+        @Override
+        public String getHelpDoc() {
+            return helpdoc;
+        }
+    };
+
+    @Argument(fullName = "optionalClpEnum",
+            shortName = "optionalClpEnum",
+            doc = "Optional Clp enum",
+            optional = true)
+    TestEnum optionalClpEnum = TestEnum.ENUM_VALUE_1;
+
+    @Argument(fullName = "requiredClpEnum",
+            shortName = "requiredClpEnum",
+            doc = "Required Clp enum",
+            optional = false)
+    TestEnum requiredClpEnum;
+
+    /**
+     * Mutually exclusive args
+     */
+    @Argument(shortName = "mutexArg", fullName = "mutexArg",
+            mutex = {"READ1_ALIGNED_BAM", "READ2_ALIGNED_BAM"},
+            optional = true)
+    public List<File> mutexSourceField;
+
+    @Argument(shortName = "mutexTargetField1", fullName= "mutexTargetField1",
+            doc = "SAM/BAM/CRAM file(s) with alignment data from the first read of a pair.",
+            mutex = {"mutexSourceField"},
+            optional = true)
+    public List<File> mutexTargetField1;
+
+    @Argument(shortName = "mutexTargetField2", fullName= "mutexTargetField2",
+            doc = "SAM/BAM file(s) with alignment data from the second read of a pair.",
+            mutex = {"mutexSourceField"},
+            optional = true)
+    public List<File> mutexTargetField2;
+
+    // Command line plugin descriptor with optional arguments
+    @Override
+    public List<? extends CommandLinePluginDescriptor<?>> getPluginDescriptors() {
+        return Collections.singletonList(
+                new CommandLinePluginUnitTest.TestPluginDescriptor(
+                        Collections.singletonList(new CommandLinePluginUnitTest.TestDefaultPlugin())));
+    }
+}
diff --git a/src/test/java/org/broadinstitute/barclay/help/TestDocWorkUnitHandler.java b/src/test/java/org/broadinstitute/barclay/help/TestDocWorkUnitHandler.java
new file mode 100644
index 0000000..63ee748
--- /dev/null
+++ b/src/test/java/org/broadinstitute/barclay/help/TestDocWorkUnitHandler.java
@@ -0,0 +1,21 @@
+package org.broadinstitute.barclay.help;
+
+import java.util.*;
+
+public class TestDocWorkUnitHandler extends DefaultDocWorkUnitHandler {
+
+    public TestDocWorkUnitHandler(final HelpDoclet doclet) {
+        super(doclet);
+    }
+    @Override
+    protected void addCustomBindings(final DocWorkUnit currentworkUnit) {
+        super.addCustomBindings(currentworkUnit);
+        if (currentworkUnit.getProperty("testPlugin") == null) {
+            currentworkUnit.setProperty("testPlugin", new HashSet<HashMap<String, Object>>());
+        }
+    }
+
+    @Override
+    protected String getTagFilterPrefix(){ return "MyTag"; }
+
+}
diff --git a/src/test/java/org/broadinstitute/barclay/help/TestDoclet.java b/src/test/java/org/broadinstitute/barclay/help/TestDoclet.java
new file mode 100644
index 0000000..c32e6a5
--- /dev/null
+++ b/src/test/java/org/broadinstitute/barclay/help/TestDoclet.java
@@ -0,0 +1,50 @@
+package org.broadinstitute.barclay.help;
+
+import com.sun.javadoc.ClassDoc;
+import com.sun.javadoc.RootDoc;
+import org.broadinstitute.barclay.argparser.CommandLineProgramProperties;
+
+import java.io.IOException;
+import java.util.Map;
+
+/**
+ * For testing of help documentation generation.
+ */
+public class TestDoclet extends HelpDoclet {
+
+    public static boolean start(RootDoc rootDoc) {
+        try {
+            return new TestDoclet().startProcessDocs(rootDoc);
+        } catch (IOException e) {
+            throw new DocException("Exception processing javadoc", e);
+        }
+    }
+
+    @Override
+    protected DocWorkUnit createWorkUnit(
+        final DocumentedFeature documentedFeature,
+        final ClassDoc classDoc,
+        final Class<?> clazz)
+    {
+        return new DocWorkUnit(
+                new TestDocWorkUnitHandler(this),
+                documentedFeature,
+                classDoc,
+                clazz);
+    }
+
+    /**
+     * Trivial helper routine that returns the map of name and summary given the workUnit
+     * AND adds a super-category so that we can custom-order the categories in the index
+     *
+     * @param workUnit
+     * @return
+     */
+    @Override
+    protected final Map<String, String> getGroupMap(DocWorkUnit workUnit) {
+        Map<String, String> root = super.getGroupMap(workUnit);
+        root.put("supercat", "other");
+        return root;
+    }
+
+}
diff --git a/src/test/java/org/broadinstitute/barclay/help/TestExtraDocs.java b/src/test/java/org/broadinstitute/barclay/help/TestExtraDocs.java
new file mode 100644
index 0000000..2567c4a
--- /dev/null
+++ b/src/test/java/org/broadinstitute/barclay/help/TestExtraDocs.java
@@ -0,0 +1,17 @@
+package org.broadinstitute.barclay.help;
+
+import org.broadinstitute.barclay.argparser.Argument;
+
+/**
+ * Class for testing extraDocs property in docgen.
+ */
+ at DocumentedFeature(groupName = "Test extra docs group name")
+public class TestExtraDocs {
+
+    @Argument(fullName = "extraDocsArgument",
+            shortName = "extDocArg",
+            doc = "Extra stuff",
+            optional = true)
+    public String optionalFileList = "initial string value";
+
+}
diff --git a/src/test/java/org/broadinstitute/barclay/utils/UtilsUnitTest.java b/src/test/java/org/broadinstitute/barclay/utils/UtilsUnitTest.java
new file mode 100644
index 0000000..d197e6b
--- /dev/null
+++ b/src/test/java/org/broadinstitute/barclay/utils/UtilsUnitTest.java
@@ -0,0 +1,33 @@
+package org.broadinstitute.barclay.utils;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import static java.util.Arrays.asList;
+
+public class UtilsUnitTest {
+
+    @Test(expectedExceptions = IllegalArgumentException.class)
+    public void testNonNullThrows(){
+        final Object o = null;
+        Utils.nonNull(o);
+    }
+
+    @Test
+    public void testNonNullDoesNotThrow(){
+        final Object o = new Object();
+        Assert.assertSame(Utils.nonNull(o), o);
+    }
+
+    @Test(expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = "^The exception message$")
+    public void testNonNullWithMessageThrows() {
+        Utils.nonNull(null, "The exception message");
+    }
+
+    @Test
+    public void testNonNullWithMessageReturn() {
+        final Object testObject = new Object();
+        Assert.assertSame(Utils.nonNull(testObject, "some message"), testObject);
+    }
+
+}
diff --git a/src/test/resources/org/broadinstitute/barclay/help/expected/BashTabCompletionDoclet/bashTabCompletionDocletTestLaunch-completion.sh b/src/test/resources/org/broadinstitute/barclay/help/expected/BashTabCompletionDoclet/bashTabCompletionDocletTestLaunch-completion.sh
new file mode 100644
index 0000000..412bb1d
--- /dev/null
+++ b/src/test/resources/org/broadinstitute/barclay/help/expected/BashTabCompletionDoclet/bashTabCompletionDocletTestLaunch-completion.sh
@@ -0,0 +1,479 @@
+
+####################
+# Tab completion file to allow for easy use of this tool with the command-line using Bash.
+####################
+
+
+####################################################################################################
+
+# High-level caller/dispatch script information:
+
+CALLER_SCRIPT_NAME="bashTabCompletionDocletTestLaunch"
+
+# A description of these variables is below in the main completion function (_masterCompletionFunction)
+CS_PREFIX_OPTIONS_ALL_LEGAL_ARGUMENTS=(--pre-help --pre-info --pre-inputFile TestExtraDocs TestArgumentContainer )
+CS_PREFIX_OPTIONS_NORMAL_COMPLETION_ARGUMENTS=(--pre-help --pre-info --pre-inputFile TestExtraDocs TestArgumentContainer )
+CS_PREFIX_OPTIONS_ALL_ARGUMENT_VALUE_TYPES=("null" "null" "File" "null" "null" )
+CS_PREFIX_OPTIONS_MUTUALLY_EXCLUSIVE_ARGS=("--pre-help;pre-info,pre-inputFile" "--pre-info;pre-help,pre-inputFile")
+CS_PREFIX_OPTIONS_SYNONYMOUS_ARGS=("--pre-help;-prh" "--pre-inputFile;-prif")
+CS_PREFIX_OPTIONS_MIN_OCCURRENCES=(0 0 1 0 0 )
+CS_PREFIX_OPTIONS_MAX_OCCURRENCES=(1 1 1 1 1 )
+
+CS_POSTFIX_OPTIONS_ALL_LEGAL_ARGUMENTS=(--post-help --post-info --post-inputFile)
+CS_POSTFIX_OPTIONS_NORMAL_COMPLETION_ARGUMENTS=(--post-help --post-info --post-inputFile)
+CS_POSTFIX_OPTIONS_ALL_ARGUMENT_VALUE_TYPES=("null" "null" "File")
+CS_POSTFIX_OPTIONS_MUTUALLY_EXCLUSIVE_ARGS=("--post-help;post-info,post-inputFile" "--post-info;post-help,post-inputFile")
+CS_POSTFIX_OPTIONS_SYNONYMOUS_ARGS=("--post-help;-poh" "--post-inputFile;-poif")
+CS_POSTFIX_OPTIONS_MIN_OCCURRENCES=(0 0 1)
+CS_POSTFIX_OPTIONS_MAX_OCCURRENCES=(1 1 1)
+
+# Whether we have to worry about these extra script options at all.
+HAS_POSTFIX_OPTIONS="true"
+
+# All the tool names we are able to complete:
+ALL_TOOLS=(TestExtraDocs TestArgumentContainer )
+
+####################################################################################################
+
+# Get the name of the tool that we're currently trying to call
+_bashTabCompletionDocletTestLaunch_getToolName()
+{
+    # Naively go through each word in the line until we find one that is in our list of tools:
+    for word in ${COMP_WORDS[@]} ; do
+        if ( echo " ${ALL_TOOLS[@]} " | grep -q " ${word} " ) ; then
+            echo "${word}"
+            break
+        fi
+    done
+}
+
+# Get the index of the toolname inside COMP_WORDS
+_bashTabCompletionDocletTestLaunch_getToolNameIndex()
+{
+    # Naively go through each word in the line until we find one that is in our list of tools:
+    local ctr=0
+    for word in ${COMP_WORDS[@]} ; do
+        if ( echo " ${ALL_TOOLS[@]} " | grep -q " ${word} " ) ; then
+            echo $ctr
+            break
+        fi
+        let ctr=$ctr+1
+    done
+}
+
+# Get all possible tool names for the current command line if the current command is a
+# complete command on its own already.
+# If there is no complete command yet, then this prints nothing.
+_bashTabCompletionDocletTestLaunch_getAllPossibleToolNames()
+{
+# We want to return a list of possible tool names if and only if
+# the current word is a valid complete tool name
+# AND
+# the current word is also a substring in more than one tool name
+
+    local tool count matches toolList
+
+    let count=0
+    matches=false
+    toolList=()
+
+    # Go through tool names and get what matches and partial matches we have:
+    for tool in ${ALL_TOOLS[@]} ; do
+        if [[ "${COMP_WORDS[COMP_CWORD]}" == "${tool}" ]] ; then
+            matches=true
+            let count=$count+1
+            toolList+=($tool)
+        elif [[ "${tool}" == "${COMP_WORDS[COMP_CWORD]}"* ]] ; then
+            toolList+=($tool)
+        fi
+    done
+
+    # If we have a complete match, then we print out our partial matches as a space separated string.
+    # That way we have a list of all possible full completions for this match.
+    # For instance, if there was a tool named "read" and another named "readBetter" this would get both.
+    if $matches ; then
+        echo "${toolList[@]}"
+    fi
+}
+
+# Gets how many dependent arguments we have left to fill
+_bashTabCompletionDocletTestLaunch_getDependentArgumentCount()
+{
+    local depArgCount=0
+
+    for word in ${COMP_LINE} ; do
+        for depArg in ${DEPENDENT_ARGUMENTS[@]} ; do
+            if [[ "${word}" == "${depArg}" ]] ; then
+                $((depArgCount++))
+            fi
+        done
+    done
+
+    echo $depArgCount
+}
+
+# Resolves the given argument name to its long (normal) name
+_bashTabCompletionDocletTestLaunch_resolveVarName()
+{
+    local argName=$1
+    if [[ "${SYNONYMOUS_ARGS[@]}" == *"${argName}"* ]] ; then
+        echo "${SYNONYMOUS_ARGS[@]}" | sed -e "s#.* \\([a-zA-Z0-9;,_\\-]*${argName}[a-zA-Z0-9,;_\\-]*\\).*#\\1#g" -e 's#;.*##g'
+    else
+        echo "${argName}"
+    fi
+}
+
+# Checks if we need to complete the VALUE for an argument.
+# Prints the index in the given argument list of the corresponding argument whose value we must complete.
+# Takes as input 1 positional argument: the name of the last argument given to this script
+# Otherwise prints -1
+_bashTabCompletionDocletTestLaunch_needToCompleteArgValue()
+{
+    if [[ "${prev}" != "--" ]] ; then
+        local resolved=$( _bashTabCompletionDocletTestLaunch_resolveVarName ${prev} )
+
+        for (( i=0 ; i < ${#ALL_LEGAL_ARGUMENTS[@]} ; i++ )) ; do
+            if [[ "${resolved}" == "${ALL_LEGAL_ARGUMENTS[i]}" ]] ; then
+
+                # Make sure the argument isn't one that takes no additional value
+                # such as a flag.
+                if [[ "${ALL_ARGUMENT_VALUE_TYPES[i]}" != "null" ]] ; then
+                    echo "$i"
+                else
+                    echo "-1"
+                fi
+                return 0
+            fi
+        done
+    fi
+
+    echo "-1"
+}
+
+# Get the completion word list for the given argument type.
+# Prints the completion string to the screen
+_bashTabCompletionDocletTestLaunch_getCompletionWordList()
+{
+    # Normalize the type string so it's easier to deal with:
+    local argType=$( echo $1 | tr '[A-Z]' '[a-z]')
+
+    local isNumeric=false
+    local isFloating=false
+
+    local completionType=""
+
+    [[ "${argType}" == *"file"* ]]      && completionType='-A file'
+    [[ "${argType}" == *"folder"* ]]    && completionType='-A directory'
+    [[ "${argType}" == *"directory"* ]] && completionType='-A directory'
+    [[ "${argType}" == *"boolean"* ]]   && completionType='-W true false'
+
+    [[ "${argType}" == "int" ]]         && completionType='-W 0 1 2 3 4 5 6 7 8 9'   && isNumeric=true
+    [[ "${argType}" == *"[int]"* ]]     && completionType='-W 0 1 2 3 4 5 6 7 8 9'   && isNumeric=true
+    [[ "${argType}" == "long" ]]        && completionType='-W 0 1 2 3 4 5 6 7 8 9'   && isNumeric=true
+    [[ "${argType}" == *"[long]"* ]]    && completionType='-W 0 1 2 3 4 5 6 7 8 9'   && isNumeric=true
+
+    [[ "${argType}" == "double" ]]      && completionType='-W . 0 1 2 3 4 5 6 7 8 9' && isNumeric=true && isFloating=true
+    [[ "${argType}" == *"[double]"* ]]  && completionType='-W . 0 1 2 3 4 5 6 7 8 9' && isNumeric=true && isFloating=true
+    [[ "${argType}" == "float" ]]       && completionType='-W . 0 1 2 3 4 5 6 7 8 9' && isNumeric=true && isFloating=true
+    [[ "${argType}" == *"[float]"* ]]   && completionType='-W . 0 1 2 3 4 5 6 7 8 9' && isNumeric=true && isFloating=true
+
+    # If we have a number, we need to prepend the current completion to it so that we can continue to tab complete:
+    if $isNumeric ; then
+        completionType=$( echo ${completionType} | sed -e "s#\([0-9]\)#$cur\1#g" )
+
+        # If we're floating point, we need to make sure we don't complete a `.` character
+        # if one already exists in our number:
+        if $isFloating ; then
+            echo "$cur" | grep -o '\.' &> /dev/null
+            local r=$?
+
+            [[ $r -eq 0 ]] && completionType=$( echo ${completionType} | awk '{$2="" ; print}' )
+        fi
+    fi
+
+    echo "${completionType}"
+}
+
+# Function to handle the completion tasks once we have populated our arg variables
+# When passed an argument handles the case for the caller script.
+_bashTabCompletionDocletTestLaunch_handleArgs()
+{
+    # Argument offset index is used in the special case where we are past the " -- " delimiter.
+    local argOffsetIndex=0
+
+    # We handle the beginning differently if this function was called with an argument
+    if [[ $# -eq 0 ]] ; then
+        # Get the number of arguments we have input so far:
+        local toolNameIndex=$(_bashTabCompletionDocletTestLaunch_getToolNameIndex)
+        local numArgs=$((COMP_CWORD-toolNameIndex-1))
+
+        # Now we check to see what kind of argument we are on right now
+        # We handle each type separately by order of precedence:
+        if [[ ${numArgs} -lt ${NUM_POSITIONAL_ARGUMENTS} ]] ; then
+            # We must complete a positional argument.
+            # Assume that positional arguments are all FILES:
+            COMPREPLY=( $(compgen -A file -- $cur) )
+            return 0
+        fi
+
+        # Dependent arguments must come right after positional arguments
+        # We must check to see how many dependent arguments we've gotten so far:
+        local numDepArgs=$( _bashTabCompletionDocletTestLaunch_getDependentArgumentCount )
+
+        if [[ $numDepArgs -lt ${#DEPENDENT_ARGUMENTS[@]} ]] ; then
+            # We must complete a dependent argument next.
+            COMPREPLY=( $(compgen -W '${DEPENDENT_ARGUMENTS[@]}' -- $cur) )
+            return 0
+        fi
+    elif [[ "${1}" == "POSTFIX_OPTIONS" ]] ; then
+        # Get the index of the special delimiter.
+        # we ignore everything up to and including it.
+        for (( i=0; i < COMP_CWORD ; i++ )) ; do
+            if [[ "${COMP_WORDS[i]}" == "--" ]] ; then
+                let argOffsetIndex=$i+1
+            fi
+        done
+    fi
+    # NOTE: We don't need to worry about the prefix options case.
+    #       The caller will specify it and it skips the two special cases above.
+
+    # First we must resolve all arguments to their full names
+    # This is necessary to save time later because of short argument names / synonyms
+    local resolvedArgList=()
+    for (( i=argOffsetIndex ; i < COMP_CWORD ; i++ )) ; do
+        prevArg=${COMP_WORDS[i]}
+
+        # Skip the current word to be completed:
+        [[ "${prevArg}" == "${cur}" ]] && continue
+
+        # Check if this has synonyms:
+        if [[ "${SYNONYMOUS_ARGS[@]}" == *"${prevArg}"* ]] ; then
+
+            local resolvedArg=$( _bashTabCompletionDocletTestLaunch_resolveVarName "${prevArg}" )
+            resolvedArgList+=($resolvedArg)
+
+        # Make sure this is an argument:
+        elif [[ "${ALL_LEGAL_ARGUMENTS[@]}" == *"${prevArg}"* ]] ; then
+            resolvedArgList+=($prevArg)
+        fi
+    done
+
+    # Check to see if the last thing we typed was a complete argument.
+    # If so, we must complete the VALUE for the argument, not the
+    # argument itself:
+    # Note: This is shorthand for last element in the array:
+    local argToComplete=$( _bashTabCompletionDocletTestLaunch_needToCompleteArgValue )
+
+    if [[ $argToComplete -ne -1 ]] ; then
+        # We must complete the VALUE for an argument.
+
+        # Get the argument type.
+        local valueType=${ALL_ARGUMENT_VALUE_TYPES[argToComplete]}
+
+        # Get the correct completion string for the type:
+        local completionString=$( _bashTabCompletionDocletTestLaunch_getCompletionWordList "${valueType}" )
+
+        if [[ ${#completionString} -eq 0 ]] ; then
+            # We don't have any information on the type to complete.
+            # We use the default SHELL behavior:
+            COMPREPLY=()
+        else
+            # We have a completion option.  Let's plug it in:
+            local compOperator=$( echo "${completionString}" | awk '{print $1}' )
+            local compOptions=$( echo "${completionString}" | awk '{$1="" ; print}' )
+
+            case ${compOperator} in
+                -A) COMPREPLY=( $(compgen -A ${compOptions} -- $cur) ) ;;
+                -W) COMPREPLY=( $(compgen -W '${compOptions}' -- $cur) ) ;;
+                 *) COMPREPLY=() ;;
+            esac
+
+        fi
+        return 0
+    fi
+
+    # We must create a list of the valid remaining arguments:
+
+    # Create a list of all arguments that are
+    # mutually exclusive with arguments we have already specified
+    local mutex_list=""
+    for prevArg in ${resolvedArgList[@]} ; do
+        if [[ "${MUTUALLY_EXCLUSIVE_ARGS[@]}" == *"${prevArg};"* ]] ; then
+            local mutexArgs=$( echo "${MUTUALLY_EXCLUSIVE_ARGS[@]}" | sed -e "s#.*${prevArg};\([a-zA-Z0-9_,\-]*\) .*#\1#g" -e "s#,# --#g" -e "s#^#--#g" )
+            mutex_list="${mutex_list}${mutexArgs}"
+        fi
+    done
+
+    local remaining_legal_arguments=()
+    for (( i=0; i < ${#NORMAL_COMPLETION_ARGUMENTS[@]} ; i++ )) ; do
+        local legalArg=${NORMAL_COMPLETION_ARGUMENTS[i]}
+        local okToAdd=true
+
+        # Get the number of times this has occurred in the arguments already:
+        local numPrevOccurred=$( grep -o -- "${legalArg}" <<< "${resolvedArgList[@]}" | wc -l | awk '{print $1}' )
+
+        if [[ $numPrevOccurred -lt "${MAX_OCCURRENCES[i]}" ]] ; then
+
+            # Make sure this arg isn't mutually exclusive to another argument that we've already had:
+            if [[ "${mutex_list}" ==    "${legalArg} "* ]] ||
+               [[ "${mutex_list}" ==  *" ${legalArg} "* ]] ||
+               [[ "${mutex_list}" ==  *" ${legalArg}"  ]] ; then
+                okToAdd=false
+            fi
+
+            # Check if we're still good to add in the argument:
+            if $okToAdd ; then
+                # Add in the argument:
+                remaining_legal_arguments+=($legalArg)
+
+                # Add in the synonyms of the argument:
+                if [[ "${SYNONYMOUS_ARGS[@]}" == *"${legalArg}"* ]] ; then
+                    local synonymString=$( echo "${SYNONYMOUS_ARGS[@]}" | sed -e "s#.*${legalArg};\([a-zA-Z0-9_,\-]*\).*#\1#g" -e "s#,# #g"  )
+                    remaining_legal_arguments+=($synonymString)
+                fi
+            fi
+        fi
+
+    done
+
+    # Add in the special option "--" which separates tool options from meta-options if they're necessary:
+    if $HAS_POSTFIX_OPTIONS ; then
+        if [[ $# -eq 0 ]] || [[ "${1}" == "PREFIX_OPTIONS"  ]] ; then
+            remaining_legal_arguments+=("--")
+        fi
+    fi
+
+    COMPREPLY=( $(compgen -W '${remaining_legal_arguments[@]}' -- $cur) )
+    return 0
+}
+
+####################################################################################################
+
+_bashTabCompletionDocletTestLaunch_masterCompletionFunction()
+{
+    # Set up global variables for the functions that do completion:
+    prev=${COMP_WORDS[COMP_CWORD-1]}
+    cur=${COMP_WORDS[COMP_CWORD]}
+
+    # How many positional arguments a tool will have.
+    # These positional arguments must come directly after a tool name.
+    NUM_POSITIONAL_ARGUMENTS=0
+
+    # The types of the positional arguments, in the order in which they must be specified
+    # on the command-line.
+    POSITIONAL_ARGUMENT_TYPE=()
+
+    # The set of legal arguments that aren't dependent arguments.
+    # (A dependent argument is an argument that must occur immediately after
+    # all positional arguments.)
+    NORMAL_COMPLETION_ARGUMENTS=()
+
+    # The set of ALL legal arguments
+    # Corresponds by index to the type of those arguments in ALL_ARGUMENT_VALUE_TYPES
+    ALL_LEGAL_ARGUMENTS=()
+
+    # The types of ALL legal arguments
+    # Corresponds by index to the names of those arguments in ALL_LEGAL_ARGUMENTS
+    ALL_ARGUMENT_VALUE_TYPES=()
+
+    # Arguments that are mutually exclusive.
+    # These are listed here as arguments concatenated together with delimiters:
+    # <Main argument>;<Mutex Argument 1>[,<Mutex Argument 2>,...]
+    MUTUALLY_EXCLUSIVE_ARGS=()
+
+    # Alternate names of arguments.
+    # These are listed here as arguments concatenated together with delimiters.
+    # <Main argument>;<Synonym Argument 1>[,<Synonym Argument 2>,...]
+    SYNONYMOUS_ARGS=()
+
+    # The minimum number of times an argument can occur.
+    MIN_OCCURRENCES=()
+
+    # The maximum number of times an argument can occur.
+    MAX_OCCURRENCES=()
+
+    # Set up locals for this function:
+    local toolName=$( _bashTabCompletionDocletTestLaunch_getToolName )
+
+    # Get possible tool matches:
+    local possibleToolMatches=$( _bashTabCompletionDocletTestLaunch_getAllPossibleToolNames )
+
+    # Check if we have postfix options
+    # and if we now need to go through them:
+    if $HAS_POSTFIX_OPTIONS && [[ "${COMP_WORDS[@]}" == *" -- "* ]] ; then
+        NUM_POSITIONAL_ARGUMENTS=0
+        POSITIONAL_ARGUMENT_TYPE=()
+        DEPENDENT_ARGUMENTS=()
+        NORMAL_COMPLETION_ARGUMENTS=("${CS_POSTFIX_OPTIONS_NORMAL_COMPLETION_ARGUMENTS[@]}")
+        MUTUALLY_EXCLUSIVE_ARGS=("${CS_POSTFIX_OPTIONS_MUTUALLY_EXCLUSIVE_ARGS[@]}")
+        SYNONYMOUS_ARGS=("${CS_POSTFIX_OPTIONS_SYNONYMOUS_ARGS[@]}")
+        MIN_OCCURRENCES=("${CS_POSTFIX_OPTIONS_MIN_OCCURRENCES[@]}")
+        MAX_OCCURRENCES=("${CS_POSTFIX_OPTIONS_MAX_OCCURRENCES[@]}")
+        ALL_LEGAL_ARGUMENTS=("${CS_POSTFIX_OPTIONS_ALL_LEGAL_ARGUMENTS[@]}")
+        ALL_ARGUMENT_VALUE_TYPES=("${CS_POSTFIX_OPTIONS_ALL_ARGUMENT_VALUE_TYPES[@]}")
+
+        # Complete the arguments for the base script:
+        # Strictly speaking, what the argument to this function is doesn't matter.
+        _bashTabCompletionDocletTestLaunch_handleArgs POSTFIX_OPTIONS
+
+    # Check if we have a complete tool match that may match more than one tool:
+    elif [[ ${#possibleToolMatches} -ne 0 ]] ; then
+
+        # Set our reply as a list of the possible tool matches:
+        COMPREPLY=( $(compgen -W '${possibleToolMatches[@]}' -- $cur) )
+
+    elif [[ ${toolName} == "TestExtraDocs" ]] ; then
+
+        # Set up the completion information for this tool:
+        DEPENDENT_ARGUMENTS=()
+        NORMAL_COMPLETION_ARGUMENTS=()
+        MUTUALLY_EXCLUSIVE_ARGS=()
+        SYNONYMOUS_ARGS=()
+        MIN_OCCURRENCES=()
+        MAX_OCCURRENCES=()
+        ALL_LEGAL_ARGUMENTS=()
+        ALL_ARGUMENT_VALUE_TYPES=()
+
+        # Complete the arguments for this tool:
+        _bashTabCompletionDocletTestLaunch_handleArgs
+    elif [[ ${toolName} == "TestArgumentContainer" ]] ; then
+
+        # Set up the completion information for this tool:
+        NUM_POSITIONAL_ARGUMENTS=2
+        POSITIONAL_ARGUMENT_TYPE=("List[File]")
+        DEPENDENT_ARGUMENTS=()
+        NORMAL_COMPLETION_ARGUMENTS=(--requiredClpEnum --requiredFileList --requiredInputFilesFromArgCollection --requiredStringInputFromArgCollection --requiredStringList --usesFieldNameForArgName --mutexArg --mutexTargetField1 --mutexTargetField2 --optionalClpEnum --optionalDouble --optionalDoubleList --optionalFileList --optionalFlag --optionalInputFilesFromArgCollection --optionalStringInputFromArgCollection --optionalStringList --testPlugin --advancedOptionalInt --deprecatedString )
+        MUTUALLY_EXCLUSIVE_ARGS=("--mutexArg;READ1_ALIGNED_BAM,READ2_ALIGNED_BAM" "--mutexTargetField1;mutexSourceField" "--mutexTargetField2;mutexSourceField" )
+        SYNONYMOUS_ARGS=("--requiredClpEnum;-requiredClpEnum" "--requiredFileList;-reqFilList" "--requiredInputFilesFromArgCollection;-rRequiredInputFilesFromArgCollection" "--requiredStringInputFromArgCollection;-requiredStringInputFromArgCollection" "--requiredStringList;-reqStrList" "--mutexArg;-mutexArg" "--mutexTargetField1;-mutexTargetField1" "--mutexTargetField2;-mutexTargetField2" "--optionalClpEnum;-optionalClpEnum" "--optionalDouble;-optDouble" "--optionalDoubleList;-optDoubleL [...]
+        MIN_OCCURRENCES=(0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 )
+        MAX_OCCURRENCES=(2147483647 2147483647 2147483647 2147483647 2147483647 2147483647 2147483647 2147483647 2147483647 2147483647 2147483647 2147483647 2147483647 2147483647 2147483647 2147483647 2147483647 2147483647 2147483647 2147483647 )
+        ALL_LEGAL_ARGUMENTS=(--requiredClpEnum --requiredFileList --requiredInputFilesFromArgCollection --requiredStringInputFromArgCollection --requiredStringList --usesFieldNameForArgName --mutexArg --mutexTargetField1 --mutexTargetField2 --optionalClpEnum --optionalDouble --optionalDoubleList --optionalFileList --optionalFlag --optionalInputFilesFromArgCollection --optionalStringInputFromArgCollection --optionalStringList --testPlugin --advancedOptionalInt --deprecatedString )
+        ALL_ARGUMENT_VALUE_TYPES=("TestEnum" "List[File]" "List[File]" "String" "List[String]" "String" "List[File]" "List[File]" "List[File]" "TestEnum" "double" "List[Double]" "List[File]" "boolean" "List[File]" "String" "List[String]" "List[String]" "int" "int" )
+
+        # Complete the arguments for this tool:
+        _bashTabCompletionDocletTestLaunch_handleArgs
+
+    # We have no postfix options or tool options.
+    # We now must complete any prefix options and the tools themselves.
+    # These are defined at the top.
+    else
+        NUM_POSITIONAL_ARGUMENTS=0
+        POSITIONAL_ARGUMENT_TYPE=()
+        DEPENDENT_ARGUMENTS=()
+        NORMAL_COMPLETION_ARGUMENTS=("${CS_PREFIX_OPTIONS_NORMAL_COMPLETION_ARGUMENTS[@]}")
+        MUTUALLY_EXCLUSIVE_ARGS=("${CS_PREFIX_OPTIONS_MUTUALLY_EXCLUSIVE_ARGS[@]}")
+        SYNONYMOUS_ARGS=("${CS_PREFIX_OPTIONS_SYNONYMOUS_ARGS[@]}")
+        MIN_OCCURRENCES=("${CS_PREFIX_OPTIONS_MIN_OCCURRENCES[@]}")
+        MAX_OCCURRENCES=("${CS_PREFIX_OPTIONS_MAX_OCCURRENCES[@]}")
+        ALL_LEGAL_ARGUMENTS=("${CS_PREFIX_OPTIONS_ALL_LEGAL_ARGUMENTS[@]}")
+        ALL_ARGUMENT_VALUE_TYPES=("${CS_PREFIX_OPTIONS_ALL_ARGUMENT_VALUE_TYPES[@]}")
+
+        # Complete the arguments for the prefix arguments and tools:
+        _bashTabCompletionDocletTestLaunch_handleArgs PREFIX_OPTIONS
+    fi
+}
+
+complete -o default -F _bashTabCompletionDocletTestLaunch_masterCompletionFunction ${CALLER_SCRIPT_NAME}
+
+
+
diff --git a/src/test/resources/org/broadinstitute/barclay/help/expected/BashTabCompletionDoclet/bashTabCompletionDocletTestLaunchWithDefaults-completion.sh b/src/test/resources/org/broadinstitute/barclay/help/expected/BashTabCompletionDoclet/bashTabCompletionDocletTestLaunchWithDefaults-completion.sh
new file mode 100644
index 0000000..9df3628
--- /dev/null
+++ b/src/test/resources/org/broadinstitute/barclay/help/expected/BashTabCompletionDoclet/bashTabCompletionDocletTestLaunchWithDefaults-completion.sh
@@ -0,0 +1,479 @@
+
+####################
+# Tab completion file to allow for easy use of this tool with the command-line using Bash.
+####################
+
+
+####################################################################################################
+
+# High-level caller/dispatch script information:
+
+CALLER_SCRIPT_NAME="bashTabCompletionDocletTestLaunchWithDefaults"
+
+# A description of these variables is below in the main completion function (_masterCompletionFunction)
+CS_PREFIX_OPTIONS_ALL_LEGAL_ARGUMENTS=( TestExtraDocs TestArgumentContainer )
+CS_PREFIX_OPTIONS_NORMAL_COMPLETION_ARGUMENTS=( TestExtraDocs TestArgumentContainer )
+CS_PREFIX_OPTIONS_ALL_ARGUMENT_VALUE_TYPES=( "null" "null" )
+CS_PREFIX_OPTIONS_MUTUALLY_EXCLUSIVE_ARGS=()
+CS_PREFIX_OPTIONS_SYNONYMOUS_ARGS=()
+CS_PREFIX_OPTIONS_MIN_OCCURRENCES=( 0 0 )
+CS_PREFIX_OPTIONS_MAX_OCCURRENCES=( 1 1 )
+
+CS_POSTFIX_OPTIONS_ALL_LEGAL_ARGUMENTS=()
+CS_POSTFIX_OPTIONS_NORMAL_COMPLETION_ARGUMENTS=()
+CS_POSTFIX_OPTIONS_ALL_ARGUMENT_VALUE_TYPES=()
+CS_POSTFIX_OPTIONS_MUTUALLY_EXCLUSIVE_ARGS=()
+CS_POSTFIX_OPTIONS_SYNONYMOUS_ARGS=()
+CS_POSTFIX_OPTIONS_MIN_OCCURRENCES=()
+CS_POSTFIX_OPTIONS_MAX_OCCURRENCES=()
+
+# Whether we have to worry about these extra script options at all.
+HAS_POSTFIX_OPTIONS="false"
+
+# All the tool names we are able to complete:
+ALL_TOOLS=(TestExtraDocs TestArgumentContainer )
+
+####################################################################################################
+
+# Get the name of the tool that we're currently trying to call
+_bashTabCompletionDocletTestLaunchWithDefaults_getToolName()
+{
+    # Naively go through each word in the line until we find one that is in our list of tools:
+    for word in ${COMP_WORDS[@]} ; do
+        if ( echo " ${ALL_TOOLS[@]} " | grep -q " ${word} " ) ; then
+            echo "${word}"
+            break
+        fi
+    done
+}
+
+# Get the index of the toolname inside COMP_WORDS
+_bashTabCompletionDocletTestLaunchWithDefaults_getToolNameIndex()
+{
+    # Naively go through each word in the line until we find one that is in our list of tools:
+    local ctr=0
+    for word in ${COMP_WORDS[@]} ; do
+        if ( echo " ${ALL_TOOLS[@]} " | grep -q " ${word} " ) ; then
+            echo $ctr
+            break
+        fi
+        let ctr=$ctr+1
+    done
+}
+
+# Get all possible tool names for the current command line if the current command is a
+# complete command on its own already.
+# If there is no complete command yet, then this prints nothing.
+_bashTabCompletionDocletTestLaunchWithDefaults_getAllPossibleToolNames()
+{
+# We want to return a list of possible tool names if and only if
+# the current word is a valid complete tool name
+# AND
+# the current word is also a substring in more than one tool name
+
+    local tool count matches toolList
+
+    let count=0
+    matches=false
+    toolList=()
+
+    # Go through tool names and get what matches and partial matches we have:
+    for tool in ${ALL_TOOLS[@]} ; do
+        if [[ "${COMP_WORDS[COMP_CWORD]}" == "${tool}" ]] ; then
+            matches=true
+            let count=$count+1
+            toolList+=($tool)
+        elif [[ "${tool}" == "${COMP_WORDS[COMP_CWORD]}"* ]] ; then
+            toolList+=($tool)
+        fi
+    done
+
+    # If we have a complete match, then we print out our partial matches as a space separated string.
+    # That way we have a list of all possible full completions for this match.
+    # For instance, if there was a tool named "read" and another named "readBetter" this would get both.
+    if $matches ; then
+        echo "${toolList[@]}"
+    fi
+}
+
+# Gets how many dependent arguments we have left to fill
+_bashTabCompletionDocletTestLaunchWithDefaults_getDependentArgumentCount()
+{
+    local depArgCount=0
+
+    for word in ${COMP_LINE} ; do
+        for depArg in ${DEPENDENT_ARGUMENTS[@]} ; do
+            if [[ "${word}" == "${depArg}" ]] ; then
+                $((depArgCount++))
+            fi
+        done
+    done
+
+    echo $depArgCount
+}
+
+# Resolves the given argument name to its long (normal) name
+_bashTabCompletionDocletTestLaunchWithDefaults_resolveVarName()
+{
+    local argName=$1
+    if [[ "${SYNONYMOUS_ARGS[@]}" == *"${argName}"* ]] ; then
+        echo "${SYNONYMOUS_ARGS[@]}" | sed -e "s#.* \\([a-zA-Z0-9;,_\\-]*${argName}[a-zA-Z0-9,;_\\-]*\\).*#\\1#g" -e 's#;.*##g'
+    else
+        echo "${argName}"
+    fi
+}
+
+# Checks if we need to complete the VALUE for an argument.
+# Prints the index in the given argument list of the corresponding argument whose value we must complete.
+# Takes as input 1 positional argument: the name of the last argument given to this script
+# Otherwise prints -1
+_bashTabCompletionDocletTestLaunchWithDefaults_needToCompleteArgValue()
+{
+    if [[ "${prev}" != "--" ]] ; then
+        local resolved=$( _bashTabCompletionDocletTestLaunchWithDefaults_resolveVarName ${prev} )
+
+        for (( i=0 ; i < ${#ALL_LEGAL_ARGUMENTS[@]} ; i++ )) ; do
+            if [[ "${resolved}" == "${ALL_LEGAL_ARGUMENTS[i]}" ]] ; then
+
+                # Make sure the argument isn't one that takes no additional value
+                # such as a flag.
+                if [[ "${ALL_ARGUMENT_VALUE_TYPES[i]}" != "null" ]] ; then
+                    echo "$i"
+                else
+                    echo "-1"
+                fi
+                return 0
+            fi
+        done
+    fi
+
+    echo "-1"
+}
+
+# Get the completion word list for the given argument type.
+# Prints the completion string to the screen
+_bashTabCompletionDocletTestLaunchWithDefaults_getCompletionWordList()
+{
+    # Normalize the type string so it's easier to deal with:
+    local argType=$( echo $1 | tr '[A-Z]' '[a-z]')
+
+    local isNumeric=false
+    local isFloating=false
+
+    local completionType=""
+
+    [[ "${argType}" == *"file"* ]]      && completionType='-A file'
+    [[ "${argType}" == *"folder"* ]]    && completionType='-A directory'
+    [[ "${argType}" == *"directory"* ]] && completionType='-A directory'
+    [[ "${argType}" == *"boolean"* ]]   && completionType='-W true false'
+
+    [[ "${argType}" == "int" ]]         && completionType='-W 0 1 2 3 4 5 6 7 8 9'   && isNumeric=true
+    [[ "${argType}" == *"[int]"* ]]     && completionType='-W 0 1 2 3 4 5 6 7 8 9'   && isNumeric=true
+    [[ "${argType}" == "long" ]]        && completionType='-W 0 1 2 3 4 5 6 7 8 9'   && isNumeric=true
+    [[ "${argType}" == *"[long]"* ]]    && completionType='-W 0 1 2 3 4 5 6 7 8 9'   && isNumeric=true
+
+    [[ "${argType}" == "double" ]]      && completionType='-W . 0 1 2 3 4 5 6 7 8 9' && isNumeric=true && isFloating=true
+    [[ "${argType}" == *"[double]"* ]]  && completionType='-W . 0 1 2 3 4 5 6 7 8 9' && isNumeric=true && isFloating=true
+    [[ "${argType}" == "float" ]]       && completionType='-W . 0 1 2 3 4 5 6 7 8 9' && isNumeric=true && isFloating=true
+    [[ "${argType}" == *"[float]"* ]]   && completionType='-W . 0 1 2 3 4 5 6 7 8 9' && isNumeric=true && isFloating=true
+
+    # If we have a number, we need to prepend the current completion to it so that we can continue to tab complete:
+    if $isNumeric ; then
+        completionType=$( echo ${completionType} | sed -e "s#\([0-9]\)#$cur\1#g" )
+
+        # If we're floating point, we need to make sure we don't complete a `.` character
+        # if one already exists in our number:
+        if $isFloating ; then
+            echo "$cur" | grep -o '\.' &> /dev/null
+            local r=$?
+
+            [[ $r -eq 0 ]] && completionType=$( echo ${completionType} | awk '{$2="" ; print}' )
+        fi
+    fi
+
+    echo "${completionType}"
+}
+
+# Function to handle the completion tasks once we have populated our arg variables
+# When passed an argument handles the case for the caller script.
+_bashTabCompletionDocletTestLaunchWithDefaults_handleArgs()
+{
+    # Argument offset index is used in the special case where we are past the " -- " delimiter.
+    local argOffsetIndex=0
+
+    # We handle the beginning differently if this function was called with an argument
+    if [[ $# -eq 0 ]] ; then
+        # Get the number of arguments we have input so far:
+        local toolNameIndex=$(_bashTabCompletionDocletTestLaunchWithDefaults_getToolNameIndex)
+        local numArgs=$((COMP_CWORD-toolNameIndex-1))
+
+        # Now we check to see what kind of argument we are on right now
+        # We handle each type separately by order of precedence:
+        if [[ ${numArgs} -lt ${NUM_POSITIONAL_ARGUMENTS} ]] ; then
+            # We must complete a positional argument.
+            # Assume that positional arguments are all FILES:
+            COMPREPLY=( $(compgen -A file -- $cur) )
+            return 0
+        fi
+
+        # Dependent arguments must come right after positional arguments
+        # We must check to see how many dependent arguments we've gotten so far:
+        local numDepArgs=$( _bashTabCompletionDocletTestLaunchWithDefaults_getDependentArgumentCount )
+
+        if [[ $numDepArgs -lt ${#DEPENDENT_ARGUMENTS[@]} ]] ; then
+            # We must complete a dependent argument next.
+            COMPREPLY=( $(compgen -W '${DEPENDENT_ARGUMENTS[@]}' -- $cur) )
+            return 0
+        fi
+    elif [[ "${1}" == "POSTFIX_OPTIONS" ]] ; then
+        # Get the index of the special delimiter.
+        # we ignore everything up to and including it.
+        for (( i=0; i < COMP_CWORD ; i++ )) ; do
+            if [[ "${COMP_WORDS[i]}" == "--" ]] ; then
+                let argOffsetIndex=$i+1
+            fi
+        done
+    fi
+    # NOTE: We don't need to worry about the prefix options case.
+    #       The caller will specify it and it skips the two special cases above.
+
+    # First we must resolve all arguments to their full names
+    # This is necessary to save time later because of short argument names / synonyms
+    local resolvedArgList=()
+    for (( i=argOffsetIndex ; i < COMP_CWORD ; i++ )) ; do
+        prevArg=${COMP_WORDS[i]}
+
+        # Skip the current word to be completed:
+        [[ "${prevArg}" == "${cur}" ]] && continue
+
+        # Check if this has synonyms:
+        if [[ "${SYNONYMOUS_ARGS[@]}" == *"${prevArg}"* ]] ; then
+
+            local resolvedArg=$( _bashTabCompletionDocletTestLaunchWithDefaults_resolveVarName "${prevArg}" )
+            resolvedArgList+=($resolvedArg)
+
+        # Make sure this is an argument:
+        elif [[ "${ALL_LEGAL_ARGUMENTS[@]}" == *"${prevArg}"* ]] ; then
+            resolvedArgList+=($prevArg)
+        fi
+    done
+
+    # Check to see if the last thing we typed was a complete argument.
+    # If so, we must complete the VALUE for the argument, not the
+    # argument itself:
+    # Note: This is shorthand for last element in the array:
+    local argToComplete=$( _bashTabCompletionDocletTestLaunchWithDefaults_needToCompleteArgValue )
+
+    if [[ $argToComplete -ne -1 ]] ; then
+        # We must complete the VALUE for an argument.
+
+        # Get the argument type.
+        local valueType=${ALL_ARGUMENT_VALUE_TYPES[argToComplete]}
+
+        # Get the correct completion string for the type:
+        local completionString=$( _bashTabCompletionDocletTestLaunchWithDefaults_getCompletionWordList "${valueType}" )
+
+        if [[ ${#completionString} -eq 0 ]] ; then
+            # We don't have any information on the type to complete.
+            # We use the default SHELL behavior:
+            COMPREPLY=()
+        else
+            # We have a completion option.  Let's plug it in:
+            local compOperator=$( echo "${completionString}" | awk '{print $1}' )
+            local compOptions=$( echo "${completionString}" | awk '{$1="" ; print}' )
+
+            case ${compOperator} in
+                -A) COMPREPLY=( $(compgen -A ${compOptions} -- $cur) ) ;;
+                -W) COMPREPLY=( $(compgen -W '${compOptions}' -- $cur) ) ;;
+                 *) COMPREPLY=() ;;
+            esac
+
+        fi
+        return 0
+    fi
+
+    # We must create a list of the valid remaining arguments:
+
+    # Create a list of all arguments that are
+    # mutually exclusive with arguments we have already specified
+    local mutex_list=""
+    for prevArg in ${resolvedArgList[@]} ; do
+        if [[ "${MUTUALLY_EXCLUSIVE_ARGS[@]}" == *"${prevArg};"* ]] ; then
+            local mutexArgs=$( echo "${MUTUALLY_EXCLUSIVE_ARGS[@]}" | sed -e "s#.*${prevArg};\([a-zA-Z0-9_,\-]*\) .*#\1#g" -e "s#,# --#g" -e "s#^#--#g" )
+            mutex_list="${mutex_list}${mutexArgs}"
+        fi
+    done
+
+    local remaining_legal_arguments=()
+    for (( i=0; i < ${#NORMAL_COMPLETION_ARGUMENTS[@]} ; i++ )) ; do
+        local legalArg=${NORMAL_COMPLETION_ARGUMENTS[i]}
+        local okToAdd=true
+
+        # Get the number of times this has occurred in the arguments already:
+        local numPrevOccurred=$( grep -o -- "${legalArg}" <<< "${resolvedArgList[@]}" | wc -l | awk '{print $1}' )
+
+        if [[ $numPrevOccurred -lt "${MAX_OCCURRENCES[i]}" ]] ; then
+
+            # Make sure this arg isn't mutually exclusive to another argument that we've already had:
+            if [[ "${mutex_list}" ==    "${legalArg} "* ]] ||
+               [[ "${mutex_list}" ==  *" ${legalArg} "* ]] ||
+               [[ "${mutex_list}" ==  *" ${legalArg}"  ]] ; then
+                okToAdd=false
+            fi
+
+            # Check if we're still good to add in the argument:
+            if $okToAdd ; then
+                # Add in the argument:
+                remaining_legal_arguments+=($legalArg)
+
+                # Add in the synonyms of the argument:
+                if [[ "${SYNONYMOUS_ARGS[@]}" == *"${legalArg}"* ]] ; then
+                    local synonymString=$( echo "${SYNONYMOUS_ARGS[@]}" | sed -e "s#.*${legalArg};\([a-zA-Z0-9_,\-]*\).*#\1#g" -e "s#,# #g"  )
+                    remaining_legal_arguments+=($synonymString)
+                fi
+            fi
+        fi
+
+    done
+
+    # Add in the special option "--" which separates tool options from meta-options if they're necessary:
+    if $HAS_POSTFIX_OPTIONS ; then
+        if [[ $# -eq 0 ]] || [[ "${1}" == "PREFIX_OPTIONS"  ]] ; then
+            remaining_legal_arguments+=("--")
+        fi
+    fi
+
+    COMPREPLY=( $(compgen -W '${remaining_legal_arguments[@]}' -- $cur) )
+    return 0
+}
+
+####################################################################################################
+
+_bashTabCompletionDocletTestLaunchWithDefaults_masterCompletionFunction()
+{
+    # Set up global variables for the functions that do completion:
+    prev=${COMP_WORDS[COMP_CWORD-1]}
+    cur=${COMP_WORDS[COMP_CWORD]}
+
+    # How many positional arguments a tool will have.
+    # These positional arguments must come directly after a tool name.
+    NUM_POSITIONAL_ARGUMENTS=0
+
+    # The types of the positional arguments, in the order in which they must be specified
+    # on the command-line.
+    POSITIONAL_ARGUMENT_TYPE=()
+
+    # The set of legal arguments that aren't dependent arguments.
+    # (A dependent argument is an argument that must occur immediately after
+    # all positional arguments.)
+    NORMAL_COMPLETION_ARGUMENTS=()
+
+    # The set of ALL legal arguments
+    # Corresponds by index to the type of those arguments in ALL_ARGUMENT_VALUE_TYPES
+    ALL_LEGAL_ARGUMENTS=()
+
+    # The types of ALL legal arguments
+    # Corresponds by index to the names of those arguments in ALL_LEGAL_ARGUMENTS
+    ALL_ARGUMENT_VALUE_TYPES=()
+
+    # Arguments that are mutually exclusive.
+    # These are listed here as arguments concatenated together with delimiters:
+    # <Main argument>;<Mutex Argument 1>[,<Mutex Argument 2>,...]
+    MUTUALLY_EXCLUSIVE_ARGS=()
+
+    # Alternate names of arguments.
+    # These are listed here as arguments concatenated together with delimiters.
+    # <Main argument>;<Synonym Argument 1>[,<Synonym Argument 2>,...]
+    SYNONYMOUS_ARGS=()
+
+    # The minimum number of times an argument can occur.
+    MIN_OCCURRENCES=()
+
+    # The maximum number of times an argument can occur.
+    MAX_OCCURRENCES=()
+
+    # Set up locals for this function:
+    local toolName=$( _bashTabCompletionDocletTestLaunchWithDefaults_getToolName )
+
+    # Get possible tool matches:
+    local possibleToolMatches=$( _bashTabCompletionDocletTestLaunchWithDefaults_getAllPossibleToolNames )
+
+    # Check if we have postfix options
+    # and if we now need to go through them:
+    if $HAS_POSTFIX_OPTIONS && [[ "${COMP_WORDS[@]}" == *" -- "* ]] ; then
+        NUM_POSITIONAL_ARGUMENTS=0
+        POSITIONAL_ARGUMENT_TYPE=()
+        DEPENDENT_ARGUMENTS=()
+        NORMAL_COMPLETION_ARGUMENTS=("${CS_POSTFIX_OPTIONS_NORMAL_COMPLETION_ARGUMENTS[@]}")
+        MUTUALLY_EXCLUSIVE_ARGS=("${CS_POSTFIX_OPTIONS_MUTUALLY_EXCLUSIVE_ARGS[@]}")
+        SYNONYMOUS_ARGS=("${CS_POSTFIX_OPTIONS_SYNONYMOUS_ARGS[@]}")
+        MIN_OCCURRENCES=("${CS_POSTFIX_OPTIONS_MIN_OCCURRENCES[@]}")
+        MAX_OCCURRENCES=("${CS_POSTFIX_OPTIONS_MAX_OCCURRENCES[@]}")
+        ALL_LEGAL_ARGUMENTS=("${CS_POSTFIX_OPTIONS_ALL_LEGAL_ARGUMENTS[@]}")
+        ALL_ARGUMENT_VALUE_TYPES=("${CS_POSTFIX_OPTIONS_ALL_ARGUMENT_VALUE_TYPES[@]}")
+
+        # Complete the arguments for the base script:
+        # Strictly speaking, what the argument to this function is doesn't matter.
+        _bashTabCompletionDocletTestLaunchWithDefaults_handleArgs POSTFIX_OPTIONS
+
+    # Check if we have a complete tool match that may match more than one tool:
+    elif [[ ${#possibleToolMatches} -ne 0 ]] ; then
+
+        # Set our reply as a list of the possible tool matches:
+        COMPREPLY=( $(compgen -W '${possibleToolMatches[@]}' -- $cur) )
+
+    elif [[ ${toolName} == "TestExtraDocs" ]] ; then
+
+        # Set up the completion information for this tool:
+        DEPENDENT_ARGUMENTS=()
+        NORMAL_COMPLETION_ARGUMENTS=()
+        MUTUALLY_EXCLUSIVE_ARGS=()
+        SYNONYMOUS_ARGS=()
+        MIN_OCCURRENCES=()
+        MAX_OCCURRENCES=()
+        ALL_LEGAL_ARGUMENTS=()
+        ALL_ARGUMENT_VALUE_TYPES=()
+
+        # Complete the arguments for this tool:
+        _bashTabCompletionDocletTestLaunchWithDefaults_handleArgs
+    elif [[ ${toolName} == "TestArgumentContainer" ]] ; then
+
+        # Set up the completion information for this tool:
+        NUM_POSITIONAL_ARGUMENTS=2
+        POSITIONAL_ARGUMENT_TYPE=("List[File]")
+        DEPENDENT_ARGUMENTS=()
+        NORMAL_COMPLETION_ARGUMENTS=(--requiredClpEnum --requiredFileList --requiredInputFilesFromArgCollection --requiredStringInputFromArgCollection --requiredStringList --usesFieldNameForArgName --mutexArg --mutexTargetField1 --mutexTargetField2 --optionalClpEnum --optionalDouble --optionalDoubleList --optionalFileList --optionalFlag --optionalInputFilesFromArgCollection --optionalStringInputFromArgCollection --optionalStringList --testPlugin --advancedOptionalInt --deprecatedString )
+        MUTUALLY_EXCLUSIVE_ARGS=("--mutexArg;READ1_ALIGNED_BAM,READ2_ALIGNED_BAM" "--mutexTargetField1;mutexSourceField" "--mutexTargetField2;mutexSourceField" )
+        SYNONYMOUS_ARGS=("--requiredClpEnum;-requiredClpEnum" "--requiredFileList;-reqFilList" "--requiredInputFilesFromArgCollection;-rRequiredInputFilesFromArgCollection" "--requiredStringInputFromArgCollection;-requiredStringInputFromArgCollection" "--requiredStringList;-reqStrList" "--mutexArg;-mutexArg" "--mutexTargetField1;-mutexTargetField1" "--mutexTargetField2;-mutexTargetField2" "--optionalClpEnum;-optionalClpEnum" "--optionalDouble;-optDouble" "--optionalDoubleList;-optDoubleL [...]
+        MIN_OCCURRENCES=(0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 )
+        MAX_OCCURRENCES=(2147483647 2147483647 2147483647 2147483647 2147483647 2147483647 2147483647 2147483647 2147483647 2147483647 2147483647 2147483647 2147483647 2147483647 2147483647 2147483647 2147483647 2147483647 2147483647 2147483647 )
+        ALL_LEGAL_ARGUMENTS=(--requiredClpEnum --requiredFileList --requiredInputFilesFromArgCollection --requiredStringInputFromArgCollection --requiredStringList --usesFieldNameForArgName --mutexArg --mutexTargetField1 --mutexTargetField2 --optionalClpEnum --optionalDouble --optionalDoubleList --optionalFileList --optionalFlag --optionalInputFilesFromArgCollection --optionalStringInputFromArgCollection --optionalStringList --testPlugin --advancedOptionalInt --deprecatedString )
+        ALL_ARGUMENT_VALUE_TYPES=("TestEnum" "List[File]" "List[File]" "String" "List[String]" "String" "List[File]" "List[File]" "List[File]" "TestEnum" "double" "List[Double]" "List[File]" "boolean" "List[File]" "String" "List[String]" "List[String]" "int" "int" )
+
+        # Complete the arguments for this tool:
+        _bashTabCompletionDocletTestLaunchWithDefaults_handleArgs
+
+    # We have no postfix options or tool options.
+    # We now must complete any prefix options and the tools themselves.
+    # These are defined at the top.
+    else
+        NUM_POSITIONAL_ARGUMENTS=0
+        POSITIONAL_ARGUMENT_TYPE=()
+        DEPENDENT_ARGUMENTS=()
+        NORMAL_COMPLETION_ARGUMENTS=("${CS_PREFIX_OPTIONS_NORMAL_COMPLETION_ARGUMENTS[@]}")
+        MUTUALLY_EXCLUSIVE_ARGS=("${CS_PREFIX_OPTIONS_MUTUALLY_EXCLUSIVE_ARGS[@]}")
+        SYNONYMOUS_ARGS=("${CS_PREFIX_OPTIONS_SYNONYMOUS_ARGS[@]}")
+        MIN_OCCURRENCES=("${CS_PREFIX_OPTIONS_MIN_OCCURRENCES[@]}")
+        MAX_OCCURRENCES=("${CS_PREFIX_OPTIONS_MAX_OCCURRENCES[@]}")
+        ALL_LEGAL_ARGUMENTS=("${CS_PREFIX_OPTIONS_ALL_LEGAL_ARGUMENTS[@]}")
+        ALL_ARGUMENT_VALUE_TYPES=("${CS_PREFIX_OPTIONS_ALL_ARGUMENT_VALUE_TYPES[@]}")
+
+        # Complete the arguments for the prefix arguments and tools:
+        _bashTabCompletionDocletTestLaunchWithDefaults_handleArgs PREFIX_OPTIONS
+    fi
+}
+
+complete -o default -F _bashTabCompletionDocletTestLaunchWithDefaults_masterCompletionFunction ${CALLER_SCRIPT_NAME}
+
+
+
diff --git a/src/test/resources/org/broadinstitute/barclay/help/expected/HelpDoclet/index.html b/src/test/resources/org/broadinstitute/barclay/help/expected/HelpDoclet/index.html
new file mode 100644
index 0000000..2db21b7
--- /dev/null
+++ b/src/test/resources/org/broadinstitute/barclay/help/expected/HelpDoclet/index.html
@@ -0,0 +1,76 @@
+<?php
+
+    include '../../../common/include/common.php';
+    include_once '../../config.php';
+    printHeader($module, "Tool Documentation Index", "Guide");
+?>
+
+<div class='row-fluid'>
+
+<div class='span9'>
+
+
+
+<h1 id="top">Tool Documentation Index
+    <small>11.1</small>
+</h1>
+<div class="accordion" id="index">
+    <div class="accordion-group">
+        <div class="accordion-heading">
+            <a class="accordion-toggle" data-toggle="collapse" data-parent="#index" href="#Testextradocsgroupname">
+                <h4>Test extra docs group name</h4>
+            </a>
+        </div>
+        <div class="accordion-body collapse" id="Testextradocsgroupname">
+            <div class="accordion-inner">
+                <p class="lead"></p>
+                <table class="table table-striped table-bordered table-condensed">
+                    <tr>
+                        <th>Name</th>
+                        <th>Summary</th>
+                    </tr>
+                            <tr>
+                                    <td><a href="org_broadinstitute_barclay_help_TestExtraDocs.html">TestExtraDocs **BETA**</a></td>
+                                <td>Class for testing extraDocs property in docgen.</td>
+                            </tr>
+                </table>
+            </div>
+        </div>
+    </div>
+    <div class="accordion-group">
+        <div class="accordion-heading">
+            <a class="accordion-toggle" data-toggle="collapse" data-parent="#index" href="#Testfeaturegroupname">
+                <h4>Test feature group name</h4>
+            </a>
+        </div>
+        <div class="accordion-body collapse" id="Testfeaturegroupname">
+            <div class="accordion-inner">
+                <p class="lead">Test program group used for testing</p>
+                <table class="table table-striped table-bordered table-condensed">
+                    <tr>
+                        <th>Name</th>
+                        <th>Summary</th>
+                    </tr>
+                            <tr>
+                                    <td><a href="org_broadinstitute_barclay_help_TestArgumentContainer.html">TestArgumentContainer **BETA**</a></td>
+                                <td>Argument container class for testing documentation generation.</td>
+                            </tr>
+                </table>
+            </div>
+        </div>
+    </div>
+</div>
+
+        <hr>
+        <p><a href='#top'><i class='fa fa-chevron-up'></i> Return to top</a></p>
+        <hr>
+        <p class="version">Barclay version 11.1 built at 2016/01/01 01:01:01.
+        </p>
+
+</div></div>
+
+<?php
+
+    printFooter($module);
+
+?>
\ No newline at end of file
diff --git a/src/test/resources/org/broadinstitute/barclay/help/expected/HelpDoclet/org_broadinstitute_barclay_help_TestArgumentContainer.html b/src/test/resources/org/broadinstitute/barclay/help/expected/HelpDoclet/org_broadinstitute_barclay_help_TestArgumentContainer.html
new file mode 100644
index 0000000..3acd11e
--- /dev/null
+++ b/src/test/resources/org/broadinstitute/barclay/help/expected/HelpDoclet/org_broadinstitute_barclay_help_TestArgumentContainer.html
@@ -0,0 +1,571 @@
+<?php
+    include '../../../common/include/common.php';
+?>
+
+<div class='row-fluid' id="top">
+
+	
+
+	<?php $group = 'Test feature group name'; ?>
+
+	<section class="span4">
+		<aside class="well">
+			<a href="index"><h4><i class='fa fa-chevron-left'></i> Back to Tool Docs Index</h4></a>
+		</aside>
+		<aside class="well">
+			<h2>Categories</h2>
+        <style>
+            #sidenav .accordion-body a {
+                color : gray;
+            }
+
+            .accordion-body li {
+                list-style : none;
+            }
+        </style>
+        <ul class="nav nav-pills nav-stacked" id="sidenav">
+				<li><a data-toggle="collapse" data-parent="#sidenav" href="#Testextradocsgroupname">Test extra docs group name</a>
+					<div id="Testextradocsgroupname"
+					<?php echo ($group == 'Test extra docs group name')? 'class="accordion-body collapse in"'.chr(62) : 'class="accordion-body collapse"'.chr(62);?>
+					<ul>
+								<li>
+									<a href="org_broadinstitute_barclay_help_TestExtraDocs.html">TestExtraDocs</a>
+								</li>
+					</ul>
+					</div>
+				</li>
+				<li><a data-toggle="collapse" data-parent="#sidenav" href="#Testfeaturegroupname">Test feature group name</a>
+					<div id="Testfeaturegroupname"
+					<?php echo ($group == 'Test feature group name')? 'class="accordion-body collapse in"'.chr(62) : 'class="accordion-body collapse"'.chr(62);?>
+					<ul>
+								<li>
+									<a href="org_broadinstitute_barclay_help_TestArgumentContainer.html">TestArgumentContainer</a>
+								</li>
+					</ul>
+					</div>
+				</li>
+        </ul>
+		</aside>
+		<?php getForumPosts( 'TestArgumentContainer' ) ?>
+
+	</section>
+
+	<div class="span8">
+
+			<h1>TestArgumentContainer **BETA**</h1>
+
+		<p class="lead">Argument container class for testing documentation generation.</p>
+
+			<h3>Category
+				<small> Test feature group name</small>
+			</h3>
+		<hr>
+		<h2>Overview</h2>
+		Argument container class for testing documentation generation. Contains an argument
+ for each @Argument, @ArgumentCollection, and @DocumentedFeature property that should
+ be tested.
+
+ Test custom tag:
+ testType
+
+ <p>
+ The purpose of this paragraph is to test embedded html formatting.
+ <ol>
+     <li>This is point number 1</li>
+     <li>This is point number 2</li>
+ </ol>
+ </p>
+
+				<hr>
+				<h2>Command-line Arguments</h2>
+				<p></p>
+				<h3>Additional References</h3>
+				<p>See these additional references for more information.</p>
+				<ul>
+						<li><a href="org_broadinstitute_barclay_help_TestExtraDocs.html">TestExtraDocs</a></li>
+				</ul>
+
+				<h3>TestArgumentContainer specific arguments</h3>
+				<p>This table summarizes the command-line arguments that are specific to this tool. For more details on each argument, see the list further down below the table or click on an argument name to jump directly to that entry in the list.</p>
+				<table class="table table-striped table-bordered table-condensed">
+					<thead>
+					<tr>
+						<th>Argument name(s)</th>
+						<th>Default value</th>
+						<th>Summary</th>
+					</tr>
+					</thead>
+					<tbody>
+			<tr>
+				<th colspan="4" id="row-divider">Positional Arguments</th>
+			</tr>
+				<tr>
+					<td><a href="#[NA - Positional]">[NA - Positional]</a><br />
+					</td>
+					<!--<td>List[File]</td> -->
+					<td>NA</td>
+					<td>Positional arguments, min = 2, max = 2</td>
+				</tr>
+			<tr>
+				<th colspan="4" id="row-divider">Required Arguments</th>
+			</tr>
+				<tr>
+					<td><a href="#--requiredClpEnum">--requiredClpEnum</a><br />
+					</td>
+					<!--<td>TestEnum</td> -->
+					<td>null</td>
+					<td>Required Clp enum</td>
+				</tr>
+				<tr>
+					<td><a href="#--requiredFileList">--requiredFileList</a><br />
+								 <em>-reqFilList</em>
+					</td>
+					<!--<td>List[File]</td> -->
+					<td>[]</td>
+					<td>Required file list</td>
+				</tr>
+				<tr>
+					<td><a href="#--requiredInputFilesFromArgCollection">--requiredInputFilesFromArgCollection</a><br />
+								 <em>-rRequiredInputFilesFromArgCollection</em>
+					</td>
+					<!--<td>List[File]</td> -->
+					<td>[]</td>
+					<td>Required input files from argument collection</td>
+				</tr>
+				<tr>
+					<td><a href="#--requiredStringInputFromArgCollection">--requiredStringInputFromArgCollection</a><br />
+					</td>
+					<!--<td>String</td> -->
+					<td>null</td>
+					<td>Required string input from argument collection</td>
+				</tr>
+				<tr>
+					<td><a href="#--requiredStringList">--requiredStringList</a><br />
+								 <em>-reqStrList</em>
+					</td>
+					<!--<td>List[String]</td> -->
+					<td>[]</td>
+					<td>A required list of strings</td>
+				</tr>
+				<tr>
+					<td><a href="#--usesFieldNameForArgName">--usesFieldNameForArgName</a><br />
+					</td>
+					<!--<td>String</td> -->
+					<td>null</td>
+					<td>Use field name if no name in annotation.</td>
+				</tr>
+			<tr>
+				<th colspan="4" id="row-divider">Optional Tool Arguments</th>
+			</tr>
+				<tr>
+					<td><a href="#--mutexArg">--mutexArg</a><br />
+					</td>
+					<!--<td>List[File]</td> -->
+					<td>[]</td>
+					<td>Undocumented option</td>
+				</tr>
+				<tr>
+					<td><a href="#--mutexTargetField1">--mutexTargetField1</a><br />
+					</td>
+					<!--<td>List[File]</td> -->
+					<td>[]</td>
+					<td>SAM/BAM/CRAM file(s) with alignment data from the first read of a pair.</td>
+				</tr>
+				<tr>
+					<td><a href="#--mutexTargetField2">--mutexTargetField2</a><br />
+					</td>
+					<!--<td>List[File]</td> -->
+					<td>[]</td>
+					<td>SAM/BAM file(s) with alignment data from the second read of a pair.</td>
+				</tr>
+				<tr>
+					<td><a href="#--optionalClpEnum">--optionalClpEnum</a><br />
+					</td>
+					<!--<td>TestEnum</td> -->
+					<td>ENUM_VALUE_1</td>
+					<td>Optional Clp enum</td>
+				</tr>
+				<tr>
+					<td><a href="#--optionalDouble">--optionalDouble</a><br />
+								 <em>-optDouble</em>
+					</td>
+					<!--<td>double</td> -->
+					<td>2.15</td>
+					<td>Optionals double with initial value 2.15</td>
+				</tr>
+				<tr>
+					<td><a href="#--optionalDoubleList">--optionalDoubleList</a><br />
+								 <em>-optDoubleList</em>
+					</td>
+					<!--<td>List[Double]</td> -->
+					<td>[100.0, 99.9, 99.0, 90.0]</td>
+					<td>optionalDoubleList with initial values: 100.0, 99.9, 99.0, 90.0</td>
+				</tr>
+				<tr>
+					<td><a href="#--optionalFileList">--optionalFileList</a><br />
+								 <em>-optFilList</em>
+					</td>
+					<!--<td>List[File]</td> -->
+					<td>[]</td>
+					<td>Optional file list</td>
+				</tr>
+				<tr>
+					<td><a href="#--optionalFlag">--optionalFlag</a><br />
+								 <em>-optFlag</em>
+					</td>
+					<!--<td>boolean</td> -->
+					<td>false</td>
+					<td>Optional flag, defaults to false.</td>
+				</tr>
+				<tr>
+					<td><a href="#--optionalInputFilesFromArgCollection">--optionalInputFilesFromArgCollection</a><br />
+					</td>
+					<!--<td>List[File]</td> -->
+					<td>[]</td>
+					<td>Optional input files from argument collection</td>
+				</tr>
+				<tr>
+					<td><a href="#--optionalStringInputFromArgCollection">--optionalStringInputFromArgCollection</a><br />
+					</td>
+					<!--<td>String</td> -->
+					<td>null</td>
+					<td>Optional string input from argument collection</td>
+				</tr>
+				<tr>
+					<td><a href="#--optionalStringList">--optionalStringList</a><br />
+								 <em>-optStrList</em>
+					</td>
+					<!--<td>List[String]</td> -->
+					<td>[]</td>
+					<td>An optional list of strings</td>
+				</tr>
+				<tr>
+					<td><a href="#--testPlugin">--testPlugin</a><br />
+					</td>
+					<!--<td>List[String]</td> -->
+					<td>[]</td>
+					<td>Undocumented option</td>
+				</tr>
+			<tr>
+				<th colspan="4" id="row-divider">Advanced Arguments</th>
+			</tr>
+				<tr>
+					<td><a href="#--advancedOptionalInt">--advancedOptionalInt</a><br />
+								 <em>-advancedOptInt</em>
+					</td>
+					<!--<td>int</td> -->
+					<td>1</td>
+					<td>advancedOptionalInt with initial value 1</td>
+				</tr>
+			<tr>
+				<th colspan="4" id="row-divider">Deprecated Arguments</th>
+			</tr>
+				<tr>
+					<td><a href="#--deprecatedString">--deprecatedString</a><br />
+								 <em>-depStr</em>
+					</td>
+					<!--<td>int</td> -->
+					<td>0</td>
+					<td>deprecated</td>
+				</tr>
+					</tbody>
+				</table>
+
+					<h3>Argument details</h3>
+					<p>Arguments in this list are specific to this tool. Keep in mind that other arguments are available that are shared with other tools (e.g. command-line GATK arguments); see Inherited arguments above.</p>
+		<hr style="border-bottom: dotted 1px #C0C0C0;" />
+		<h3><a name="[NA - Positional]">[NA - Positional] </a>
+			
+		</h3>
+		<p class="args">
+			<b>Positional arguments, min = 2, max = 2</b><br />
+			Positional arguments, min = 2, max = 2
+		</p>
+		<p>
+				<span class="badge badge-important">R</span>
+			<span class="label label-info ">List[File]</span>
+				 <span class="label">NA</span>
+		</p>
+		<hr style="border-bottom: dotted 1px #C0C0C0;" />
+		<h3><a name="--advancedOptionalInt">--advancedOptionalInt </a>
+			 / <small>-advancedOptInt</small>
+		</h3>
+		<p class="args">
+			<b>advancedOptionalInt with initial value 1</b><br />
+			Advanced, Optional int
+		</p>
+		<p>
+			<span class="label label-info ">int</span>
+				 <span class="label">1</span>
+				 <span class="label label-warning">[ [ -∞</span>
+				 <span class="label label-warning">∞ ] ]</span>
+		</p>
+		<hr style="border-bottom: dotted 1px #C0C0C0;" />
+		<h3><a name="--deprecatedString">--deprecatedString </a>
+			 / <small>-depStr</small>
+		</h3>
+		<p class="args">
+			<b>deprecated</b><br />
+			Deprecated string
+		</p>
+		<p>
+			<span class="label label-info ">int</span>
+				 <span class="label">0</span>
+				 <span class="label label-warning">[ [ -∞</span>
+				 <span class="label label-warning">∞ ] ]</span>
+		</p>
+		<hr style="border-bottom: dotted 1px #C0C0C0;" />
+		<h3><a name="--mutexArg">--mutexArg </a>
+			 / <small>-mutexArg</small>
+		</h3>
+		<p class="args">
+			<b>Undocumented option</b><br />
+			Mutually exclusive args
+		</p>
+			<p><b>Exclusion:</b> This argument cannot be used at the same time as <code>READ1_ALIGNED_BAM, READ2_ALIGNED_BAM</code>.</p>
+		<p>
+			<span class="label label-info ">List[File]</span>
+				 <span class="label">[]</span>
+		</p>
+		<hr style="border-bottom: dotted 1px #C0C0C0;" />
+		<h3><a name="--mutexTargetField1">--mutexTargetField1 </a>
+			 / <small>-mutexTargetField1</small>
+		</h3>
+		<p class="args">
+			<b>SAM/BAM/CRAM file(s) with alignment data from the first read of a pair.</b><br />
+			
+		</p>
+			<p><b>Exclusion:</b> This argument cannot be used at the same time as <code>mutexSourceField</code>.</p>
+		<p>
+			<span class="label label-info ">List[File]</span>
+				 <span class="label">[]</span>
+		</p>
+		<hr style="border-bottom: dotted 1px #C0C0C0;" />
+		<h3><a name="--mutexTargetField2">--mutexTargetField2 </a>
+			 / <small>-mutexTargetField2</small>
+		</h3>
+		<p class="args">
+			<b>SAM/BAM file(s) with alignment data from the second read of a pair.</b><br />
+			
+		</p>
+			<p><b>Exclusion:</b> This argument cannot be used at the same time as <code>mutexSourceField</code>.</p>
+		<p>
+			<span class="label label-info ">List[File]</span>
+				 <span class="label">[]</span>
+		</p>
+		<hr style="border-bottom: dotted 1px #C0C0C0;" />
+		<h3><a name="--optionalClpEnum">--optionalClpEnum </a>
+			 / <small>-optionalClpEnum</small>
+		</h3>
+		<p class="args">
+			<b>Optional Clp enum</b><br />
+			
+		</p>
+			<p>
+				The --optionalClpEnum argument is an enumerated type (TestEnum), which can have one of the following values:
+			<dl class="enum">
+					<dt class="enum">ENUM_VALUE_1</dt>
+					<dd class="enum"></dd>
+					<dt class="enum">ENUM_VALUE_2</dt>
+					<dd class="enum"></dd>
+			</dl>
+			</p>
+		<p>
+			<span class="label label-info ">TestEnum</span>
+				 <span class="label">ENUM_VALUE_1</span>
+		</p>
+		<hr style="border-bottom: dotted 1px #C0C0C0;" />
+		<h3><a name="--optionalDouble">--optionalDouble </a>
+			 / <small>-optDouble</small>
+		</h3>
+		<p class="args">
+			<b>Optionals double with initial value 2.15</b><br />
+			Required double
+		</p>
+		<p>
+			<span class="label label-info ">double</span>
+				 <span class="label">2.15</span>
+				 <span class="label label-warning">[ [ -∞</span>
+				 <span class="label label-warning">∞ ] ]</span>
+		</p>
+		<hr style="border-bottom: dotted 1px #C0C0C0;" />
+		<h3><a name="--optionalDoubleList">--optionalDoubleList </a>
+			 / <small>-optDoubleList</small>
+		</h3>
+		<p class="args">
+			<b>optionalDoubleList with initial values: 100.0, 99.9, 99.0, 90.0</b><br />
+			Optional double list.
+		</p>
+		<p>
+			<span class="label label-info ">List[Double]</span>
+				 <span class="label">[100.0, 99.9, 99.0, 90.0]</span>
+		</p>
+		<hr style="border-bottom: dotted 1px #C0C0C0;" />
+		<h3><a name="--optionalFileList">--optionalFileList </a>
+			 / <small>-optFilList</small>
+		</h3>
+		<p class="args">
+			<b>Optional file list</b><br />
+			Optional file list.
+		</p>
+		<p>
+			<span class="label label-info ">List[File]</span>
+				 <span class="label">[]</span>
+		</p>
+		<hr style="border-bottom: dotted 1px #C0C0C0;" />
+		<h3><a name="--optionalFlag">--optionalFlag </a>
+			 / <small>-optFlag</small>
+		</h3>
+		<p class="args">
+			<b>Optional flag, defaults to false.</b><br />
+			Optional flag.
+		</p>
+		<p>
+			<span class="label label-info ">boolean</span>
+				 <span class="label">false</span>
+		</p>
+		<hr style="border-bottom: dotted 1px #C0C0C0;" />
+		<h3><a name="--optionalInputFilesFromArgCollection">--optionalInputFilesFromArgCollection </a>
+			 / <small>-optionalInputFilesFromArgCollection</small>
+		</h3>
+		<p class="args">
+			<b>Optional input files from argument collection</b><br />
+			
+		</p>
+		<p>
+			<span class="label label-info ">List[File]</span>
+				 <span class="label">[]</span>
+		</p>
+		<hr style="border-bottom: dotted 1px #C0C0C0;" />
+		<h3><a name="--optionalStringInputFromArgCollection">--optionalStringInputFromArgCollection </a>
+			 / <small>-optionalStringInputFromArgCollection</small>
+		</h3>
+		<p class="args">
+			<b>Optional string input from argument collection</b><br />
+			
+		</p>
+		<p>
+			<span class="label label-info ">String</span>
+				 <span class="label">null</span>
+		</p>
+		<hr style="border-bottom: dotted 1px #C0C0C0;" />
+		<h3><a name="--optionalStringList">--optionalStringList </a>
+			 / <small>-optStrList</small>
+		</h3>
+		<p class="args">
+			<b>An optional list of strings</b><br />
+			Optional string list.
+		</p>
+		<p>
+			<span class="label label-info ">List[String]</span>
+				 <span class="label">[]</span>
+		</p>
+		<hr style="border-bottom: dotted 1px #C0C0C0;" />
+		<h3><a name="--requiredClpEnum">--requiredClpEnum </a>
+			 / <small>-requiredClpEnum</small>
+		</h3>
+		<p class="args">
+			<b>Required Clp enum</b><br />
+			
+		</p>
+			<p>
+				The --requiredClpEnum argument is an enumerated type (TestEnum), which can have one of the following values:
+			<dl class="enum">
+					<dt class="enum">ENUM_VALUE_1</dt>
+					<dd class="enum"></dd>
+					<dt class="enum">ENUM_VALUE_2</dt>
+					<dd class="enum"></dd>
+			</dl>
+			</p>
+		<p>
+				<span class="badge badge-important">R</span>
+			<span class="label label-info ">TestEnum</span>
+				 <span class="label">null</span>
+		</p>
+		<hr style="border-bottom: dotted 1px #C0C0C0;" />
+		<h3><a name="--requiredFileList">--requiredFileList </a>
+			 / <small>-reqFilList</small>
+		</h3>
+		<p class="args">
+			<b>Required file list</b><br />
+			Required file list.
+		</p>
+		<p>
+				<span class="badge badge-important">R</span>
+			<span class="label label-info ">List[File]</span>
+				 <span class="label">[]</span>
+		</p>
+		<hr style="border-bottom: dotted 1px #C0C0C0;" />
+		<h3><a name="--requiredInputFilesFromArgCollection">--requiredInputFilesFromArgCollection </a>
+			 / <small>-rRequiredInputFilesFromArgCollection</small>
+		</h3>
+		<p class="args">
+			<b>Required input files from argument collection</b><br />
+			
+		</p>
+		<p>
+				<span class="badge badge-important">R</span>
+			<span class="label label-info ">List[File]</span>
+				 <span class="label">[]</span>
+		</p>
+		<hr style="border-bottom: dotted 1px #C0C0C0;" />
+		<h3><a name="--requiredStringInputFromArgCollection">--requiredStringInputFromArgCollection </a>
+			 / <small>-requiredStringInputFromArgCollection</small>
+		</h3>
+		<p class="args">
+			<b>Required string input from argument collection</b><br />
+			
+		</p>
+		<p>
+				<span class="badge badge-important">R</span>
+			<span class="label label-info ">String</span>
+				 <span class="label">null</span>
+		</p>
+		<hr style="border-bottom: dotted 1px #C0C0C0;" />
+		<h3><a name="--requiredStringList">--requiredStringList </a>
+			 / <small>-reqStrList</small>
+		</h3>
+		<p class="args">
+			<b>A required list of strings</b><br />
+			Required string list.
+		</p>
+		<p>
+				<span class="badge badge-important">R</span>
+			<span class="label label-info ">List[String]</span>
+				 <span class="label">[]</span>
+		</p>
+		<hr style="border-bottom: dotted 1px #C0C0C0;" />
+		<h3><a name="--testPlugin">--testPlugin </a>
+			
+		</h3>
+		<p class="args">
+			<b>Undocumented option</b><br />
+			
+		</p>
+		<p>
+			<span class="label label-info ">List[String]</span>
+				 <span class="label">[]</span>
+		</p>
+		<hr style="border-bottom: dotted 1px #C0C0C0;" />
+		<h3><a name="--usesFieldNameForArgName">--usesFieldNameForArgName </a>
+			
+		</h3>
+		<p class="args">
+			<b>Use field name if no name in annotation.</b><br />
+			
+		</p>
+		<p>
+				<span class="badge badge-important">R</span>
+			<span class="label label-info ">String</span>
+				 <span class="label">null</span>
+		</p>
+
+        <hr>
+        <p><a href='#top'><i class='fa fa-chevron-up'></i> Return to top</a></p>
+        <hr>
+        <p class="version">Barclay version 11.1 built at 2016/01/01 01:01:01.
+        </p>
+
+	</div>
+
+	<?php printFooter($module); ?>
\ No newline at end of file
diff --git a/src/test/resources/org/broadinstitute/barclay/help/expected/HelpDoclet/org_broadinstitute_barclay_help_TestArgumentContainer.json b/src/test/resources/org/broadinstitute/barclay/help/expected/HelpDoclet/org_broadinstitute_barclay_help_TestArgumentContainer.json
new file mode 100644
index 0000000..a0fbaf8
--- /dev/null
+++ b/src/test/resources/org/broadinstitute/barclay/help/expected/HelpDoclet/org_broadinstitute_barclay_help_TestArgumentContainer.json
@@ -0,0 +1,341 @@
+{
+  "summary": "Argument container class for testing documentation generation.",
+  "arguments": [
+    {
+      "summary": "Positional arguments, min \u003d 2, max \u003d 2",
+      "name": "[NA - Positional]",
+      "synonyms": "NA",
+      "type": "List[File]",
+      "required": "yes",
+      "fulltext": "Positional arguments, min \u003d 2, max \u003d 2",
+      "defaultValue": "NA",
+      "minValue": "NA",
+      "maxValue": "NA",
+      "minRecValue": "NA",
+      "maxRecValue": "NA",
+      "kind": "positional",
+      "options": []
+    },
+    {
+      "summary": "advancedOptionalInt with initial value 1",
+      "name": "--advancedOptionalInt",
+      "synonyms": "-advancedOptInt",
+      "type": "int",
+      "required": "no",
+      "fulltext": "Advanced, Optional int",
+      "defaultValue": "1",
+      "minValue": "-Infinity",
+      "maxValue": "Infinity",
+      "minRecValue": "NA",
+      "maxRecValue": "NA",
+      "kind": "advanced",
+      "options": []
+    },
+    {
+      "summary": "deprecated",
+      "name": "--deprecatedString",
+      "synonyms": "-depStr",
+      "type": "int",
+      "required": "no",
+      "fulltext": "Deprecated string",
+      "defaultValue": "0",
+      "minValue": "-Infinity",
+      "maxValue": "Infinity",
+      "minRecValue": "NA",
+      "maxRecValue": "NA",
+      "kind": "deprecated",
+      "options": []
+    },
+    {
+      "summary": "Undocumented option",
+      "name": "--mutexArg",
+      "synonyms": "-mutexArg",
+      "type": "List[File]",
+      "required": "no",
+      "fulltext": "Mutually exclusive args",
+      "defaultValue": "[]",
+      "minValue": "NA",
+      "maxValue": "NA",
+      "minRecValue": "NA",
+      "maxRecValue": "NA",
+      "kind": "optional",
+      "options": []
+    },
+    {
+      "summary": "SAM/BAM/CRAM file(s) with alignment data from the first read of a pair.",
+      "name": "--mutexTargetField1",
+      "synonyms": "-mutexTargetField1",
+      "type": "List[File]",
+      "required": "no",
+      "fulltext": "",
+      "defaultValue": "[]",
+      "minValue": "NA",
+      "maxValue": "NA",
+      "minRecValue": "NA",
+      "maxRecValue": "NA",
+      "kind": "optional",
+      "options": []
+    },
+    {
+      "summary": "SAM/BAM file(s) with alignment data from the second read of a pair.",
+      "name": "--mutexTargetField2",
+      "synonyms": "-mutexTargetField2",
+      "type": "List[File]",
+      "required": "no",
+      "fulltext": "",
+      "defaultValue": "[]",
+      "minValue": "NA",
+      "maxValue": "NA",
+      "minRecValue": "NA",
+      "maxRecValue": "NA",
+      "kind": "optional",
+      "options": []
+    },
+    {
+      "summary": "Optional Clp enum",
+      "name": "--optionalClpEnum",
+      "synonyms": "-optionalClpEnum",
+      "type": "TestEnum",
+      "required": "no",
+      "fulltext": "",
+      "defaultValue": "ENUM_VALUE_1",
+      "minValue": "NA",
+      "maxValue": "NA",
+      "minRecValue": "NA",
+      "maxRecValue": "NA",
+      "kind": "optional",
+      "options": [
+        {
+          "summary": "",
+          "name": "ENUM_VALUE_1"
+        },
+        {
+          "summary": "",
+          "name": "ENUM_VALUE_2"
+        }
+      ]
+    },
+    {
+      "summary": "Optionals double with initial value 2.15",
+      "name": "--optionalDouble",
+      "synonyms": "-optDouble",
+      "type": "double",
+      "required": "no",
+      "fulltext": "Required double",
+      "defaultValue": "2.15",
+      "minValue": "-Infinity",
+      "maxValue": "Infinity",
+      "minRecValue": "NA",
+      "maxRecValue": "NA",
+      "kind": "optional",
+      "options": []
+    },
+    {
+      "summary": "optionalDoubleList with initial values: 100.0, 99.9, 99.0, 90.0",
+      "name": "--optionalDoubleList",
+      "synonyms": "-optDoubleList",
+      "type": "List[Double]",
+      "required": "no",
+      "fulltext": "Optional double list.",
+      "defaultValue": "[100.0, 99.9, 99.0, 90.0]",
+      "minValue": "NA",
+      "maxValue": "NA",
+      "minRecValue": "NA",
+      "maxRecValue": "NA",
+      "kind": "optional",
+      "options": []
+    },
+    {
+      "summary": "Optional file list",
+      "name": "--optionalFileList",
+      "synonyms": "-optFilList",
+      "type": "List[File]",
+      "required": "no",
+      "fulltext": "Optional file list.",
+      "defaultValue": "[]",
+      "minValue": "NA",
+      "maxValue": "NA",
+      "minRecValue": "NA",
+      "maxRecValue": "NA",
+      "kind": "optional",
+      "options": []
+    },
+    {
+      "summary": "Optional flag, defaults to false.",
+      "name": "--optionalFlag",
+      "synonyms": "-optFlag",
+      "type": "boolean",
+      "required": "no",
+      "fulltext": "Optional flag.",
+      "defaultValue": "false",
+      "minValue": "NA",
+      "maxValue": "NA",
+      "minRecValue": "NA",
+      "maxRecValue": "NA",
+      "kind": "optional",
+      "options": []
+    },
+    {
+      "summary": "Optional input files from argument collection",
+      "name": "--optionalInputFilesFromArgCollection",
+      "synonyms": "-optionalInputFilesFromArgCollection",
+      "type": "List[File]",
+      "required": "no",
+      "fulltext": "",
+      "defaultValue": "[]",
+      "minValue": "NA",
+      "maxValue": "NA",
+      "minRecValue": "NA",
+      "maxRecValue": "NA",
+      "kind": "optional",
+      "options": []
+    },
+    {
+      "summary": "Optional string input from argument collection",
+      "name": "--optionalStringInputFromArgCollection",
+      "synonyms": "-optionalStringInputFromArgCollection",
+      "type": "String",
+      "required": "no",
+      "fulltext": "",
+      "defaultValue": "null",
+      "minValue": "NA",
+      "maxValue": "NA",
+      "minRecValue": "NA",
+      "maxRecValue": "NA",
+      "kind": "optional",
+      "options": []
+    },
+    {
+      "summary": "An optional list of strings",
+      "name": "--optionalStringList",
+      "synonyms": "-optStrList",
+      "type": "List[String]",
+      "required": "no",
+      "fulltext": "Optional string list.",
+      "defaultValue": "[]",
+      "minValue": "NA",
+      "maxValue": "NA",
+      "minRecValue": "NA",
+      "maxRecValue": "NA",
+      "kind": "optional",
+      "options": []
+    },
+    {
+      "summary": "Required Clp enum",
+      "name": "--requiredClpEnum",
+      "synonyms": "-requiredClpEnum",
+      "type": "TestEnum",
+      "required": "yes",
+      "fulltext": "",
+      "defaultValue": "null",
+      "minValue": "NA",
+      "maxValue": "NA",
+      "minRecValue": "NA",
+      "maxRecValue": "NA",
+      "kind": "required",
+      "options": [
+        {
+          "summary": "",
+          "name": "ENUM_VALUE_1"
+        },
+        {
+          "summary": "",
+          "name": "ENUM_VALUE_2"
+        }
+      ]
+    },
+    {
+      "summary": "Required file list",
+      "name": "--requiredFileList",
+      "synonyms": "-reqFilList",
+      "type": "List[File]",
+      "required": "yes",
+      "fulltext": "Required file list.",
+      "defaultValue": "[]",
+      "minValue": "NA",
+      "maxValue": "NA",
+      "minRecValue": "NA",
+      "maxRecValue": "NA",
+      "kind": "required",
+      "options": []
+    },
+    {
+      "summary": "Required input files from argument collection",
+      "name": "--requiredInputFilesFromArgCollection",
+      "synonyms": "-rRequiredInputFilesFromArgCollection",
+      "type": "List[File]",
+      "required": "yes",
+      "fulltext": "",
+      "defaultValue": "[]",
+      "minValue": "NA",
+      "maxValue": "NA",
+      "minRecValue": "NA",
+      "maxRecValue": "NA",
+      "kind": "required",
+      "options": []
+    },
+    {
+      "summary": "Required string input from argument collection",
+      "name": "--requiredStringInputFromArgCollection",
+      "synonyms": "-requiredStringInputFromArgCollection",
+      "type": "String",
+      "required": "yes",
+      "fulltext": "",
+      "defaultValue": "null",
+      "minValue": "NA",
+      "maxValue": "NA",
+      "minRecValue": "NA",
+      "maxRecValue": "NA",
+      "kind": "required",
+      "options": []
+    },
+    {
+      "summary": "A required list of strings",
+      "name": "--requiredStringList",
+      "synonyms": "-reqStrList",
+      "type": "List[String]",
+      "required": "yes",
+      "fulltext": "Required string list.",
+      "defaultValue": "[]",
+      "minValue": "NA",
+      "maxValue": "NA",
+      "minRecValue": "NA",
+      "maxRecValue": "NA",
+      "kind": "required",
+      "options": []
+    },
+    {
+      "summary": "Undocumented option",
+      "name": "--testPlugin",
+      "synonyms": "NA",
+      "type": "List[String]",
+      "required": "no",
+      "fulltext": "",
+      "defaultValue": "[]",
+      "minValue": "NA",
+      "maxValue": "NA",
+      "minRecValue": "NA",
+      "maxRecValue": "NA",
+      "kind": "optional",
+      "options": []
+    },
+    {
+      "summary": "Use field name if no name in annotation.",
+      "name": "--usesFieldNameForArgName",
+      "synonyms": "NA",
+      "type": "String",
+      "required": "yes",
+      "fulltext": "",
+      "defaultValue": "null",
+      "minValue": "NA",
+      "maxValue": "NA",
+      "minRecValue": "NA",
+      "maxRecValue": "NA",
+      "kind": "required",
+      "options": []
+    }
+  ],
+  "description": "Argument container class for testing documentation generation. Contains an argument\n for each @Argument, @ArgumentCollection, and @DocumentedFeature property that should\n be tested.\n\n Test custom tag:\n testType\n\n \u003cp\u003e\n The purpose of this paragraph is to test embedded html formatting.\n \u003col\u003e\n     \u003cli\u003eThis is point number 1\u003c/li\u003e\n     \u003cli\u003eThis is point number 2\u003c/li\u003e\n \u003c/ol\u003e\n \u003c/p\u003e",
+  "name": "TestArgumentContainer",
+  "group": "Test feature group name"
+}
\ No newline at end of file
diff --git a/src/test/resources/org/broadinstitute/barclay/help/expected/HelpDoclet/org_broadinstitute_barclay_help_TestExtraDocs.html b/src/test/resources/org/broadinstitute/barclay/help/expected/HelpDoclet/org_broadinstitute_barclay_help_TestExtraDocs.html
new file mode 100644
index 0000000..ac527a4
--- /dev/null
+++ b/src/test/resources/org/broadinstitute/barclay/help/expected/HelpDoclet/org_broadinstitute_barclay_help_TestExtraDocs.html
@@ -0,0 +1,77 @@
+<?php
+    include '../../../common/include/common.php';
+?>
+
+<div class='row-fluid' id="top">
+
+	
+
+	<?php $group = 'Test extra docs group name'; ?>
+
+	<section class="span4">
+		<aside class="well">
+			<a href="index"><h4><i class='fa fa-chevron-left'></i> Back to Tool Docs Index</h4></a>
+		</aside>
+		<aside class="well">
+			<h2>Categories</h2>
+        <style>
+            #sidenav .accordion-body a {
+                color : gray;
+            }
+
+            .accordion-body li {
+                list-style : none;
+            }
+        </style>
+        <ul class="nav nav-pills nav-stacked" id="sidenav">
+				<li><a data-toggle="collapse" data-parent="#sidenav" href="#Testextradocsgroupname">Test extra docs group name</a>
+					<div id="Testextradocsgroupname"
+					<?php echo ($group == 'Test extra docs group name')? 'class="accordion-body collapse in"'.chr(62) : 'class="accordion-body collapse"'.chr(62);?>
+					<ul>
+								<li>
+									<a href="org_broadinstitute_barclay_help_TestExtraDocs.html">TestExtraDocs</a>
+								</li>
+					</ul>
+					</div>
+				</li>
+				<li><a data-toggle="collapse" data-parent="#sidenav" href="#Testfeaturegroupname">Test feature group name</a>
+					<div id="Testfeaturegroupname"
+					<?php echo ($group == 'Test feature group name')? 'class="accordion-body collapse in"'.chr(62) : 'class="accordion-body collapse"'.chr(62);?>
+					<ul>
+								<li>
+									<a href="org_broadinstitute_barclay_help_TestArgumentContainer.html">TestArgumentContainer</a>
+								</li>
+					</ul>
+					</div>
+				</li>
+        </ul>
+		</aside>
+		<?php getForumPosts( 'TestExtraDocs' ) ?>
+
+	</section>
+
+	<div class="span8">
+
+			<h1>TestExtraDocs **BETA**</h1>
+
+		<p class="lead">Class for testing extraDocs property in docgen.</p>
+
+			<h3>Category
+				<small> Test extra docs group name</small>
+			</h3>
+		<hr>
+		<h2>Overview</h2>
+		Class for testing extraDocs property in docgen.
+
+
+
+
+        <hr>
+        <p><a href='#top'><i class='fa fa-chevron-up'></i> Return to top</a></p>
+        <hr>
+        <p class="version">Barclay version 11.1 built at 2016/01/01 01:01:01.
+        </p>
+
+	</div>
+
+	<?php printFooter($module); ?>
\ No newline at end of file
diff --git a/src/test/resources/org/broadinstitute/barclay/help/expected/HelpDoclet/org_broadinstitute_barclay_help_TestExtraDocs.json b/src/test/resources/org/broadinstitute/barclay/help/expected/HelpDoclet/org_broadinstitute_barclay_help_TestExtraDocs.json
new file mode 100644
index 0000000..c6abf60
--- /dev/null
+++ b/src/test/resources/org/broadinstitute/barclay/help/expected/HelpDoclet/org_broadinstitute_barclay_help_TestExtraDocs.json
@@ -0,0 +1,6 @@
+{
+  "summary": "Class for testing extraDocs property in docgen.",
+  "description": "Class for testing extraDocs property in docgen.",
+  "name": "TestExtraDocs",
+  "group": "Test extra docs group name"
+}
\ No newline at end of file
diff --git a/src/test/resources/org/broadinstitute/barclay/help/expected/TestDoclet/index.html b/src/test/resources/org/broadinstitute/barclay/help/expected/TestDoclet/index.html
new file mode 100644
index 0000000..72ba12e
--- /dev/null
+++ b/src/test/resources/org/broadinstitute/barclay/help/expected/TestDoclet/index.html
@@ -0,0 +1,80 @@
+<?php
+
+    include '../../../common/include/common.php';
+    include_once '../../config.php';
+    printHeader($module, "Tool Documentation Index", "Guide");
+?>
+
+<div class='row-fluid'>
+
+<div class='span9'>
+
+
+
+<h1 id="top">Tool Documentation Index
+    <small>11.1</small>
+</h1>
+<div class="accordion" id="index">
+		<br />
+		<br />
+		<br />
+    <div class="accordion-group">
+        <div class="accordion-heading">
+            <a class="accordion-toggle" data-toggle="collapse" data-parent="#index" href="#Testextradocsgroupname">
+                <h4>Test extra docs group name</h4>
+            </a>
+        </div>
+        <div class="accordion-body collapse" id="Testextradocsgroupname">
+            <div class="accordion-inner">
+                <p class="lead"></p>
+                <table class="table table-striped table-bordered table-condensed">
+                    <tr>
+                        <th>Name</th>
+                        <th>Summary</th>
+                    </tr>
+                            <tr>
+                                    <td><a href="org_broadinstitute_barclay_help_TestExtraDocs.html">TestExtraDocs **BETA**</a></td>
+                                <td>Class for testing extraDocs property in docgen.</td>
+                            </tr>
+                </table>
+            </div>
+        </div>
+    </div>
+    <div class="accordion-group">
+        <div class="accordion-heading">
+            <a class="accordion-toggle" data-toggle="collapse" data-parent="#index" href="#Testfeaturegroupname">
+                <h4>Test feature group name</h4>
+            </a>
+        </div>
+        <div class="accordion-body collapse" id="Testfeaturegroupname">
+            <div class="accordion-inner">
+                <p class="lead">Test program group used for testing</p>
+                <table class="table table-striped table-bordered table-condensed">
+                    <tr>
+                        <th>Name</th>
+                        <th>Summary</th>
+                    </tr>
+                            <tr>
+                                    <td><a href="org_broadinstitute_barclay_help_TestArgumentContainer.html">TestArgumentContainer **BETA**</a></td>
+                                <td>Argument container class for testing documentation generation.</td>
+                            </tr>
+                </table>
+            </div>
+        </div>
+    </div>
+		<br />
+</div>
+
+        <hr>
+        <p><a href='#top'><i class='fa fa-chevron-up'></i> Return to top</a></p>
+        <hr>
+        <p class="version">Barclay version 11.1 built at 2016/01/01 01:01:01.
+        </p>
+
+</div></div>
+
+<?php
+
+    printFooter($module);
+
+?>
\ No newline at end of file
diff --git a/src/test/resources/org/broadinstitute/barclay/help/expected/TestDoclet/org_broadinstitute_barclay_help_TestArgumentContainer.html b/src/test/resources/org/broadinstitute/barclay/help/expected/TestDoclet/org_broadinstitute_barclay_help_TestArgumentContainer.html
new file mode 100644
index 0000000..7d576b2
--- /dev/null
+++ b/src/test/resources/org/broadinstitute/barclay/help/expected/TestDoclet/org_broadinstitute_barclay_help_TestArgumentContainer.html
@@ -0,0 +1,583 @@
+<?php
+    include '../../../common/include/common.php';
+?>
+
+<div class='row-fluid' id="top">
+
+	
+
+	<?php $group = 'Test feature group name'; ?>
+
+	<section class="span4">
+		<aside class="well">
+			<a href="index"><h4><i class='fa fa-chevron-left'></i> Back to Tool Docs Index</h4></a>
+		</aside>
+		<aside class="well">
+			<h2>Categories</h2>
+        <style>
+            #sidenav .accordion-body a {
+                color : gray;
+            }
+
+            .accordion-body li {
+                list-style : none;
+            }
+        </style>
+        <ul class="nav nav-pills nav-stacked" id="sidenav">
+        		<hr>
+        		<hr>
+        		<hr>
+						<li><a data-toggle="collapse" data-parent="#sidenav" href="#Testextradocsgroupname">Test extra docs group name</a>
+							<div id="Testextradocsgroupname"
+								<?php echo ($group == 'Test extra docs group name')? 'class="accordion-body collapse in"'.chr(62) : 'class="accordion-body collapse"'.chr(62);?>
+								<ul>
+											<li>
+												<a href="org_broadinstitute_barclay_help_TestExtraDocs.html">TestExtraDocs</a>
+											</li>
+								</ul>
+							</div>
+						</li>
+						<li><a data-toggle="collapse" data-parent="#sidenav" href="#Testfeaturegroupname">Test feature group name</a>
+							<div id="Testfeaturegroupname"
+								<?php echo ($group == 'Test feature group name')? 'class="accordion-body collapse in"'.chr(62) : 'class="accordion-body collapse"'.chr(62);?>
+								<ul>
+											<li>
+												<a href="org_broadinstitute_barclay_help_TestArgumentContainer.html">TestArgumentContainer</a>
+											</li>
+								</ul>
+							</div>
+						</li>
+        		<hr>
+        </ul>
+		</aside>
+		<?php getForumPosts( 'TestArgumentContainer' ) ?>
+
+	</section>
+
+	<div class="span8">
+
+			<h1>TestArgumentContainer **BETA**</h1>
+
+		<p class="lead">Argument container class for testing documentation generation.</p>
+
+			<h3>Category
+				<small> Test feature group name</small>
+			</h3>
+		<hr>
+		<h2>Overview</h2>
+		Argument container class for testing documentation generation. Contains an argument
+ for each @Argument, @ArgumentCollection, and @DocumentedFeature property that should
+ be tested.
+
+ Test custom tag:
+ 
+
+ <p>
+ The purpose of this paragraph is to test embedded html formatting.
+ <ol>
+     <li>This is point number 1</li>
+     <li>This is point number 2</li>
+ </ol>
+ </p>
+
+				<hr>
+				<h2>Additional Information</h2>
+				<p></p>
+				<h3>Test Plugins</h3>
+					<p>This Test Plugin is automatically applied to the data by the Engine before processing by TestArgumentContainer.</p>
+				<ul>
+						<li><a href="org_broadinstitute_barclay_argparser_CommandLinePluginUnitTest$TestDefaultPlugin.html">TestDefaultPlugin</a></li>
+				</ul>
+				<hr>
+				<h2>Command-line Arguments</h2>
+				<p></p>
+				<h3>Additional References</h3>
+				<p>See these additional references for more information.</p>
+				<ul>
+						<li><a href="org_broadinstitute_barclay_help_TestExtraDocs.html">TestExtraDocs</a></li>
+				</ul>
+
+				<h3>TestArgumentContainer specific arguments</h3>
+				<p>This table summarizes the command-line arguments that are specific to this tool. For more details on each argument, see the list further down below the table or click on an argument name to jump directly to that entry in the list.</p>
+				<table class="table table-striped table-bordered table-condensed">
+					<thead>
+					<tr>
+						<th>Argument name(s)</th>
+						<th>Default value</th>
+						<th>Summary</th>
+					</tr>
+					</thead>
+					<tbody>
+			<tr>
+				<th colspan="4" id="row-divider">Positional Arguments</th>
+			</tr>
+				<tr>
+					<td><a href="#[NA - Positional]">[NA - Positional]</a><br />
+					</td>
+					<!--<td>List[File]</td> -->
+					<td>NA</td>
+					<td>Positional arguments, min = 2, max = 2</td>
+				</tr>
+			<tr>
+				<th colspan="4" id="row-divider">Required Arguments</th>
+			</tr>
+				<tr>
+					<td><a href="#--requiredClpEnum">--requiredClpEnum</a><br />
+					</td>
+					<!--<td>TestEnum</td> -->
+					<td>null</td>
+					<td>Required Clp enum</td>
+				</tr>
+				<tr>
+					<td><a href="#--requiredFileList">--requiredFileList</a><br />
+								 <em>-reqFilList</em>
+					</td>
+					<!--<td>List[File]</td> -->
+					<td>[]</td>
+					<td>Required file list</td>
+				</tr>
+				<tr>
+					<td><a href="#--requiredInputFilesFromArgCollection">--requiredInputFilesFromArgCollection</a><br />
+								 <em>-rRequiredInputFilesFromArgCollection</em>
+					</td>
+					<!--<td>List[File]</td> -->
+					<td>[]</td>
+					<td>Required input files from argument collection</td>
+				</tr>
+				<tr>
+					<td><a href="#--requiredStringInputFromArgCollection">--requiredStringInputFromArgCollection</a><br />
+					</td>
+					<!--<td>String</td> -->
+					<td>null</td>
+					<td>Required string input from argument collection</td>
+				</tr>
+				<tr>
+					<td><a href="#--requiredStringList">--requiredStringList</a><br />
+								 <em>-reqStrList</em>
+					</td>
+					<!--<td>List[String]</td> -->
+					<td>[]</td>
+					<td>A required list of strings</td>
+				</tr>
+				<tr>
+					<td><a href="#--usesFieldNameForArgName">--usesFieldNameForArgName</a><br />
+					</td>
+					<!--<td>String</td> -->
+					<td>null</td>
+					<td>Use field name if no name in annotation.</td>
+				</tr>
+			<tr>
+				<th colspan="4" id="row-divider">Optional Tool Arguments</th>
+			</tr>
+				<tr>
+					<td><a href="#--mutexArg">--mutexArg</a><br />
+					</td>
+					<!--<td>List[File]</td> -->
+					<td>[]</td>
+					<td>Undocumented option</td>
+				</tr>
+				<tr>
+					<td><a href="#--mutexTargetField1">--mutexTargetField1</a><br />
+					</td>
+					<!--<td>List[File]</td> -->
+					<td>[]</td>
+					<td>SAM/BAM/CRAM file(s) with alignment data from the first read of a pair.</td>
+				</tr>
+				<tr>
+					<td><a href="#--mutexTargetField2">--mutexTargetField2</a><br />
+					</td>
+					<!--<td>List[File]</td> -->
+					<td>[]</td>
+					<td>SAM/BAM file(s) with alignment data from the second read of a pair.</td>
+				</tr>
+				<tr>
+					<td><a href="#--optionalClpEnum">--optionalClpEnum</a><br />
+					</td>
+					<!--<td>TestEnum</td> -->
+					<td>ENUM_VALUE_1</td>
+					<td>Optional Clp enum</td>
+				</tr>
+				<tr>
+					<td><a href="#--optionalDouble">--optionalDouble</a><br />
+								 <em>-optDouble</em>
+					</td>
+					<!--<td>double</td> -->
+					<td>2.15</td>
+					<td>Optionals double with initial value 2.15</td>
+				</tr>
+				<tr>
+					<td><a href="#--optionalDoubleList">--optionalDoubleList</a><br />
+								 <em>-optDoubleList</em>
+					</td>
+					<!--<td>List[Double]</td> -->
+					<td>[100.0, 99.9, 99.0, 90.0]</td>
+					<td>optionalDoubleList with initial values: 100.0, 99.9, 99.0, 90.0</td>
+				</tr>
+				<tr>
+					<td><a href="#--optionalFileList">--optionalFileList</a><br />
+								 <em>-optFilList</em>
+					</td>
+					<!--<td>List[File]</td> -->
+					<td>[]</td>
+					<td>Optional file list</td>
+				</tr>
+				<tr>
+					<td><a href="#--optionalFlag">--optionalFlag</a><br />
+								 <em>-optFlag</em>
+					</td>
+					<!--<td>boolean</td> -->
+					<td>false</td>
+					<td>Optional flag, defaults to false.</td>
+				</tr>
+				<tr>
+					<td><a href="#--optionalInputFilesFromArgCollection">--optionalInputFilesFromArgCollection</a><br />
+					</td>
+					<!--<td>List[File]</td> -->
+					<td>[]</td>
+					<td>Optional input files from argument collection</td>
+				</tr>
+				<tr>
+					<td><a href="#--optionalStringInputFromArgCollection">--optionalStringInputFromArgCollection</a><br />
+					</td>
+					<!--<td>String</td> -->
+					<td>null</td>
+					<td>Optional string input from argument collection</td>
+				</tr>
+				<tr>
+					<td><a href="#--optionalStringList">--optionalStringList</a><br />
+								 <em>-optStrList</em>
+					</td>
+					<!--<td>List[String]</td> -->
+					<td>[]</td>
+					<td>An optional list of strings</td>
+				</tr>
+				<tr>
+					<td><a href="#--testPlugin">--testPlugin</a><br />
+					</td>
+					<!--<td>List[String]</td> -->
+					<td>[]</td>
+					<td>Undocumented option</td>
+				</tr>
+			<tr>
+				<th colspan="4" id="row-divider">Advanced Arguments</th>
+			</tr>
+				<tr>
+					<td><a href="#--advancedOptionalInt">--advancedOptionalInt</a><br />
+								 <em>-advancedOptInt</em>
+					</td>
+					<!--<td>int</td> -->
+					<td>1</td>
+					<td>advancedOptionalInt with initial value 1</td>
+				</tr>
+			<tr>
+				<th colspan="4" id="row-divider">Deprecated Arguments</th>
+			</tr>
+				<tr>
+					<td><a href="#--deprecatedString">--deprecatedString</a><br />
+								 <em>-depStr</em>
+					</td>
+					<!--<td>int</td> -->
+					<td>0</td>
+					<td>deprecated</td>
+				</tr>
+					</tbody>
+				</table>
+
+					<h3>Argument details</h3>
+					<p>Arguments in this list are specific to this tool. Keep in mind that other arguments are available that are shared with other tools (e.g. command-line GATK arguments); see Inherited arguments above.</p>
+		<hr style="border-bottom: dotted 1px #C0C0C0;" />
+		<h3><a name="[NA - Positional]">[NA - Positional] </a>
+			
+		</h3>
+		<p class="args">
+			<b>Positional arguments, min = 2, max = 2</b><br />
+			Positional arguments, min = 2, max = 2
+		</p>
+		<p>
+				<span class="badge badge-important">R</span>
+			<span class="label label-info ">List[File]</span>
+				 <span class="label">NA</span>
+		</p>
+		<hr style="border-bottom: dotted 1px #C0C0C0;" />
+		<h3><a name="--advancedOptionalInt">--advancedOptionalInt </a>
+			 / <small>-advancedOptInt</small>
+		</h3>
+		<p class="args">
+			<b>advancedOptionalInt with initial value 1</b><br />
+			Advanced, Optional int
+		</p>
+		<p>
+			<span class="label label-info ">int</span>
+				 <span class="label">1</span>
+				 <span class="label label-warning">[ [ -∞</span>
+				 <span class="label label-warning">∞ ] ]</span>
+		</p>
+		<hr style="border-bottom: dotted 1px #C0C0C0;" />
+		<h3><a name="--deprecatedString">--deprecatedString </a>
+			 / <small>-depStr</small>
+		</h3>
+		<p class="args">
+			<b>deprecated</b><br />
+			Deprecated string
+		</p>
+		<p>
+			<span class="label label-info ">int</span>
+				 <span class="label">0</span>
+				 <span class="label label-warning">[ [ -∞</span>
+				 <span class="label label-warning">∞ ] ]</span>
+		</p>
+		<hr style="border-bottom: dotted 1px #C0C0C0;" />
+		<h3><a name="--mutexArg">--mutexArg </a>
+			 / <small>-mutexArg</small>
+		</h3>
+		<p class="args">
+			<b>Undocumented option</b><br />
+			Mutually exclusive args
+		</p>
+			<p><b>Exclusion:</b> This argument cannot be used at the same time as <code>READ1_ALIGNED_BAM, READ2_ALIGNED_BAM</code>.</p>
+		<p>
+			<span class="label label-info ">List[File]</span>
+				 <span class="label">[]</span>
+		</p>
+		<hr style="border-bottom: dotted 1px #C0C0C0;" />
+		<h3><a name="--mutexTargetField1">--mutexTargetField1 </a>
+			 / <small>-mutexTargetField1</small>
+		</h3>
+		<p class="args">
+			<b>SAM/BAM/CRAM file(s) with alignment data from the first read of a pair.</b><br />
+			
+		</p>
+			<p><b>Exclusion:</b> This argument cannot be used at the same time as <code>mutexSourceField</code>.</p>
+		<p>
+			<span class="label label-info ">List[File]</span>
+				 <span class="label">[]</span>
+		</p>
+		<hr style="border-bottom: dotted 1px #C0C0C0;" />
+		<h3><a name="--mutexTargetField2">--mutexTargetField2 </a>
+			 / <small>-mutexTargetField2</small>
+		</h3>
+		<p class="args">
+			<b>SAM/BAM file(s) with alignment data from the second read of a pair.</b><br />
+			
+		</p>
+			<p><b>Exclusion:</b> This argument cannot be used at the same time as <code>mutexSourceField</code>.</p>
+		<p>
+			<span class="label label-info ">List[File]</span>
+				 <span class="label">[]</span>
+		</p>
+		<hr style="border-bottom: dotted 1px #C0C0C0;" />
+		<h3><a name="--optionalClpEnum">--optionalClpEnum </a>
+			 / <small>-optionalClpEnum</small>
+		</h3>
+		<p class="args">
+			<b>Optional Clp enum</b><br />
+			
+		</p>
+			<p>
+				The --optionalClpEnum argument is an enumerated type (TestEnum), which can have one of the following values:
+			<dl class="enum">
+					<dt class="enum">ENUM_VALUE_1</dt>
+					<dd class="enum"></dd>
+					<dt class="enum">ENUM_VALUE_2</dt>
+					<dd class="enum"></dd>
+			</dl>
+			</p>
+		<p>
+			<span class="label label-info ">TestEnum</span>
+				 <span class="label">ENUM_VALUE_1</span>
+		</p>
+		<hr style="border-bottom: dotted 1px #C0C0C0;" />
+		<h3><a name="--optionalDouble">--optionalDouble </a>
+			 / <small>-optDouble</small>
+		</h3>
+		<p class="args">
+			<b>Optionals double with initial value 2.15</b><br />
+			Required double
+		</p>
+		<p>
+			<span class="label label-info ">double</span>
+				 <span class="label">2.15</span>
+				 <span class="label label-warning">[ [ -∞</span>
+				 <span class="label label-warning">∞ ] ]</span>
+		</p>
+		<hr style="border-bottom: dotted 1px #C0C0C0;" />
+		<h3><a name="--optionalDoubleList">--optionalDoubleList </a>
+			 / <small>-optDoubleList</small>
+		</h3>
+		<p class="args">
+			<b>optionalDoubleList with initial values: 100.0, 99.9, 99.0, 90.0</b><br />
+			Optional double list.
+		</p>
+		<p>
+			<span class="label label-info ">List[Double]</span>
+				 <span class="label">[100.0, 99.9, 99.0, 90.0]</span>
+		</p>
+		<hr style="border-bottom: dotted 1px #C0C0C0;" />
+		<h3><a name="--optionalFileList">--optionalFileList </a>
+			 / <small>-optFilList</small>
+		</h3>
+		<p class="args">
+			<b>Optional file list</b><br />
+			Optional file list.
+		</p>
+		<p>
+			<span class="label label-info ">List[File]</span>
+				 <span class="label">[]</span>
+		</p>
+		<hr style="border-bottom: dotted 1px #C0C0C0;" />
+		<h3><a name="--optionalFlag">--optionalFlag </a>
+			 / <small>-optFlag</small>
+		</h3>
+		<p class="args">
+			<b>Optional flag, defaults to false.</b><br />
+			Optional flag.
+		</p>
+		<p>
+			<span class="label label-info ">boolean</span>
+				 <span class="label">false</span>
+		</p>
+		<hr style="border-bottom: dotted 1px #C0C0C0;" />
+		<h3><a name="--optionalInputFilesFromArgCollection">--optionalInputFilesFromArgCollection </a>
+			 / <small>-optionalInputFilesFromArgCollection</small>
+		</h3>
+		<p class="args">
+			<b>Optional input files from argument collection</b><br />
+			
+		</p>
+		<p>
+			<span class="label label-info ">List[File]</span>
+				 <span class="label">[]</span>
+		</p>
+		<hr style="border-bottom: dotted 1px #C0C0C0;" />
+		<h3><a name="--optionalStringInputFromArgCollection">--optionalStringInputFromArgCollection </a>
+			 / <small>-optionalStringInputFromArgCollection</small>
+		</h3>
+		<p class="args">
+			<b>Optional string input from argument collection</b><br />
+			
+		</p>
+		<p>
+			<span class="label label-info ">String</span>
+				 <span class="label">null</span>
+		</p>
+		<hr style="border-bottom: dotted 1px #C0C0C0;" />
+		<h3><a name="--optionalStringList">--optionalStringList </a>
+			 / <small>-optStrList</small>
+		</h3>
+		<p class="args">
+			<b>An optional list of strings</b><br />
+			Optional string list.
+		</p>
+		<p>
+			<span class="label label-info ">List[String]</span>
+				 <span class="label">[]</span>
+		</p>
+		<hr style="border-bottom: dotted 1px #C0C0C0;" />
+		<h3><a name="--requiredClpEnum">--requiredClpEnum </a>
+			 / <small>-requiredClpEnum</small>
+		</h3>
+		<p class="args">
+			<b>Required Clp enum</b><br />
+			
+		</p>
+			<p>
+				The --requiredClpEnum argument is an enumerated type (TestEnum), which can have one of the following values:
+			<dl class="enum">
+					<dt class="enum">ENUM_VALUE_1</dt>
+					<dd class="enum"></dd>
+					<dt class="enum">ENUM_VALUE_2</dt>
+					<dd class="enum"></dd>
+			</dl>
+			</p>
+		<p>
+				<span class="badge badge-important">R</span>
+			<span class="label label-info ">TestEnum</span>
+				 <span class="label">null</span>
+		</p>
+		<hr style="border-bottom: dotted 1px #C0C0C0;" />
+		<h3><a name="--requiredFileList">--requiredFileList </a>
+			 / <small>-reqFilList</small>
+		</h3>
+		<p class="args">
+			<b>Required file list</b><br />
+			Required file list.
+		</p>
+		<p>
+				<span class="badge badge-important">R</span>
+			<span class="label label-info ">List[File]</span>
+				 <span class="label">[]</span>
+		</p>
+		<hr style="border-bottom: dotted 1px #C0C0C0;" />
+		<h3><a name="--requiredInputFilesFromArgCollection">--requiredInputFilesFromArgCollection </a>
+			 / <small>-rRequiredInputFilesFromArgCollection</small>
+		</h3>
+		<p class="args">
+			<b>Required input files from argument collection</b><br />
+			
+		</p>
+		<p>
+				<span class="badge badge-important">R</span>
+			<span class="label label-info ">List[File]</span>
+				 <span class="label">[]</span>
+		</p>
+		<hr style="border-bottom: dotted 1px #C0C0C0;" />
+		<h3><a name="--requiredStringInputFromArgCollection">--requiredStringInputFromArgCollection </a>
+			 / <small>-requiredStringInputFromArgCollection</small>
+		</h3>
+		<p class="args">
+			<b>Required string input from argument collection</b><br />
+			
+		</p>
+		<p>
+				<span class="badge badge-important">R</span>
+			<span class="label label-info ">String</span>
+				 <span class="label">null</span>
+		</p>
+		<hr style="border-bottom: dotted 1px #C0C0C0;" />
+		<h3><a name="--requiredStringList">--requiredStringList </a>
+			 / <small>-reqStrList</small>
+		</h3>
+		<p class="args">
+			<b>A required list of strings</b><br />
+			Required string list.
+		</p>
+		<p>
+				<span class="badge badge-important">R</span>
+			<span class="label label-info ">List[String]</span>
+				 <span class="label">[]</span>
+		</p>
+		<hr style="border-bottom: dotted 1px #C0C0C0;" />
+		<h3><a name="--testPlugin">--testPlugin </a>
+			
+		</h3>
+		<p class="args">
+			<b>Undocumented option</b><br />
+			
+		</p>
+		<p>
+			<span class="label label-info ">List[String]</span>
+				 <span class="label">[]</span>
+		</p>
+		<hr style="border-bottom: dotted 1px #C0C0C0;" />
+		<h3><a name="--usesFieldNameForArgName">--usesFieldNameForArgName </a>
+			
+		</h3>
+		<p class="args">
+			<b>Use field name if no name in annotation.</b><br />
+			
+		</p>
+		<p>
+				<span class="badge badge-important">R</span>
+			<span class="label label-info ">String</span>
+				 <span class="label">null</span>
+		</p>
+
+        <hr>
+        <p><a href='#top'><i class='fa fa-chevron-up'></i> Return to top</a></p>
+        <hr>
+        <p class="version">Barclay version 11.1 built at 2016/01/01 01:01:01.
+        </p>
+
+	</div>
+
+	<?php printFooter($module); ?>
\ No newline at end of file
diff --git a/src/test/resources/org/broadinstitute/barclay/help/expected/TestDoclet/org_broadinstitute_barclay_help_TestArgumentContainer.json b/src/test/resources/org/broadinstitute/barclay/help/expected/TestDoclet/org_broadinstitute_barclay_help_TestArgumentContainer.json
new file mode 100644
index 0000000..2d6cbf1
--- /dev/null
+++ b/src/test/resources/org/broadinstitute/barclay/help/expected/TestDoclet/org_broadinstitute_barclay_help_TestArgumentContainer.json
@@ -0,0 +1,341 @@
+{
+  "summary": "Argument container class for testing documentation generation.",
+  "arguments": [
+    {
+      "summary": "Positional arguments, min \u003d 2, max \u003d 2",
+      "name": "[NA - Positional]",
+      "synonyms": "NA",
+      "type": "List[File]",
+      "required": "yes",
+      "fulltext": "Positional arguments, min \u003d 2, max \u003d 2",
+      "defaultValue": "NA",
+      "minValue": "NA",
+      "maxValue": "NA",
+      "minRecValue": "NA",
+      "maxRecValue": "NA",
+      "kind": "positional",
+      "options": []
+    },
+    {
+      "summary": "advancedOptionalInt with initial value 1",
+      "name": "--advancedOptionalInt",
+      "synonyms": "-advancedOptInt",
+      "type": "int",
+      "required": "no",
+      "fulltext": "Advanced, Optional int",
+      "defaultValue": "1",
+      "minValue": "-Infinity",
+      "maxValue": "Infinity",
+      "minRecValue": "NA",
+      "maxRecValue": "NA",
+      "kind": "advanced",
+      "options": []
+    },
+    {
+      "summary": "deprecated",
+      "name": "--deprecatedString",
+      "synonyms": "-depStr",
+      "type": "int",
+      "required": "no",
+      "fulltext": "Deprecated string",
+      "defaultValue": "0",
+      "minValue": "-Infinity",
+      "maxValue": "Infinity",
+      "minRecValue": "NA",
+      "maxRecValue": "NA",
+      "kind": "deprecated",
+      "options": []
+    },
+    {
+      "summary": "Undocumented option",
+      "name": "--mutexArg",
+      "synonyms": "-mutexArg",
+      "type": "List[File]",
+      "required": "no",
+      "fulltext": "Mutually exclusive args",
+      "defaultValue": "[]",
+      "minValue": "NA",
+      "maxValue": "NA",
+      "minRecValue": "NA",
+      "maxRecValue": "NA",
+      "kind": "optional",
+      "options": []
+    },
+    {
+      "summary": "SAM/BAM/CRAM file(s) with alignment data from the first read of a pair.",
+      "name": "--mutexTargetField1",
+      "synonyms": "-mutexTargetField1",
+      "type": "List[File]",
+      "required": "no",
+      "fulltext": "",
+      "defaultValue": "[]",
+      "minValue": "NA",
+      "maxValue": "NA",
+      "minRecValue": "NA",
+      "maxRecValue": "NA",
+      "kind": "optional",
+      "options": []
+    },
+    {
+      "summary": "SAM/BAM file(s) with alignment data from the second read of a pair.",
+      "name": "--mutexTargetField2",
+      "synonyms": "-mutexTargetField2",
+      "type": "List[File]",
+      "required": "no",
+      "fulltext": "",
+      "defaultValue": "[]",
+      "minValue": "NA",
+      "maxValue": "NA",
+      "minRecValue": "NA",
+      "maxRecValue": "NA",
+      "kind": "optional",
+      "options": []
+    },
+    {
+      "summary": "Optional Clp enum",
+      "name": "--optionalClpEnum",
+      "synonyms": "-optionalClpEnum",
+      "type": "TestEnum",
+      "required": "no",
+      "fulltext": "",
+      "defaultValue": "ENUM_VALUE_1",
+      "minValue": "NA",
+      "maxValue": "NA",
+      "minRecValue": "NA",
+      "maxRecValue": "NA",
+      "kind": "optional",
+      "options": [
+        {
+          "summary": "",
+          "name": "ENUM_VALUE_1"
+        },
+        {
+          "summary": "",
+          "name": "ENUM_VALUE_2"
+        }
+      ]
+    },
+    {
+      "summary": "Optionals double with initial value 2.15",
+      "name": "--optionalDouble",
+      "synonyms": "-optDouble",
+      "type": "double",
+      "required": "no",
+      "fulltext": "Required double",
+      "defaultValue": "2.15",
+      "minValue": "-Infinity",
+      "maxValue": "Infinity",
+      "minRecValue": "NA",
+      "maxRecValue": "NA",
+      "kind": "optional",
+      "options": []
+    },
+    {
+      "summary": "optionalDoubleList with initial values: 100.0, 99.9, 99.0, 90.0",
+      "name": "--optionalDoubleList",
+      "synonyms": "-optDoubleList",
+      "type": "List[Double]",
+      "required": "no",
+      "fulltext": "Optional double list.",
+      "defaultValue": "[100.0, 99.9, 99.0, 90.0]",
+      "minValue": "NA",
+      "maxValue": "NA",
+      "minRecValue": "NA",
+      "maxRecValue": "NA",
+      "kind": "optional",
+      "options": []
+    },
+    {
+      "summary": "Optional file list",
+      "name": "--optionalFileList",
+      "synonyms": "-optFilList",
+      "type": "List[File]",
+      "required": "no",
+      "fulltext": "Optional file list.",
+      "defaultValue": "[]",
+      "minValue": "NA",
+      "maxValue": "NA",
+      "minRecValue": "NA",
+      "maxRecValue": "NA",
+      "kind": "optional",
+      "options": []
+    },
+    {
+      "summary": "Optional flag, defaults to false.",
+      "name": "--optionalFlag",
+      "synonyms": "-optFlag",
+      "type": "boolean",
+      "required": "no",
+      "fulltext": "Optional flag.",
+      "defaultValue": "false",
+      "minValue": "NA",
+      "maxValue": "NA",
+      "minRecValue": "NA",
+      "maxRecValue": "NA",
+      "kind": "optional",
+      "options": []
+    },
+    {
+      "summary": "Optional input files from argument collection",
+      "name": "--optionalInputFilesFromArgCollection",
+      "synonyms": "-optionalInputFilesFromArgCollection",
+      "type": "List[File]",
+      "required": "no",
+      "fulltext": "",
+      "defaultValue": "[]",
+      "minValue": "NA",
+      "maxValue": "NA",
+      "minRecValue": "NA",
+      "maxRecValue": "NA",
+      "kind": "optional",
+      "options": []
+    },
+    {
+      "summary": "Optional string input from argument collection",
+      "name": "--optionalStringInputFromArgCollection",
+      "synonyms": "-optionalStringInputFromArgCollection",
+      "type": "String",
+      "required": "no",
+      "fulltext": "",
+      "defaultValue": "null",
+      "minValue": "NA",
+      "maxValue": "NA",
+      "minRecValue": "NA",
+      "maxRecValue": "NA",
+      "kind": "optional",
+      "options": []
+    },
+    {
+      "summary": "An optional list of strings",
+      "name": "--optionalStringList",
+      "synonyms": "-optStrList",
+      "type": "List[String]",
+      "required": "no",
+      "fulltext": "Optional string list.",
+      "defaultValue": "[]",
+      "minValue": "NA",
+      "maxValue": "NA",
+      "minRecValue": "NA",
+      "maxRecValue": "NA",
+      "kind": "optional",
+      "options": []
+    },
+    {
+      "summary": "Required Clp enum",
+      "name": "--requiredClpEnum",
+      "synonyms": "-requiredClpEnum",
+      "type": "TestEnum",
+      "required": "yes",
+      "fulltext": "",
+      "defaultValue": "null",
+      "minValue": "NA",
+      "maxValue": "NA",
+      "minRecValue": "NA",
+      "maxRecValue": "NA",
+      "kind": "required",
+      "options": [
+        {
+          "summary": "",
+          "name": "ENUM_VALUE_1"
+        },
+        {
+          "summary": "",
+          "name": "ENUM_VALUE_2"
+        }
+      ]
+    },
+    {
+      "summary": "Required file list",
+      "name": "--requiredFileList",
+      "synonyms": "-reqFilList",
+      "type": "List[File]",
+      "required": "yes",
+      "fulltext": "Required file list.",
+      "defaultValue": "[]",
+      "minValue": "NA",
+      "maxValue": "NA",
+      "minRecValue": "NA",
+      "maxRecValue": "NA",
+      "kind": "required",
+      "options": []
+    },
+    {
+      "summary": "Required input files from argument collection",
+      "name": "--requiredInputFilesFromArgCollection",
+      "synonyms": "-rRequiredInputFilesFromArgCollection",
+      "type": "List[File]",
+      "required": "yes",
+      "fulltext": "",
+      "defaultValue": "[]",
+      "minValue": "NA",
+      "maxValue": "NA",
+      "minRecValue": "NA",
+      "maxRecValue": "NA",
+      "kind": "required",
+      "options": []
+    },
+    {
+      "summary": "Required string input from argument collection",
+      "name": "--requiredStringInputFromArgCollection",
+      "synonyms": "-requiredStringInputFromArgCollection",
+      "type": "String",
+      "required": "yes",
+      "fulltext": "",
+      "defaultValue": "null",
+      "minValue": "NA",
+      "maxValue": "NA",
+      "minRecValue": "NA",
+      "maxRecValue": "NA",
+      "kind": "required",
+      "options": []
+    },
+    {
+      "summary": "A required list of strings",
+      "name": "--requiredStringList",
+      "synonyms": "-reqStrList",
+      "type": "List[String]",
+      "required": "yes",
+      "fulltext": "Required string list.",
+      "defaultValue": "[]",
+      "minValue": "NA",
+      "maxValue": "NA",
+      "minRecValue": "NA",
+      "maxRecValue": "NA",
+      "kind": "required",
+      "options": []
+    },
+    {
+      "summary": "Undocumented option",
+      "name": "--testPlugin",
+      "synonyms": "NA",
+      "type": "List[String]",
+      "required": "no",
+      "fulltext": "",
+      "defaultValue": "[]",
+      "minValue": "NA",
+      "maxValue": "NA",
+      "minRecValue": "NA",
+      "maxRecValue": "NA",
+      "kind": "optional",
+      "options": []
+    },
+    {
+      "summary": "Use field name if no name in annotation.",
+      "name": "--usesFieldNameForArgName",
+      "synonyms": "NA",
+      "type": "String",
+      "required": "yes",
+      "fulltext": "",
+      "defaultValue": "null",
+      "minValue": "NA",
+      "maxValue": "NA",
+      "minRecValue": "NA",
+      "maxRecValue": "NA",
+      "kind": "required",
+      "options": []
+    }
+  ],
+  "description": "Argument container class for testing documentation generation. Contains an argument\n for each @Argument, @ArgumentCollection, and @DocumentedFeature property that should\n be tested.\n\n Test custom tag:\n \n\n \u003cp\u003e\n The purpose of this paragraph is to test embedded html formatting.\n \u003col\u003e\n     \u003cli\u003eThis is point number 1\u003c/li\u003e\n     \u003cli\u003eThis is point number 2\u003c/li\u003e\n \u003c/ol\u003e\n \u003c/p\u003e",
+  "name": "TestArgumentContainer",
+  "group": "Test feature group name"
+}
\ No newline at end of file
diff --git a/src/test/resources/org/broadinstitute/barclay/help/expected/TestDoclet/org_broadinstitute_barclay_help_TestExtraDocs.html b/src/test/resources/org/broadinstitute/barclay/help/expected/TestDoclet/org_broadinstitute_barclay_help_TestExtraDocs.html
new file mode 100644
index 0000000..d770744
--- /dev/null
+++ b/src/test/resources/org/broadinstitute/barclay/help/expected/TestDoclet/org_broadinstitute_barclay_help_TestExtraDocs.html
@@ -0,0 +1,81 @@
+<?php
+    include '../../../common/include/common.php';
+?>
+
+<div class='row-fluid' id="top">
+
+	
+
+	<?php $group = 'Test extra docs group name'; ?>
+
+	<section class="span4">
+		<aside class="well">
+			<a href="index"><h4><i class='fa fa-chevron-left'></i> Back to Tool Docs Index</h4></a>
+		</aside>
+		<aside class="well">
+			<h2>Categories</h2>
+        <style>
+            #sidenav .accordion-body a {
+                color : gray;
+            }
+
+            .accordion-body li {
+                list-style : none;
+            }
+        </style>
+        <ul class="nav nav-pills nav-stacked" id="sidenav">
+        		<hr>
+        		<hr>
+        		<hr>
+						<li><a data-toggle="collapse" data-parent="#sidenav" href="#Testextradocsgroupname">Test extra docs group name</a>
+							<div id="Testextradocsgroupname"
+								<?php echo ($group == 'Test extra docs group name')? 'class="accordion-body collapse in"'.chr(62) : 'class="accordion-body collapse"'.chr(62);?>
+								<ul>
+											<li>
+												<a href="org_broadinstitute_barclay_help_TestExtraDocs.html">TestExtraDocs</a>
+											</li>
+								</ul>
+							</div>
+						</li>
+						<li><a data-toggle="collapse" data-parent="#sidenav" href="#Testfeaturegroupname">Test feature group name</a>
+							<div id="Testfeaturegroupname"
+								<?php echo ($group == 'Test feature group name')? 'class="accordion-body collapse in"'.chr(62) : 'class="accordion-body collapse"'.chr(62);?>
+								<ul>
+											<li>
+												<a href="org_broadinstitute_barclay_help_TestArgumentContainer.html">TestArgumentContainer</a>
+											</li>
+								</ul>
+							</div>
+						</li>
+        		<hr>
+        </ul>
+		</aside>
+		<?php getForumPosts( 'TestExtraDocs' ) ?>
+
+	</section>
+
+	<div class="span8">
+
+			<h1>TestExtraDocs **BETA**</h1>
+
+		<p class="lead">Class for testing extraDocs property in docgen.</p>
+
+			<h3>Category
+				<small> Test extra docs group name</small>
+			</h3>
+		<hr>
+		<h2>Overview</h2>
+		Class for testing extraDocs property in docgen.
+
+
+
+
+        <hr>
+        <p><a href='#top'><i class='fa fa-chevron-up'></i> Return to top</a></p>
+        <hr>
+        <p class="version">Barclay version 11.1 built at 2016/01/01 01:01:01.
+        </p>
+
+	</div>
+
+	<?php printFooter($module); ?>
\ No newline at end of file
diff --git a/src/test/resources/org/broadinstitute/barclay/help/expected/TestDoclet/org_broadinstitute_barclay_help_TestExtraDocs.json b/src/test/resources/org/broadinstitute/barclay/help/expected/TestDoclet/org_broadinstitute_barclay_help_TestExtraDocs.json
new file mode 100644
index 0000000..c6abf60
--- /dev/null
+++ b/src/test/resources/org/broadinstitute/barclay/help/expected/TestDoclet/org_broadinstitute_barclay_help_TestExtraDocs.json
@@ -0,0 +1,6 @@
+{
+  "summary": "Class for testing extraDocs property in docgen.",
+  "description": "Class for testing extraDocs property in docgen.",
+  "name": "TestExtraDocs",
+  "group": "Test extra docs group name"
+}
\ No newline at end of file
diff --git a/src/test/resources/org/broadinstitute/barclay/help/templates/TestDoclet/common.html.ftl b/src/test/resources/org/broadinstitute/barclay/help/templates/TestDoclet/common.html.ftl
new file mode 100644
index 0000000..a4eaaec
--- /dev/null
+++ b/src/test/resources/org/broadinstitute/barclay/help/templates/TestDoclet/common.html.ftl
@@ -0,0 +1,53 @@
+<#--
+        This file contains part of the theming used to present Barclay docs on a website. Styling is separated
+        out, so pages will be minimalistic html unless replacement styling is provided.
+        -->
+
+    <#macro footerInfo>
+        <hr>
+        <p><a href='#top'><i class='fa fa-chevron-up'></i> Return to top</a></p>
+        <hr>
+        <p class="version">Barclay version ${version} built at ${timestamp}.
+        <#-- closing P tag in next macro -->
+    </#macro>
+    
+    <#macro footerClose>
+    	<#-- ugly little hack to enable adding tool-specific info inline -->
+        </p>
+    </#macro>
+
+    <#macro getCategories groups>
+        <style>
+            #sidenav .accordion-body a {
+                color : gray;
+            }
+
+            .accordion-body li {
+                list-style : none;
+            }
+        </style>
+        <ul class="nav nav-pills nav-stacked" id="sidenav">
+        	<#assign seq = ["engine", "tools", "other", "utilities"]>
+        	<#list seq as supercat>
+        		<hr>
+        		<#list groups?sort_by("name") as group>
+        			<#if group.supercat == supercat>
+						<li><a data-toggle="collapse" data-parent="#sidenav" href="#${group.id}">${group.name}</a>
+							<div id="${group.id}"
+								<?php echo ($group == '${group.name}')? 'class="accordion-body collapse in"'.chr(62) : 'class="accordion-body collapse"'.chr(62);?>
+								<ul>
+									<#list data as datum>
+										<#if datum.group == group.name>
+											<li>
+												<a href="${datum.filename}">${datum.name}</a>
+											</li>
+										</#if>
+									</#list>
+								</ul>
+							</div>
+						</li>
+        			</#if>
+        		</#list>
+        	</#list>
+        </ul>
+    </#macro>
\ No newline at end of file
diff --git a/src/test/resources/org/broadinstitute/barclay/help/templates/TestDoclet/generic.html.ftl b/src/test/resources/org/broadinstitute/barclay/help/templates/TestDoclet/generic.html.ftl
new file mode 100644
index 0000000..d18d9e7
--- /dev/null
+++ b/src/test/resources/org/broadinstitute/barclay/help/templates/TestDoclet/generic.html.ftl
@@ -0,0 +1,205 @@
+<?php
+    include '../../../common/include/common.php';
+?>
+
+<div class='row-fluid' id="top">
+
+	<#include "common.html.ftl"/>
+
+	<#macro argumentlist name myargs>
+		<#if myargs?size != 0>
+			<tr>
+				<th colspan="4" id="row-divider">${name}</th>
+			</tr>
+			<#list myargs as arg>
+				<tr>
+					<td><a href="#${arg.name}">${arg.name}</a><br />
+						<#if arg.synonyms != "NA">
+							<#if arg.name[2..] != arg.synonyms[1..]>
+								 <em>${arg.synonyms}</em>
+							</#if>
+						</#if>
+					</td>
+					<!--<td>${arg.type}</td> -->
+					<td>${arg.defaultValue!"NA"}</td>
+					<td>${arg.summary}</td>
+				</tr>
+			</#list>
+		</#if>
+	</#macro>
+
+	<#macro argumentDetails arg>
+		<hr style="border-bottom: dotted 1px #C0C0C0;" />
+		<h3><a name="${arg.name}">${arg.name} </a>
+			<#if arg.synonyms != "NA"> / <small>${arg.synonyms}</small></#if>
+		</h3>
+		<p class="args">
+			<b>${arg.summary}</b><br />
+			${arg.fulltext}
+		</p>
+		<#if arg.otherArgumentRequired != "NA">
+			<p><b>Dependency:</b> This argument requires that you also specify <code>${arg.otherArgumentRequired}</code>.</p>
+		</#if>
+		<#if arg.exclusiveOf != "NA">
+			<p><b>Exclusion:</b> This argument cannot be used at the same time as <code>${arg.exclusiveOf}</code>.</p>
+		</#if>
+		<#if arg.options?has_content>
+			<p>
+				The ${arg.name} argument is an enumerated type (${arg.type}), which can have one of the following values:
+			<dl class="enum">
+				<#list arg.options as option>
+					<dt class="enum">${option.name}</dt>
+					<dd class="enum">${option.summary}</dd>
+				</#list>
+			</dl>
+			</p>
+		</#if>
+		<p><#if arg.required != "NA">
+			<#if arg.required == "yes">
+				<span class="badge badge-important">R</span>
+			</#if>
+		</#if>
+			<span class="label label-info ">${arg.type}</span>
+			<#if arg.defaultValue?has_content>
+				 <span class="label">${arg.defaultValue}</span>
+			</#if>
+			<#if arg.minValue?is_number>
+				 <span class="label label-warning">[ [ ${arg.minValue}</span>
+			</#if>
+			<#if arg.minRecValue?is_number>
+				 <span class="label label-success">[ ${arg.minRecValue}</span>
+			</#if>
+			<#if arg.maxRecValue?is_number>
+				 <span class="label label-success">${arg.maxRecValue} ]</span>
+			</#if>
+			<#if arg.maxValue?is_number>
+				 <span class="label label-warning">${arg.maxValue} ] ]</span>
+			</#if>
+		</p>
+	</#macro>
+
+	<#macro relatedByType name type>
+		<#list relatedDocs as relatedDoc>
+			<#if relatedDoc.relation == type>
+				<h3>${name}</h3>
+				<ul>
+					<#list relatedDocs as relatedDoc>
+						<#if relatedDoc.relation == type>
+							<li><a href="${relatedDoc.filename}">${relatedDoc.name}</a> is a ${relatedDoc.relation}</li>
+						</#if>
+					</#list>
+				</ul>
+				<#break>
+			</#if>
+		</#list>
+	</#macro>
+
+	<?php $group = '${group}'; ?>
+
+	<section class="span4">
+		<aside class="well">
+			<a href="index"><h4><i class='fa fa-chevron-left'></i> Back to Tool Docs Index</h4></a>
+		</aside>
+		<aside class="well">
+			<h2>Categories</h2>
+			<@getCategories groups=groups />
+		</aside>
+		<?php getForumPosts( '${name}' ) ?>
+
+	</section>
+
+	<div class="span8">
+
+		<#if beta??>
+			<h1>${name} **BETA**</h1>
+		<#else>
+			<h1>${name}</h1>
+		</#if>
+
+		<p class="lead">${summary}</p>
+
+		<#if group?? >
+			<h3>Category
+				<small> ${group}</small>
+			</h3>
+		</#if>
+		<hr>
+		<h2>Overview</h2>
+		${description}
+
+		<#-- Create references to additional capabilities if appropriate -->
+			<#if testPlugin?size != 0>
+				<hr>
+				<h2>Additional Information</h2>
+				<p></p>
+			</#if>
+			<#if testPlugin?size != 0>
+				<h3>Test Plugins</h3>
+				<#if testPlugin?size = 1>
+					<p>This Test Plugin is automatically applied to the data by the Engine before processing by ${name}.</p>
+				</#if>
+				<#if (testPlugin?size > 1) >
+					<p>These Test Plugins are automatically applied to the data by the Engine before processing by ${name}.</p>
+				</#if>
+				<ul>
+					<#list testPlugin as plugin>
+						<li><a href="${plugin.filename}">${plugin.name}</a></li>
+					</#list>
+				</ul>
+			</#if>
+			<#if extradocs?size != 0 || arguments.all?size != 0>
+				<hr>
+				<h2>Command-line Arguments</h2>
+				<p></p>
+			</#if>
+			<#if extradocs?size != 0>
+				<h3>Additional References</h3>
+				<p>See these additional references for more information.</p>
+				<ul>
+					<#list extradocs as extradoc>
+						<li><a href="${extradoc.filename}">${extradoc.name}</a></li>
+					</#list>
+				</ul>
+			</#if>
+
+			<#-- Create the argument summary -->
+			<#if arguments.all?size != 0>
+				<h3>${name} specific arguments</h3>
+				<p>This table summarizes the command-line arguments that are specific to this tool. For more details on each argument, see the list further down below the table or click on an argument name to jump directly to that entry in the list.</p>
+				<table class="table table-striped table-bordered table-condensed">
+					<thead>
+					<tr>
+						<th>Argument name(s)</th>
+						<th>Default value</th>
+						<th>Summary</th>
+					</tr>
+					</thead>
+					<tbody>
+					<@argumentlist name="Positional Arguments" myargs=arguments.positional/>
+					<@argumentlist name="Required Arguments" myargs=arguments.required/>
+					<@argumentlist name="Optional Tool Arguments" myargs=arguments.optional/>
+					<@argumentlist name="Optional Common Arguments" myargs=arguments.common/>
+					<@argumentlist name="Dependent Arguments" myargs=arguments.dependent/>
+					<@argumentlist name="Advanced Arguments" myargs=arguments.advanced/>
+					<@argumentlist name="Hidden Arguments" myargs=arguments.hidden/>
+					<@argumentlist name="Deprecated Arguments" myargs=arguments.deprecated/>
+					</tbody>
+				</table>
+			</#if>
+
+			<#-- List all of the things -->
+			<#if arguments.all?size != 0>
+				<#-- Create the argument details -->
+					<h3>Argument details</h3>
+					<p>Arguments in this list are specific to this tool. Keep in mind that other arguments are available that are shared with other tools (e.g. command-line GATK arguments); see Inherited arguments above.</p>
+					<#list arguments.all as arg>
+						<@argumentDetails arg=arg/>
+					</#list>
+			</#if>
+
+			<@footerInfo />
+			<@footerClose />
+
+	</div>
+
+	<?php printFooter($module); ?>
\ No newline at end of file
diff --git a/src/test/resources/org/broadinstitute/barclay/help/templates/TestDoclet/generic.index.html.ftl b/src/test/resources/org/broadinstitute/barclay/help/templates/TestDoclet/generic.index.html.ftl
new file mode 100644
index 0000000..f5de067
--- /dev/null
+++ b/src/test/resources/org/broadinstitute/barclay/help/templates/TestDoclet/generic.index.html.ftl
@@ -0,0 +1,71 @@
+<?php
+
+    include '../../../common/include/common.php';
+    include_once '../../config.php';
+    printHeader($module, "Tool Documentation Index", "Guide");
+?>
+
+<div class='row-fluid'>
+
+<div class='span9'>
+
+<#include "common.html.ftl"/>
+
+<#macro emitGroup group>
+    <div class="accordion-group">
+        <div class="accordion-heading">
+            <a class="accordion-toggle" data-toggle="collapse" data-parent="#index" href="#${group.id}">
+                <h4>${group.name}</h4>
+            </a>
+        </div>
+        <div class="accordion-body collapse" id="${group.id}">
+            <div class="accordion-inner">
+                <p class="lead">${group.summary}</p>
+                <table class="table table-striped table-bordered table-condensed">
+                    <tr>
+                        <th>Name</th>
+                        <th>Summary</th>
+                    </tr>
+                    <#list data as datum>
+                        <#if datum.group == group.name>
+                            <tr>
+                                <#if datum.beta??>
+                                    <td><a href="${datum.filename}">${datum.name} **BETA**</a></td>
+                                <#else>
+                                    <td><a href="${datum.filename}">${datum.name}</a></td>
+                                </#if>
+                                <td>${datum.summary}</td>
+                            </tr>
+                        </#if>
+                    </#list>
+                </table>
+            </div>
+        </div>
+    </div>
+</#macro>
+
+<h1 id="top">Tool Documentation Index
+    <small>${version}</small>
+</h1>
+<div class="accordion" id="index">
+    <#assign seq = ["engine", "tools", "other", "utilities"]>
+	<#list seq as supercat>
+		<br />
+		<#list groups?sort_by("name") as group>
+			<#if group.supercat == supercat>
+				<@emitGroup group=group/>
+			</#if>
+		</#list>
+	</#list>
+</div>
+
+<@footerInfo />
+<@footerClose />
+
+</div></div>
+
+<?php
+
+    printFooter($module);
+
+?>
\ No newline at end of file

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/debian-med/libbroad-barclay-java.git



More information about the debian-med-commit mailing list