diff --git a/common-test/pom.xml b/common-test/pom.xml index 57f12ebf..c7c3272f 100644 --- a/common-test/pom.xml +++ b/common-test/pom.xml @@ -3,7 +3,7 @@ se.softhouse jargo-parent - 0.4.5-SNAPSHOT + 0.4.15-SNAPSHOT common-test Reusable classes to use from tests. @@ -32,7 +32,6 @@ org.jacoco jacoco-maven-plugin - 0.7.4.201502262128 diff --git a/common-test/src/main/java/se/softhouse/common/testlib/Explanation.java b/common-test/src/main/java/se/softhouse/common/testlib/Explanation.java index 2038818d..9815f737 100644 --- a/common-test/src/main/java/se/softhouse/common/testlib/Explanation.java +++ b/common-test/src/main/java/se/softhouse/common/testlib/Explanation.java @@ -32,4 +32,6 @@ private Explanation() * make the test fail for the wrong reasons. */ public static final String FAIL_FAST = "fail-fast during configuration phase"; + + public static final String TESTING_INVALID_CODE = "Invalid code"; } diff --git a/common-test/src/main/java/se/softhouse/common/testlib/Launcher.java b/common-test/src/main/java/se/softhouse/common/testlib/Launcher.java index 63a10eea..e37f1846 100644 --- a/common-test/src/main/java/se/softhouse/common/testlib/Launcher.java +++ b/common-test/src/main/java/se/softhouse/common/testlib/Launcher.java @@ -16,6 +16,8 @@ import static com.google.common.base.Preconditions.checkNotNull; import static java.lang.reflect.Modifier.isPublic; import static java.lang.reflect.Modifier.isStatic; +import static java.util.Collections.emptyList; +import static java.util.Collections.emptyMap; import java.io.File; import java.io.IOException; @@ -23,8 +25,11 @@ import java.lang.management.RuntimeMXBean; import java.util.Arrays; import java.util.List; +import java.util.Map; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import javax.annotation.concurrent.Immutable; @@ -47,12 +52,14 @@ public static final class LaunchedProgram private final String errors; private final String output; private final String debugInformation; + private final int exitCode; - private LaunchedProgram(String errors, String output, String debugInformation) + private LaunchedProgram(String errors, String output, String debugInformation, int exitCode) { this.errors = errors; this.output = output; this.debugInformation = debugInformation; + this.exitCode = exitCode; } /** @@ -71,6 +78,14 @@ public String errors() return errors; } + /** + * @return the exit code/status of the finished program + */ + public int exitCode() + { + return exitCode; + } + /** * Returns information suitable to print in case of errors on a debug level */ @@ -91,25 +106,16 @@ private Launcher() } /** - * Runs {@code classWithMainMethod} in a separate process using the system property - * {@code java.home} to find java and {@code java.class.path} for the class path. This method - * will wait until program execution has finished. + * Like {@link #launch(Class, String...)} but with more configurable options * - * @param classWithMainMethod a class with a static "main" method - * @param programArguments optional arguments to pass to the program - * @return output/errors from the executed program - * @throws IOException if an I/O error occurs while starting {@code classWithMainMethod} as a - * process - * @throws InterruptedException if the thread starting the program is - * {@link Thread#interrupted()} while waiting for the program to finish - * @throws IllegalArgumentException if {@code classWithMainMethod} doesn't have a public static - * main method - * @throws SecurityException if it's not possible to validate the existence of a main method in - * {@code classWithMainMethod} (or if {@link SecurityManager#checkExec checkExec} - * fails) + * @param envVariables variables to add to {@link ProcessBuilder#environment()}. + * @param additionalVmArgs the vm args to pass in */ - public static LaunchedProgram launch(Class classWithMainMethod, String ... programArguments) throws IOException, InterruptedException + public static LaunchedProgram launch(List additionalVmArgs, Map envVariables, Class classWithMainMethod, + String ... programArguments) throws IOException, InterruptedException { + checkNotNull(additionalVmArgs); + checkNotNull(envVariables); checkNotNull(programArguments); try { @@ -130,11 +136,35 @@ public static LaunchedProgram launch(Class classWithMainMethod, String ... pr List args = Lists.newArrayList(jvm, "-cp", classPath); args.addAll(vmArgs); + args.addAll(additionalVmArgs); args.add(classWithMainMethod.getName()); args.addAll(Arrays.asList(programArguments)); String debugInformation = "\njvm: " + jvm + "\nvm args: " + vmArgs + "\nclasspath: " + classPath; - return execute(args, debugInformation); + return execute(args, envVariables, debugInformation); + } + + /** + * Runs {@code classWithMainMethod} in a separate process using the system property + * {@code java.home} to find java and {@code java.class.path} for the class path. This method + * will wait until program execution has finished. + * + * @param classWithMainMethod a class with a static "main" method + * @param programArguments optional arguments to pass to the program + * @return output/errors from the executed program + * @throws IOException if an I/O error occurs while starting {@code classWithMainMethod} as a + * process + * @throws InterruptedException if the thread starting the program is + * {@link Thread#interrupted()} while waiting for the program to finish + * @throws IllegalArgumentException if {@code classWithMainMethod} doesn't have a public static + * main method + * @throws SecurityException if it's not possible to validate the existence of a main method in + * {@code classWithMainMethod} (or if {@link SecurityManager#checkExec checkExec} + * fails) + */ + public static LaunchedProgram launch(Class classWithMainMethod, String ... programArguments) throws IOException, InterruptedException + { + return launch(emptyList(), emptyMap(), classWithMainMethod, programArguments); } /** @@ -151,20 +181,34 @@ public static LaunchedProgram launch(Class classWithMainMethod, String ... pr */ public static LaunchedProgram launch(String program, String ... programArguments) throws IOException, InterruptedException { - return execute(Lists.asList(program, programArguments), "Failed to launch " + program); + return execute(Lists.asList(program, programArguments), emptyMap(), "Failed to launch " + program); } - private static LaunchedProgram execute(List args, String debugInformation) throws IOException, InterruptedException + /** + * Different JDKs behave differently, to have consistent results, let's clean it away + * No way to turn it off either: https://bugs.openjdk.java.net/browse/JDK-8039152 + */ + private static final Pattern JVM_OUTPUT_CLEANER = Pattern.compile("Picked up _JAVA_OPTIONS.*\n"); + + private static LaunchedProgram execute(List args, Map envVariables, String debugInformation) + throws IOException, InterruptedException { - Process program = new ProcessBuilder().command(args).start(); + ProcessBuilder processBuilder = new ProcessBuilder().command(args); + processBuilder.environment().putAll(envVariables); + Process program = processBuilder.start(); Future stdout = Streams.readAsynchronously(program.getInputStream()); Future stderr = Streams.readAsynchronously(program.getErrorStream()); - program.waitFor(); + int exitCode = program.waitFor(); try { String output = stdout.get(); String errors = stderr.get(); - return new LaunchedProgram(errors, output, debugInformation); + Matcher matcher = JVM_OUTPUT_CLEANER.matcher(errors); + if(matcher.find()) + { + errors = matcher.replaceFirst(""); + } + return new LaunchedProgram(errors, output, debugInformation, exitCode); } catch(ExecutionException e) { diff --git a/common-test/src/test/java/se/softhouse/common/testlib/EnumTesterTest.java b/common-test/src/test/java/se/softhouse/common/testlib/EnumTesterTest.java index 69434a4f..7e951a90 100644 --- a/common-test/src/test/java/se/softhouse/common/testlib/EnumTesterTest.java +++ b/common-test/src/test/java/se/softhouse/common/testlib/EnumTesterTest.java @@ -22,6 +22,8 @@ import com.google.common.testing.NullPointerTester; import com.google.common.testing.NullPointerTester.Visibility; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; + public class EnumTesterTest { @Test @@ -55,11 +57,12 @@ private enum InvalidEnum ONE, TWO; - @Override + @Override public String toString() { return TWO.name(); } + } @Test @@ -78,18 +81,17 @@ public void testThatInvalidToStringIsDetected() } } - private enum InvalidValueOfEnum + private enum InvalidValueOfEnum{ONE,TWO { - ONE, - TWO - { - @Override + + @Override + @SuppressFBWarnings(value = "NP_TOSTRING_COULD_RETURN_NULL", justification = Explanation.TESTING_INVALID_CODE) public String toString() { return null; } - }; - } + + };} @Test public void testThatPackageProtectedValueOfIsNotCallable() throws ClassNotFoundException diff --git a/config/code-formatter.xml b/config/code-formatter.xml index 5380d264..56d2de3f 100644 --- a/config/code-formatter.xml +++ b/config/code-formatter.xml @@ -72,7 +72,7 @@ - + @@ -156,7 +156,7 @@ - + @@ -178,7 +178,7 @@ - + @@ -200,7 +200,7 @@ - + @@ -270,7 +270,7 @@ - + @@ -312,4 +312,5 @@ + diff --git a/config/eclipse.txt b/config/eclipse.txt index cc234964..6cdd6fbf 100644 --- a/config/eclipse.txt +++ b/config/eclipse.txt @@ -1,9 +1,6 @@ -Recommended plugins: -Workspace mechanic (http://code.google.com/a/eclipselabs.org/p/workspacemechanic/) -From Marketplace: -AnyEdit Tools -EclEmma -Maven Integration for eclipse (m2eclipse) +Recommended plugins from Marketplace: +Workspace mechanic +Spotbugs NOTE: As the workspace mechanic tasks modifies workspace global settings it's recommended to develop in a fresh workspace. \ No newline at end of file diff --git a/config/workspace-mechanic-tasks/format-comments.epf b/config/workspace-mechanic-tasks/format-comments.epf new file mode 100644 index 00000000..0d41ee6a --- /dev/null +++ b/config/workspace-mechanic-tasks/format-comments.epf @@ -0,0 +1,34 @@ +# @title Added some formatting settings for comments +# @description Will minimize whitespace diffs +# @task_type LASTMOD +# +# Created by the Workspace Mechanic Preference Recorder +#Thu May 03 07:09:00 CEST 2018 +/instance/org.eclipse.jdt.core/org.eclipse.jdt.core.formatter.parentheses_positions_in_if_while_statement=common_lines +/instance/org.eclipse.jdt.core/org.eclipse.jdt.core.formatter.parentheses_positions_in_method_delcaration=common_lines +/instance/org.eclipse.jdt.core/org.eclipse.jdt.core.formatter.wrap_before_conditional_operator=true +/instance/org.eclipse.jdt.core/org.eclipse.jdt.core.formatter.parentheses_positions_in_method_invocation=common_lines +/instance/org.eclipse.jdt.ui/formatter_settings_version=13 +/instance/org.eclipse.jdt.core/org.eclipse.jdt.core.formatter.comment.line_length=140 +/instance/org.eclipse.jdt.core/org.eclipse.jdt.core.formatter.parentheses_positions_in_catch_clause=common_lines +/instance/org.eclipse.jdt.core/org.eclipse.jdt.core.formatter.parentheses_positions_in_switch_statement=common_lines +/instance/org.eclipse.jdt.core/org.eclipse.jdt.core.formatter.parentheses_positions_in_lambda_declaration=common_lines +/instance/org.eclipse.jdt.core/org.eclipse.jdt.core.formatter.parentheses_positions_in_annotation=common_lines +/instance/org.eclipse.jdt.core/org.eclipse.jdt.core.formatter.wrap_before_assignment_operator=false +/instance/org.eclipse.jdt.core/org.eclipse.jdt.core.formatter.parentheses_positions_in_enum_constant_declaration=common_lines +/instance/org.eclipse.jdt.core/org.eclipse.jdt.core.formatter.insert_new_line_after_type_annotation=do not insert +/instance/org.eclipse.jdt.core/org.eclipse.jdt.core.formatter.parentheses_positions_in_try_clause=common_lines +/instance/org.eclipse.jdt.core/org.eclipse.jdt.core.formatter.alignment_for_expressions_in_for_loop_header=0 +/instance/org.eclipse.jdt.ui/org.eclipse.jdt.ui.formatterprofiles=\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n +/instance/org.eclipse.jdt.core/org.eclipse.jdt.core.formatter.parentheses_positions_in_for_statment=common_lines +/instance/org.eclipse.jdt.core/org.eclipse.jdt.core.formatter.comment.count_line_length_from_starting_position=false +/instance/org.eclipse.jdt.core/org.eclipse.jdt.core.formatter.insert_space_after_lambda_arrow=insert +/instance/org.eclipse.jdt.core/org.eclipse.jdt.core.formatter.brace_position_for_lambda_body=end_of_line +/instance/org.eclipse.jdt.core/org.eclipse.jdt.core.formatter.align_fields_grouping_blank_lines=2147483647 +/instance/org.eclipse.jdt.core/org.eclipse.jdt.core.formatter.alignment_for_parameterized_type_references=0 +/instance/org.eclipse.jdt.core/org.eclipse.jdt.core.formatter.insert_space_before_lambda_arrow=insert +/instance/org.eclipse.jdt.core/org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_enum_constant=insert +/instance/org.eclipse.jdt.core/org.eclipse.jdt.core.formatter.alignment_for_type_arguments=0 +/instance/org.eclipse.jdt.core/org.eclipse.jdt.core.formatter.alignment_for_type_parameters=0 +/instance/org.eclipse.jdt.core/org.eclipse.jdt.core.formatter.alignment_for_module_statements=16 +file_export_version=3.0 diff --git a/jargo-addons/pom.xml b/jargo-addons/pom.xml index 9c05ffdd..39360a7b 100644 --- a/jargo-addons/pom.xml +++ b/jargo-addons/pom.xml @@ -3,7 +3,7 @@ se.softhouse jargo-parent - 0.4.5-SNAPSHOT + 0.4.15-SNAPSHOT jargo-addons Code that could have been added to jargo if it didn't require third party dependencies. @@ -11,7 +11,7 @@ se.softhouse jargo - 0.4.5-SNAPSHOT + 0.4.15-SNAPSHOT joda-time @@ -22,14 +22,14 @@ se.softhouse common-test - 0.4.5-SNAPSHOT + 0.4.15-SNAPSHOT test se.softhouse jargo - 0.4.5-SNAPSHOT + 0.4.15-SNAPSHOT test-jar test diff --git a/jargo/pom.xml b/jargo/pom.xml index 152a9b31..0b1393e0 100644 --- a/jargo/pom.xml +++ b/jargo/pom.xml @@ -3,7 +3,7 @@ se.softhouse jargo-parent - 0.4.5-SNAPSHOT + 0.4.15-SNAPSHOT jargo An argument and options parser for java @@ -63,7 +63,7 @@ se.softhouse common-test - 0.4.5-SNAPSHOT + 0.4.15-SNAPSHOT test diff --git a/jargo/src/main/java/se/softhouse/common/collections/CharacterTrie.java b/jargo/src/main/java/se/softhouse/common/collections/CharacterTrie.java index 30a0a49e..416f74b3 100644 --- a/jargo/src/main/java/se/softhouse/common/collections/CharacterTrie.java +++ b/jargo/src/main/java/se/softhouse/common/collections/CharacterTrie.java @@ -32,12 +32,12 @@ *
  * Stores {@link String}s in a trie.
  * The main purpose when using a structure like this is the methods
- * {@link #findLongestPrefix(CharSequence)} and {@link #getEntriesWithPrefix(CharSequence)}.
+ * {@link #findLongestPrefix(CharSequence)} and {@link #getEntriesWithPrefix(String)}.
  *
  * Neither null keys or null values are allowed because just like the
  * devil, they are evil.
  *
- * If you're iterating over the whole trie more often than you do {@link #getEntriesWithPrefix(CharSequence) simple lookups}
+ * If you're iterating over the whole trie more often than you do {@link #getEntriesWithPrefix(String) simple lookups}
  * you're probably better off using a {@link TreeMap}.
  *
  * TODO(jontejj): Implement SortedMap instead of Map
@@ -283,7 +283,7 @@ private void clear()
 		 *
 		 * @param level the current level we're in (in the current stack)
 		 */
-		private Entry successor(Entry predecessor, CharSequence predecessorKey, int level, boolean isGoingDown)
+		private Entry successor(Entry predecessor, CharSequence predecessorKey, Entry prefixRoot, int level, boolean isGoingDown)
 		{
 			if(isValue && predecessor != this && isGoingDown)
 				return this;
@@ -304,11 +304,11 @@ private Entry successor(Entry predecessor, CharSequence predecessorKey, in
 
 				// Visit the next child
 				if(next != null)
-					return next.getValue().successor(predecessor, predecessorKey, level + 1, true);
+					return next.getValue().successor(predecessor, predecessorKey, prefixRoot, level + 1, true);
 			}
 
-			if(!isRoot()) // Go back up and enter the sibling
-				return parent.successor(predecessor, predecessorKey, level - 1, false);
+			if(this != prefixRoot) // Go back up and enter the sibling
+				return parent.successor(predecessor, predecessorKey, prefixRoot, level - 1, false);
 
 			return null;
 		}
@@ -494,15 +494,14 @@ public Map.Entry findLongestPrefix(final CharSequence prefix)
 	}
 
 	/**
-	 * Returns all entries whose key starts with {@code prefix}. The returned {@link Set} is a view
-	 * so removed elements from it are also removed in this structure.
+	 * Returns all entries whose key starts with {@code prefix}. The returned {@link Map} is an unmodifiable view.
 	 */
-	public Set> getEntriesWithPrefix(final CharSequence prefix)
+	public Map getEntriesWithPrefix(final String prefix)
 	{
 		Entry startingPoint = root.findChild(prefix);
 		if(startingPoint == null)
-			return Collections.emptySet();
-		return new EntrySet(startingPoint);
+			return Collections.emptyMap();
+		return new PrefixMap(startingPoint);
 	}
 
 	/**
@@ -561,9 +560,11 @@ private final class EntryIterator implements Iterator>
 		private int expectedModCount = modCount;
 		private Entry next;
 		private Entry lastReturned = null;
+		private Entry prefixRoot;
 
 		private EntryIterator(Entry startingPoint)
 		{
+			this.prefixRoot = startingPoint;
 			next = startingPoint;
 		}
 
@@ -577,13 +578,13 @@ public boolean hasNext()
 			{
 				if(lastReturned == null)
 				{
-					next = next.successor(lastReturned, "", 0, true);
+					next = next.successor(lastReturned, "", prefixRoot, 0, true);
 				}
 				else
 				{
 					CharSequence lastKey = lastReturned.getKey();
 					int lastDepth = lastKey.length();
-					next = next.successor(lastReturned, lastKey, lastDepth, true);
+					next = next.successor(lastReturned, lastKey, prefixRoot, lastDepth, true);
 				}
 			}
 			return next != null;
@@ -616,4 +617,33 @@ private void verifyUnmodified()
 				throw new ConcurrentModificationException("Trie modified during iteration");
 		}
 	}
+
+	private final class PrefixMap extends AbstractMap
+	{
+		private final CharacterTrie.Entry startingPoint;
+
+		private PrefixMap(CharacterTrie.Entry startingPoint)
+		{
+			this.startingPoint = startingPoint;
+		}
+
+		@Override
+		public int size()
+		{
+			return startingPoint.size();
+		}
+
+		@Override
+		public void clear()
+		{
+			modCount++;
+			startingPoint.clear();
+		}
+
+		@Override
+		public Set> entrySet()
+		{
+			return new EntrySet(startingPoint);
+		}
+	}
 }
diff --git a/jargo/src/main/java/se/softhouse/common/guavaextensions/Functions2.java b/jargo/src/main/java/se/softhouse/common/guavaextensions/Functions2.java
index 53d06370..75d1f671 100644
--- a/jargo/src/main/java/se/softhouse/common/guavaextensions/Functions2.java
+++ b/jargo/src/main/java/se/softhouse/common/guavaextensions/Functions2.java
@@ -12,23 +12,24 @@
  */
 package se.softhouse.common.guavaextensions;
 
-import se.softhouse.common.strings.StringsUtil;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.Objects.requireNonNull;
+import static java.util.stream.Collectors.toList;
+import static se.softhouse.common.guavaextensions.Preconditions2.check;
 
-import javax.annotation.Nonnull;
-import javax.annotation.concurrent.Immutable;
 import java.io.File;
 import java.io.IOException;
+import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.util.Collections;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.function.Function;
 
-import static java.util.Objects.requireNonNull;
-import static java.util.stream.Collectors.toList;
-import static se.softhouse.common.guavaextensions.Preconditions2.check;
-import static se.softhouse.common.strings.StringsUtil.UTF8;
+import javax.annotation.Nonnull;
+import javax.annotation.concurrent.Immutable;
 
 /**
  * Gives you static access to additional implementations of the {@link Function} interface, as a
@@ -148,6 +149,16 @@ public List apply(List value)
 		}
 	}
 
+	/**
+	 * Creates a {@link Function} that wraps {@link Set}s with
+	 * {@link Collections#unmodifiableSet(Set)}
+	 */
+	@Nonnull
+	public static  Function, Set> unmodifiableSet()
+	{
+		return s -> Collections.unmodifiableSet(s);
+	}
+
 	/**
 	 * Creates a {@link Function} that wraps {@link Map}s with
 	 * {@link Collections#unmodifiableMap(Map)}
@@ -213,7 +224,7 @@ public T apply(T input)
 
 	/**
 	 * Returns a {@link Function} that reads whole {@link File}s into {@link String}s using the
-	 * {@link StringsUtil#UTF8 UTF-8} charset.
+	 * {@link StandardCharsets#UTF_8 UTF-8} charset.
 	 */
 	public static Function fileToString()
 	{
@@ -231,7 +242,7 @@ public String apply(@Nonnull File input)
 				throw new IllegalArgumentException(input.getAbsolutePath() + " is a directory, not a file");
 			try
 			{
-				return new String(Files.readAllBytes(input.toPath()), UTF8);
+				return new String(Files.readAllBytes(input.toPath()), UTF_8);
 			}
 			catch(IOException e)
 			{
diff --git a/jargo/src/main/java/se/softhouse/common/guavaextensions/Predicates2.java b/jargo/src/main/java/se/softhouse/common/guavaextensions/Predicates2.java
index 2d7df872..83ecaf2b 100644
--- a/jargo/src/main/java/se/softhouse/common/guavaextensions/Predicates2.java
+++ b/jargo/src/main/java/se/softhouse/common/guavaextensions/Predicates2.java
@@ -49,24 +49,25 @@ enum ObjectPredicates implements Predicate
 		ALWAYS_TRUE((i) -> true),
 		ALWAYS_FALSE((i) -> false);
 
-		private Predicate predicate;
+	private Predicate predicate;
 
-		ObjectPredicates(Predicate predicate)
+	ObjectPredicates(Predicate predicate)
 		{
 			this.predicate = predicate;
 		}
 
-		@Override
+	@Override
 		public boolean test(Object o)
 		{
 			return predicate.test(o);
 		}
 
-		@SuppressWarnings("unchecked") // safe contravariant cast as all ObjectPredicates
+	@SuppressWarnings("unchecked") // safe contravariant cast as all ObjectPredicates
 		 Predicate withNarrowedType()
 		{
 			return (Predicate) this;
 		}
+
 	}
 
 	/**
diff --git a/jargo/src/main/java/se/softhouse/common/guavaextensions/Suppliers2.java b/jargo/src/main/java/se/softhouse/common/guavaextensions/Suppliers2.java
index 34214804..6761088b 100644
--- a/jargo/src/main/java/se/softhouse/common/guavaextensions/Suppliers2.java
+++ b/jargo/src/main/java/se/softhouse/common/guavaextensions/Suppliers2.java
@@ -12,18 +12,23 @@
  */
 package se.softhouse.common.guavaextensions;
 
+import static java.lang.String.format;
 import static java.util.Objects.requireNonNull;
 import static se.softhouse.common.guavaextensions.Preconditions2.check;
 
 import java.io.Serializable;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.function.Function;
+import java.util.function.Predicate;
 import java.util.function.Supplier;
 
 import javax.annotation.CheckReturnValue;
 import javax.annotation.Nullable;
 import javax.annotation.concurrent.Immutable;
 
+import se.softhouse.jargo.internal.Texts.UserErrors;
+
 /**
  * Additional implementations of the {@link Supplier} interface
  */
@@ -100,6 +105,46 @@ public T get()
 		}
 	}
 
+	/**
+	 * @param origin the supplier that provides the original value to check and transform
+	 * @param transformer a function that will be used to transform the origin value to something
+	 *            else
+	 * @param predicate function that will be used to check for correctness
+	 * @return a wrapping {@link Supplier}
+	 */
+	@CheckReturnValue
+	public static  Supplier wrapWithPredicateAndTransform(Supplier origin, Function transformer,
+			Predicate predicate)
+	{
+		return new PredicatedAndTransformedSupplier(origin, transformer, predicate);
+	}
+
+	private static final class PredicatedAndTransformedSupplier implements Supplier
+	{
+		private final Supplier origin;
+		private final Function transformer;
+		private final Predicate predicate;
+
+		private PredicatedAndTransformedSupplier(Supplier origin, Function transformer, Predicate predicate)
+		{
+			this.origin = requireNonNull(origin);
+			this.transformer = requireNonNull(transformer);
+			this.predicate = requireNonNull(predicate);
+		}
+
+		@Override
+		public F get()
+		{
+			T originValue = origin.get();
+			if(!predicate.test(originValue))
+				throw new IllegalArgumentException(format(UserErrors.DISALLOWED_VALUE, originValue, predicate));
+			return transformer.apply(originValue);
+		}
+
+		//
+
+	}
+
 	/**
 	 * Returns true if {@code Supplier} is likely to supply values very fast
 	 */
diff --git a/jargo/src/main/java/se/softhouse/common/numbers/NumberType.java b/jargo/src/main/java/se/softhouse/common/numbers/NumberType.java
index 0ec8fdfa..e9f79e8e 100644
--- a/jargo/src/main/java/se/softhouse/common/numbers/NumberType.java
+++ b/jargo/src/main/java/se/softhouse/common/numbers/NumberType.java
@@ -12,12 +12,12 @@
  */
 package se.softhouse.common.numbers;
 
+import static java.lang.System.lineSeparator;
 import static java.util.Arrays.asList;
 import static java.util.Collections.unmodifiableList;
 import static java.util.Objects.requireNonNull;
 import static se.softhouse.common.strings.Describables.illegalArgument;
 import static se.softhouse.common.strings.Describers.numberDescriber;
-import static se.softhouse.common.strings.StringsUtil.NEWLINE;
 import static se.softhouse.common.strings.StringsUtil.pointingAtIndex;
 
 import java.math.BigDecimal;
@@ -216,7 +216,7 @@ NumberFormat parser(Locale inLocale)
 		return formatter;
 	}
 
-	private static final String TEMPLATE = "'%s' is not a valid %s (Localization: %s)" + NEWLINE + " %s";
+	private static final String TEMPLATE = "'%s' is not a valid %s (Localization: %s)" + lineSeparator() + " %s";
 
 	Describable formatError(Object invalidValue, Locale locale, ParsePosition positionForInvalidCharacter)
 	{
diff --git a/jargo/src/main/java/se/softhouse/common/strings/Describable.java b/jargo/src/main/java/se/softhouse/common/strings/Describable.java
index 466fa154..bfc1601d 100644
--- a/jargo/src/main/java/se/softhouse/common/strings/Describable.java
+++ b/jargo/src/main/java/se/softhouse/common/strings/Describable.java
@@ -20,11 +20,12 @@
  * Allows for lazily created descriptions (i.e {@link String}s) and the possible performance
  * optimization that the string is not constructed if it's not used.
  * If you already have a created {@link String} it's recommended to just use that instead.
- * This interface is typically implemented using an anonymous class.
+ * This interface is typically implemented using a lambda.
  * 
  * @see Describables
  */
 @Immutable
+@FunctionalInterface
 public interface Describable
 {
 	/**
diff --git a/jargo/src/main/java/se/softhouse/common/strings/Describers.java b/jargo/src/main/java/se/softhouse/common/strings/Describers.java
index 48c3bb3f..ceeaccf3 100644
--- a/jargo/src/main/java/se/softhouse/common/strings/Describers.java
+++ b/jargo/src/main/java/se/softhouse/common/strings/Describers.java
@@ -12,9 +12,11 @@
  */
 package se.softhouse.common.strings;
 
-import javax.annotation.CheckReturnValue;
-import javax.annotation.Nonnull;
-import javax.annotation.concurrent.Immutable;
+import static java.lang.System.lineSeparator;
+import static java.util.Collections.unmodifiableMap;
+import static java.util.Objects.requireNonNull;
+import static se.softhouse.common.guavaextensions.Preconditions2.check;
+
 import java.io.File;
 import java.text.NumberFormat;
 import java.util.Iterator;
@@ -24,10 +26,9 @@
 import java.util.Map.Entry;
 import java.util.function.Function;
 
-import static java.util.Collections.unmodifiableMap;
-import static java.util.Objects.requireNonNull;
-import static se.softhouse.common.guavaextensions.Preconditions2.check;
-import static se.softhouse.common.strings.StringsUtil.NEWLINE;
+import javax.annotation.CheckReturnValue;
+import javax.annotation.Nonnull;
+import javax.annotation.concurrent.Immutable;
 
 /**
  * Gives you static access to implementations of the {@link Describer} interface.
@@ -141,22 +142,23 @@ enum BooleanDescribers implements Describer
 	{
 		ENABLED_DISABLED
 		{
-			@Override
-			public String describe(Boolean value, Locale inLocale)
-			{
-				return value ? "enabled" : "disabled";
-			}
-		},
-		ON_OFF
-		{
-			@Override
-			public String describe(Boolean value, Locale inLocale)
-			{
-				return value ? "on" : "off";
-			}
-		};
+
+	@Override
+	public String describe(Boolean value, Locale inLocale)
+	{
+		return value ? "enabled" : "disabled";
+	}
+
+	},ON_OFF{
+
+	@Override
+	public String describe(Boolean value, Locale inLocale)
+	{
+		return value ? "on" : "off";
 	}
 
+	};}
+
 	/**
 	 * Describes {@link Number}s in a {@link Locale} sensitive manner using {@link NumberFormat}.
 	 */
@@ -264,7 +266,7 @@ public String describe(Map values, Locale inLocale)
 				result.append(entry.getValue());
 				String descriptionForEntry = descriptions.get(key);
 				check(descriptionForEntry != null, "Undescribed key: %s", key);
-				result.append(NEWLINE).append(" ").append(descriptionForEntry).append(NEWLINE);
+				result.append(lineSeparator()).append(" ").append(descriptionForEntry).append(lineSeparator());
 			}
 			return result.toString();
 		}
@@ -273,7 +275,7 @@ public String describe(Map values, Locale inLocale)
 	/**
 	 * Describes key values in a {@link Map}. Keys are described with
 	 * {@link Describers#toStringDescriber()} and values with {@code valueDescriber}. "=" is used as
-	 * the separator between key and value. {@link StringsUtil#NEWLINE} separates entries.
+	 * the separator between key and value. {@link System#lineSeparator()} separates entries.
 	 */
 	@CheckReturnValue
 	@Nonnull
@@ -286,7 +288,7 @@ public static  Describer> mapDescriber(Describer valueDescrib
 	 * Describes key values in a {@link Map}. Keys are described with
 	 * {@link Describers#toStringDescriber()} and values with {@code valueDescriber}.
 	 * {@code valueSeparator} is used as the separator between key and value.
-	 * {@link StringsUtil#NEWLINE} separates entries.
+	 * {@link System#lineSeparator()} separates entries.
 	 */
 	@CheckReturnValue
 	@Nonnull
@@ -298,7 +300,7 @@ public static  Describer> mapDescriber(Describer valueDescrib
 	/**
 	 * Describes key values in a {@link Map}. Keys are described with {@code keyDescriber} and
 	 * values with {@code valueDescriber}.
-	 * "=" is used as the separator between key and value. {@link StringsUtil#NEWLINE} separates
+	 * "=" is used as the separator between key and value. {@link System#lineSeparator()} separates
 	 * entries.
 	 */
 	@CheckReturnValue
@@ -311,7 +313,7 @@ public static  Describer> mapDescriber(Describer keyDescriber
 	/**
 	 * Describes key values in a {@link Map}. Keys are described with {@code keyDescriber} and
 	 * values with {@code valueDescriber}. {@code valueSeparator} is used as the separator between
-	 * key and value. {@link StringsUtil#NEWLINE} separates entries.
+	 * key and value. {@link System#lineSeparator()} separates entries.
 	 */
 	@CheckReturnValue
 	@Nonnull
@@ -350,7 +352,7 @@ public String describe(Map values, Locale inLocale)
 
 			while(iterator.hasNext())
 			{
-				result.append(NEWLINE);
+				result.append(lineSeparator());
 				describeEntry(iterator.next(), inLocale, result);
 			}
 			return result.toString();
diff --git a/jargo/src/main/java/se/softhouse/common/strings/Lines.java b/jargo/src/main/java/se/softhouse/common/strings/Lines.java
index 7e39169e..221e888a 100644
--- a/jargo/src/main/java/se/softhouse/common/strings/Lines.java
+++ b/jargo/src/main/java/se/softhouse/common/strings/Lines.java
@@ -12,7 +12,7 @@
  */
 package se.softhouse.common.strings;
 
-import static se.softhouse.common.strings.StringsUtil.NEWLINE;
+import static java.lang.System.lineSeparator;
 
 import java.text.BreakIterator;
 import java.util.Locale;
@@ -20,7 +20,7 @@
 import javax.annotation.concurrent.Immutable;
 
 /**
- * Utilities for working with {@link StringsUtil#NEWLINE new line} in texts.
+ * Utilities for working with {@link System#lineSeparator() new line} in texts.
  */
 @Immutable
 public final class Lines
@@ -34,7 +34,7 @@ private Lines()
 	 * href="http://docs.oracle.com/javase/tutorial/i18n/text/line.html">appropriate (as defined
 	 * by {@code locale}).
 	 * 
-	 * @param value the value to separate with {@link StringsUtil#NEWLINE new lines}
+	 * @param value the value to separate with {@link System#lineSeparator() new lines}
 	 * @param maxLineLength how long each line are allowed to be
 	 */
 	public static StringBuilder wrap(CharSequence value, int maxLineLength, Locale locale)
@@ -47,7 +47,7 @@ public static StringBuilder wrap(CharSequence value, int maxLineLength, Locale l
 	 * href="http://docs.oracle.com/javase/tutorial/i18n/text/line.html">appropriate (as defined
 	 * by {@code locale}).
 	 * 
-	 * @param value the value to separate with {@link StringsUtil#NEWLINE new lines}
+	 * @param value the value to separate with {@link System#lineSeparator() new lines}
 	 * @param startingIndex the index where each line starts, useful for a fixed-size table for
 	 *            instance
 	 * @param maxLineLength how long each line are allowed to be
@@ -69,7 +69,7 @@ public static StringBuilder wrap(CharSequence value, int startingIndex, int maxL
 			lineLength = lineLength + word.length();
 			if(lineLength >= maxLineLength)
 			{
-				result.append(NEWLINE);
+				result.append(lineSeparator());
 				lineLength = startingIndex;
 			}
 			result.append(word);
diff --git a/jargo/src/main/java/se/softhouse/common/strings/StringsUtil.java b/jargo/src/main/java/se/softhouse/common/strings/StringsUtil.java
index 540d4632..1b0758d2 100644
--- a/jargo/src/main/java/se/softhouse/common/strings/StringsUtil.java
+++ b/jargo/src/main/java/se/softhouse/common/strings/StringsUtil.java
@@ -12,19 +12,23 @@
  */
 package se.softhouse.common.strings;
 
-import javax.annotation.CheckReturnValue;
-import javax.annotation.Nonnull;
-import javax.annotation.concurrent.Immutable;
-import java.nio.charset.Charset;
+import static java.util.Objects.requireNonNull;
+import static java.util.stream.IntStream.of;
+import static se.softhouse.common.guavaextensions.Lists2.isEmpty;
+import static se.softhouse.common.guavaextensions.Preconditions2.check;
+
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
+import java.util.Locale;
+import java.util.SortedSet;
+import java.util.TreeSet;
 import java.util.stream.Collectors;
 
-import static java.util.Objects.requireNonNull;
-import static java.util.stream.IntStream.of;
-import static se.softhouse.common.guavaextensions.Lists2.isEmpty;
-import static se.softhouse.common.guavaextensions.Preconditions2.check;
+import javax.annotation.CheckReturnValue;
+import javax.annotation.Nonnull;
+import javax.annotation.concurrent.Immutable;
 
 /**
  * Utilities for working with {@link String}s
@@ -36,40 +40,17 @@ private StringsUtil()
 	{
 	}
 
-	/**
-	 * A suitable string to represent newlines on this specific platform
-	 */
-	public static final String NEWLINE = System.getProperty("line.separator");
-
-	/**
-	 * A {@link Charset} for UTF8
-	 */
-	public static final Charset UTF8 = Charset.forName("UTF-8");
-
 	/**
 	 * The ASCII tab (\t) character
 	 */
 	public static final char TAB = '\t';
 
-	/**
-	 * @param numberOfSpaces to put in the created string
-	 * @return a string with numberOfSpaces in it
-	 * @deprecated use {@link #repeat(String, int)} instead
-	 */
-	@Nonnull
-	@CheckReturnValue
-	@Deprecated
-	public static String spaces(final int numberOfSpaces)
-	{
-		return repeat(" ", numberOfSpaces);
-	}
-
 	/**
 	 * Returns a " ^" string pointing at the position indicated by {@code indexToPointAt}
 	 */
 	public static String pointingAtIndex(int indexToPointAt)
 	{
-		return spaces(indexToPointAt) + "^";
+		return repeat(" ", indexToPointAt) + "^";
 	}
 
 	/**
@@ -160,6 +141,50 @@ public static List closestMatches(final String input, final Iterable prefixes(final String partOfWord, final Collection validOptions)
+	{
+		requireNonNull(partOfWord);
+		return validOptions.stream().filter(a -> a.startsWith(partOfWord)).collect(Collectors.toCollection(TreeSet::new));
+	}
+
+	/**
+	 * Like {@link #prefixes(String, Collection)} but ignoring the case of partOfWord.
+	 * This method is also easier to use if you have many valid options and need to lazily generate the options
+	 * ({@link Iterable} is easier to implement than {@link Collection}).
+	 * 
+	 * @param locale the locale to use for converting the {@code options / partOfWord} into lower case
+	 * @return the matches found, with the case from {@code partOfWord} preserved
+	 */
+	public static SortedSet prefixesIgnoringCase(final String partOfWord, final Iterable validOptions, Locale locale)
+	{
+		requireNonNull(partOfWord);
+		requireNonNull(locale);
+		TreeSet matches = new TreeSet<>();
+		for(String option : validOptions)
+		{
+			if(option.startsWith(partOfWord))
+			{
+				matches.add(option);
+			}
+			else
+			{
+				String optionInLowerCase = option.toLowerCase(locale);
+				if(optionInLowerCase.startsWith(partOfWord.toLowerCase(locale)))
+				{
+					matches.add(partOfWord + optionInLowerCase.substring(partOfWord.length()));
+				}
+			}
+		}
+		return matches;
+	}
+
 	static final class CloseMatch
 	{
 		private final int measuredDistance;
@@ -321,7 +346,9 @@ public static String repeat(String part, int times)
 		check(times >= 0, "Negative repitions is not supported. Was: ", times);
 		StringBuilder builder = new StringBuilder(part.length() * times);
 		for(int i = 0; i < times; i++)
+		{
 			builder.append(part);
+		}
 		return builder.toString();
 	}
 }
diff --git a/jargo/src/main/java/se/softhouse/jargo/Argument.java b/jargo/src/main/java/se/softhouse/jargo/Argument.java
index 5013eb94..027284a5 100644
--- a/jargo/src/main/java/se/softhouse/jargo/Argument.java
+++ b/jargo/src/main/java/se/softhouse/jargo/Argument.java
@@ -12,18 +12,12 @@
  */
 package se.softhouse.jargo;
 
-import se.softhouse.common.guavaextensions.Suppliers2;
-import se.softhouse.common.strings.Describable;
-import se.softhouse.common.strings.Describer;
-import se.softhouse.jargo.StringParsers.HelpParser;
-import se.softhouse.jargo.StringParsers.InternalStringParser;
-import se.softhouse.jargo.internal.Texts.ProgrammaticErrors;
-import se.softhouse.jargo.internal.Texts.UserErrors;
+import static java.util.Arrays.asList;
+import static se.softhouse.common.guavaextensions.Predicates2.alwaysTrue;
+import static se.softhouse.common.strings.Describables.format;
+import static se.softhouse.jargo.ArgumentExceptions.withMessage;
+import static se.softhouse.jargo.CommandLineParser.STANDARD_COMPLETER;
 
-import javax.annotation.CheckReturnValue;
-import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
-import javax.annotation.concurrent.Immutable;
 import java.text.CollationKey;
 import java.text.Collator;
 import java.util.Arrays;
@@ -31,14 +25,23 @@
 import java.util.List;
 import java.util.Locale;
 import java.util.Optional;
+import java.util.Set;
 import java.util.function.Function;
 import java.util.function.Predicate;
 import java.util.function.Supplier;
 
-import static java.util.Arrays.asList;
-import static se.softhouse.common.guavaextensions.Predicates2.alwaysTrue;
-import static se.softhouse.common.strings.Describables.format;
-import static se.softhouse.jargo.ArgumentExceptions.withMessage;
+import javax.annotation.CheckReturnValue;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+import se.softhouse.common.guavaextensions.Suppliers2;
+import se.softhouse.common.strings.Describable;
+import se.softhouse.common.strings.Describer;
+import se.softhouse.jargo.StringParsers.HelpParser;
+import se.softhouse.jargo.StringParsers.InternalStringParser;
+import se.softhouse.jargo.internal.Texts.ProgrammaticErrors;
+import se.softhouse.jargo.internal.Texts.UserErrors;
 
 /**
  * 
@@ -67,6 +70,7 @@ public final class Argument
 	@Nonnull private final Supplier defaultValue;
 	@Nullable private final Describer defaultValueDescriber;
 	@Nonnull private final Predicate limiter;
+	@Nullable private final Function> completer;
 
 	// Internal bookkeeping
 	@Nonnull private final Function finalizer;
@@ -98,14 +102,8 @@ public final class Argument
 
 		this.finalizer = builder.finalizer();
 		this.limiter = builder.limiter();
-		if(builder.defaultValueSupplier() != null)
-		{
-			this.defaultValue = builder.defaultValueSupplier();
-		}
-		else
-		{
-			this.defaultValue = (Supplier) parser::defaultValue;
-		}
+		this.defaultValue = builder.defaultValueSupplierOrFromParser();
+		this.completer = builder.completer();
 
 		// Fail-fast for invalid default values that are created already
 		if(Suppliers2.isSuppliedAlready(defaultValue))
@@ -304,7 +302,7 @@ private CommandLineParserInstance commandLineParser()
 	{
 		// Not cached to save memory, users should use CommandLineParser.withArguments if they are
 		// concerned about reuse
-		return new CommandLineParserInstance(Arrays.>asList(Argument.this));
+		return new CommandLineParserInstance(Arrays.>asList(Argument.this), STANDARD_COMPLETER);
 	}
 
 	/**
@@ -332,9 +330,25 @@ enum ParameterArity
 
 	static final Predicate> IS_VISIBLE = input -> !input.hideFromUsage;
 
+	static final Predicate> IS_REPEATED = input -> input.isAllowedToRepeat;
+
 	static final Predicate> IS_OF_VARIABLE_ARITY = input -> input.parser().parameterArity() == ParameterArity.VARIABLE_AMOUNT;
 
 	// TODO(jontejj): replace this with a comparator that uses the Usage.locale instead of
 	// Locale.ROOT?
 	static final Comparator> NAME_COMPARATOR = (lhs, rhs) -> lhs.sortingKey.compareTo(rhs.sortingKey);
+
+	@CheckReturnValue
+	Iterable complete(String partOfWord, ArgumentIterator iterator)
+	{
+		if(!separator.equals(ArgumentBuilder.DEFAULT_SEPARATOR))
+		{
+			// Remove "-D" from "-Dkey=value"
+			partOfWord = partOfWord.substring(iterator.getCurrentArgumentName().length());
+		}
+
+		if(completer != null)
+			return completer.apply(partOfWord);
+		return parser().complete(this, partOfWord, iterator);
+	}
 }
diff --git a/jargo/src/main/java/se/softhouse/jargo/ArgumentBuilder.java b/jargo/src/main/java/se/softhouse/jargo/ArgumentBuilder.java
index 12dea4df..b92b4c54 100644
--- a/jargo/src/main/java/se/softhouse/jargo/ArgumentBuilder.java
+++ b/jargo/src/main/java/se/softhouse/jargo/ArgumentBuilder.java
@@ -27,11 +27,13 @@
 import static se.softhouse.jargo.StringParsers.stringParser;
 
 import java.util.Arrays;
+import java.util.HashSet;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Optional;
+import java.util.Set;
 import java.util.StringJoiner;
 import java.util.function.Function;
 import java.util.function.Predicate;
@@ -48,8 +50,10 @@
 import se.softhouse.common.guavaextensions.Predicates2;
 import se.softhouse.common.guavaextensions.Suppliers2;
 import se.softhouse.common.strings.Describable;
+import se.softhouse.common.strings.Describables;
 import se.softhouse.common.strings.Describer;
 import se.softhouse.common.strings.Describers;
+import se.softhouse.common.strings.StringsUtil;
 import se.softhouse.jargo.ForwardingStringParser.SimpleForwardingStringParser;
 import se.softhouse.jargo.StringParsers.FixedArityParser;
 import se.softhouse.jargo.StringParsers.InternalStringParser;
@@ -57,6 +61,7 @@
 import se.softhouse.jargo.StringParsers.RepeatedArgumentParser;
 import se.softhouse.jargo.StringParsers.StringParserBridge;
 import se.softhouse.jargo.StringParsers.StringSplitterParser;
+import se.softhouse.jargo.StringParsers.TransformingParser;
 import se.softhouse.jargo.StringParsers.VariableArityParser;
 import se.softhouse.jargo.internal.Texts.ProgrammaticErrors;
 import se.softhouse.jargo.internal.Texts.UserErrors;
@@ -96,6 +101,7 @@ public abstract class ArgumentBuilder, T>
 	private boolean isAllowedToRepeat = false;
 	@Nonnull private Optional metaDescription = Optional.empty();
 	private boolean hideFromUsage = false;
+	@Nullable private Function> completer;
 
 	private boolean isPropertyMap = false;
 
@@ -130,7 +136,7 @@ protected ArgumentBuilder()
 		this(null);
 	}
 
-	ArgumentBuilder(final InternalStringParser stringParser)
+	ArgumentBuilder(@Nullable final InternalStringParser stringParser)
 	{
 		this.internalStringParser = stringParser;
 		myself = self();
@@ -437,6 +443,7 @@ public final SELF metaDescription(final String aMetaDescription)
 	 * Hides this argument so that it's not displayed in the usage texts.
* It's recommended that hidden arguments have a reasonable {@link #defaultValue(Object)} and * aren't {@link #required()}, in fact this is recommended for all arguments. + * TODO(jontejj): hidden args should not be completable with {@link CommandLineParser#completer(Completer)}? * * @return this builder */ @@ -470,14 +477,18 @@ public SELF separator(final String aSeparator) * message of that exception will be used by {@link ArgumentException#getMessageAndUsage()}. * When this is needed it's generally recommended to write a parser of its own instead. * - * Note:{@link Object#toString() toString()} on {@code aLimiter} will replace {@link StringParser#descriptionOfValidValues(Locale)} in the usage + * Descriptions:{@link Object#toString() toString()} on {@code aLimiter} will replace {@link StringParser#descriptionOfValidValues(Locale)} in the usage. + * A simpler alternative to provide a description is to use {@link #limitTo(Predicate, String)}. * - * Note:The validity of any {@link #defaultValueSupplier(Supplier) default value} isn't checked until + * Supplied default values:The validity of any {@link #defaultValueSupplier(Supplier) default value} isn't checked until * it's actually needed when {@link ParsedArguments#get(Argument)} is called. This is so * because {@link Supplier#get()} (or {@link StringParser#defaultValue()}) could take an arbitrary long time. * - * Note:Any previously set limiter will be {@link Predicates2#and(Predicate, Predicate) + * Many limiters:Any previously set limiter will be {@link Predicates2#and(Predicate, Predicate) * and'ed} together with {@code aLimiter}. + * + * Interopability with completers Values suggested with any {@link #completer(Function)} will not be limited automatically. + * So before generating the suggestions, you should make sure that they are confirming to your limiter. *
* * @param aLimiter a limiter @@ -485,7 +496,66 @@ public SELF separator(final String aSeparator) */ public SELF limitTo(Predicate aLimiter) { - limiter = Predicates2.and(limiter, aLimiter); + limiter = Predicates2.and(limiter, aLimiter); + return myself; + } + + /** + * Like {@link #limitTo(Predicate)} but with {@code aDescription} to describe the valid values this limiter accepts + * + * @return this builder + */ + public SELF limitTo(Predicate aLimiter, String aDescription) + { + return limitTo(aLimiter, Describables.withString(aDescription)); + } + + /** + * Like {@link #limitTo(Predicate, String)} but allows for lazy construction of the description + */ + public SELF limitTo(Predicate aLimiter, Describable aDescription) + { + requireNonNull(aLimiter); + requireNonNull(aDescription); + Predicate withDescription = new Predicate(){ + + @Override + public boolean test(T t) + { + return aLimiter.test(t); + } + + @Override + public String toString() + { + return aDescription.description(); + } + }; + limitTo(withDescription); + return myself; + } + + /** + * Can be used to complete input values from a dynamic (or static) source. For example + * + *
+	 * 
+	 *
+	 * stringArgument("-u", "--username").completer((str) {@literal ->} StringsUtil.prefixes(str, service.users()));
+	 * 
+	 * 
+ * + * Where {@code service.users()} would return the name of all your users. + * {@link StringsUtil#prefixes(String, java.util.Collection)} is a convenient friend method to easily filter + * out usernames that does not match {@code str} (in this case). + * + * @param aCompleter function that takes in a part of a parameter and returns + * a set of strings that would make this parameter a valid one + */ + public SELF completer(Function> aCompleter) + { + requireNonNull(aCompleter); + this.completer = aCompleter; return myself; } @@ -670,6 +740,26 @@ public RepeatedArgumentBuilder repeated() return new RepeatedArgumentBuilder(this); } + /** + * Makes it possible to chain together different transformation / map / conversion operations + * + *
+	 * 
+	 * int size = Arguments.stringArgument("--foo").transform(String::length).parse("--foo", "abcd");
+	 * assertThat(size).isEqualTo(4);
+	 * 
+	 * 
+ * + * @param transformer the function that takes a value of the previous type (like String in the + * example), and converts it into another type of value + * @return a new (more specific) builder + */ + @CheckReturnValue + public TransformingArgumentBuilder transform(Function transformer) + { + return new TransformingArgumentBuilder(this, transformer); + } + @Override public String toString() { @@ -701,6 +791,8 @@ void copy(final ArgumentBuilder copy) this.isAllowedToRepeat = copy.isAllowedToRepeat; this.metaDescription = copy.metaDescription; this.hideFromUsage = copy.hideFromUsage; + this.completer = copy.completer; + this.isPropertyMap = copy.isPropertyMap; } /** @@ -772,7 +864,19 @@ private void checkNoSpaces(Iterable argumentNames) check(!name.contains(" "), "Detected a space in %s, argument names must not have spaces in them", name); } } - + + @Nonnull Supplier defaultValueSupplierOrFromParser() + { + if(defaultValueSupplier != null) + return defaultValueSupplier; + return (Supplier) internalParser()::defaultValue; + } + + @Nullable Function> completer() + { + return completer; + } + /** * @formatter.on */ @@ -942,9 +1046,9 @@ public ArityArgumentBuilder variableArity() } } - private static class ListArgumentBuilder, T> extends InternalArgumentBuilder> + public static class ListArgumentBuilder, T> extends InternalArgumentBuilder> { - ListArgumentBuilder(InternalStringParser> parser) + private ListArgumentBuilder(InternalStringParser> parser) { super(parser); } @@ -963,6 +1067,17 @@ void copyAsListBuilder(ArgumentBuilder builder, int nrOfElementsInDefaultV defaultValueDescriber(Describers.listDescriber(builder.defaultValueDescriber)); } } + + /** + * Transforms this argument from a {@link List} to a {@link Set}. Thereby removing any duplicate values, given that + * {@link Object#equals(Object)} and {@link Object#hashCode()} has been implemented correctly by the element type. + * + * @return a {@link se.softhouse.jargo.ArgumentBuilder.TransformingArgumentBuilder} that you can continue to configure + */ + public TransformingArgumentBuilder> unique() + { + return transform(list -> (Set) new HashSet<>(list)).finalizeWith(Functions2.unmodifiableSet()); + } } /** @@ -1152,6 +1267,8 @@ public SplitterArgumentBuilder splitWith(final String valueSeparator) @NotThreadSafe public static final class MapArgumentBuilder extends InternalArgumentBuilder, Map> { + static final String DEFAULT_KV_SEPARATOR = "="; + private final ArgumentBuilder valueBuilder; private final StringParser keyParser; @@ -1173,7 +1290,7 @@ InternalStringParser> internalParser() check(names().size() > 0, ProgrammaticErrors.NO_NAME_FOR_PROPERTY_MAP); if(separator().equals(DEFAULT_SEPARATOR)) { - separator("="); + separator(DEFAULT_KV_SEPARATOR); } else { @@ -1289,4 +1406,48 @@ public ArityArgumentBuilder> variableArity() throw new IllegalStateException("You can't use both splitWith and variableArity"); } } + + /** + * An intermediate builder used by {@link #transform(Function)}. It's used to switch the + * {@code } + * argument of the previous builder to {@code } and to indicate invalid call orders. + * + * @param The new type + */ + @NotThreadSafe + public static final class TransformingArgumentBuilder extends InternalArgumentBuilder, F> + { + private TransformingArgumentBuilder(final ArgumentBuilder builder, final Function transformer) + { + super(new TransformingParser(builder.internalParser(), transformer, builder.limiter())); + copy(builder); + + Supplier defaultValueSupplier = builder.defaultValueSupplierOrFromParser(); + defaultValueSupplier(Suppliers2.wrapWithPredicateAndTransform(defaultValueSupplier, transformer, builder.limiter())); + + if(builder.defaultValueDescriber() != null) + { + defaultValueDescriber(new BeforeTransformationDescriber<>(defaultValueSupplier, builder.defaultValueDescriber())); + } + } + } + + private static final class BeforeTransformationDescriber implements Describer + { + private final Supplier valueProvider; + private final Describer beforeDescriber; + + BeforeTransformationDescriber(Supplier valueProvider, Describer beforeDescriber) + { + this.valueProvider = requireNonNull(valueProvider); + this.beforeDescriber = requireNonNull(beforeDescriber); + } + + @Override + public String describe(Object value, Locale inLocale) + { + F beforeValue = valueProvider.get(); + return beforeDescriber.apply(beforeValue); + } + } } diff --git a/jargo/src/main/java/se/softhouse/jargo/ArgumentException.java b/jargo/src/main/java/se/softhouse/jargo/ArgumentException.java index 756cbeda..04a0c9a6 100644 --- a/jargo/src/main/java/se/softhouse/jargo/ArgumentException.java +++ b/jargo/src/main/java/se/softhouse/jargo/ArgumentException.java @@ -12,8 +12,8 @@ */ package se.softhouse.jargo; +import static java.lang.System.lineSeparator; import static java.util.Objects.requireNonNull; -import static se.softhouse.common.strings.StringsUtil.NEWLINE; import java.io.Serializable; @@ -84,7 +84,7 @@ public final Usage getMessageAndUsage() { // TODO(jontejj): jack into the uncaughtExceptionHandler and remove stacktraces? Potentially // very annoying feature... - String message = getMessage(usedArgumentName) + usageReference() + NEWLINE + NEWLINE; + String message = getMessage(usedArgumentName) + usageReference() + lineSeparator() + lineSeparator(); return getUsage().withErrorMessage(message); } @@ -157,7 +157,7 @@ ArgumentException withUsageReference(final Argument usageReference) private String usageReference() { - return usageReferenceName; + return usageReferenceName != null ? usageReferenceName : ""; } /** diff --git a/jargo/src/main/java/se/softhouse/jargo/ArgumentExceptions.java b/jargo/src/main/java/se/softhouse/jargo/ArgumentExceptions.java index aafe16a0..bb3f8aa3 100644 --- a/jargo/src/main/java/se/softhouse/jargo/ArgumentExceptions.java +++ b/jargo/src/main/java/se/softhouse/jargo/ArgumentExceptions.java @@ -12,11 +12,16 @@ */ package se.softhouse.jargo; +import static java.lang.System.lineSeparator; import static java.util.Objects.requireNonNull; +import static se.softhouse.common.strings.Describables.asSerializable; +import static se.softhouse.common.strings.Describables.cache; +import static se.softhouse.common.strings.StringsUtil.TAB; import static se.softhouse.common.strings.StringsUtil.numberToPositionalString; import java.io.Serializable; import java.util.Collection; +import java.util.List; import javax.annotation.CheckReturnValue; import javax.annotation.Nonnull; @@ -26,7 +31,6 @@ import se.softhouse.common.strings.Describable; import se.softhouse.common.strings.Describables; import se.softhouse.common.strings.Describables.SerializableDescription; -import se.softhouse.jargo.CommandLineParserInstance.ArgumentIterator; import se.softhouse.jargo.internal.Texts.UserErrors; /** @@ -150,21 +154,23 @@ static ArgumentException forMissingArguments(final Collection> missi return new MissingRequiredArgumentException(missingArguments); } - private static final class MissingRequiredArgumentException extends ArgumentException + static final class MissingRequiredArgumentException extends ArgumentException { - private final String missingArguments; + private final transient Collection> missingArguments; + private final SerializableDescription missingArgs; private MissingRequiredArgumentException(final Collection> missingArguments) { - this.missingArguments = missingArguments.toString(); + this.missingArguments = requireNonNull(missingArguments); + this.missingArgs = asSerializable(cache(() -> missingArguments.toString())); } @Override protected String getMessage(String argumentNameOrcommandName) { if(isCausedByCommand(argumentNameOrcommandName)) - return String.format(UserErrors.MISSING_COMMAND_ARGUMENTS, argumentNameOrcommandName, missingArguments); - return String.format(UserErrors.MISSING_REQUIRED_ARGUMENTS, missingArguments); + return String.format(UserErrors.MISSING_COMMAND_ARGUMENTS, argumentNameOrcommandName, missingArgs.description()); + return String.format(UserErrors.MISSING_REQUIRED_ARGUMENTS, missingArgs.description()); } private boolean isCausedByCommand(@Nullable String argumentNameOrcommandName) @@ -172,10 +178,15 @@ private boolean isCausedByCommand(@Nullable String argumentNameOrcommandName) return argumentNameOrcommandName != null; } + public Collection> missingArguments() + { + return missingArguments; + } + /** * For {@link Serializable} */ - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 2L; } /** @@ -208,30 +219,37 @@ static ArgumentException forMissingParameter(Argument argumentWithMissingPara return new MissingParameterException(argumentWithMissingParameter); } - static final class MissingParameterException extends ArgumentException + static class MissingParameterException extends ArgumentException { - private final String parameterDescription; + private final SerializableDescription parameter; + private final transient Argument argumentWithMissingParameter; - private MissingParameterException(Argument argumentWithMissingParameter) + private MissingParameterException(Argument arg) { - this.parameterDescription = argumentWithMissingParameter.metaDescriptionInRightColumn(); + this.argumentWithMissingParameter = requireNonNull(arg); + this.parameter = asSerializable(cache(() -> arg.metaDescriptionInRightColumn())); } @Override protected String getMessage(String argumentNameOrcommandName) { - return String.format(UserErrors.MISSING_PARAMETER, parameterDescription, argumentNameOrcommandName); + return String.format(UserErrors.MISSING_PARAMETER, parameterDescription(), argumentNameOrcommandName); } String parameterDescription() { - return parameterDescription; + return parameter.description(); + } + + Argument argumentWithMissingParameter() + { + return argumentWithMissingParameter; } /** * For {@link Serializable} */ - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 2L; } /** @@ -248,31 +266,30 @@ String parameterDescription() @Nonnull static ArgumentException forMissingNthParameter(MissingParameterException cause, int missingIndex) { - return new MissingNthParameterException(cause.parameterDescription(), missingIndex).andCause(cause); + return new MissingNthParameterException(cause.argumentWithMissingParameter, missingIndex).andCause(cause); } - private static final class MissingNthParameterException extends ArgumentException + private static final class MissingNthParameterException extends MissingParameterException { - private final String parameterDescription; private final int missingIndex; - private MissingNthParameterException(String parameterDescription, int missingIndex) + private MissingNthParameterException(Argument arg, int missingIndex) { - this.parameterDescription = parameterDescription; + super(arg); this.missingIndex = missingIndex; } @Override protected String getMessage(String argumentNameOrCommandName) { - return String.format( UserErrors.MISSING_NTH_PARAMETER, numberToPositionalString(missingIndex + 1), parameterDescription, + return String.format( UserErrors.MISSING_NTH_PARAMETER, numberToPositionalString(missingIndex + 1), parameterDescription(), argumentNameOrCommandName); } /** * For {@link Serializable} */ - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 2L; } /** @@ -324,4 +341,42 @@ protected String getMessage(String argumentNameOrcommandName) */ private static final long serialVersionUID = 1L; } + + /** + * See {@link UserErrors#SUGGESTION} + */ + @CheckReturnValue + @Nonnull + static ArgumentException withSuggestions(String wrongArgument, List suggestions) + { + return new SuggestiveArgumentException(wrongArgument, suggestions); + } + + static final class SuggestiveArgumentException extends ArgumentException + { + private final String wrongArgument; + private final List suggestions; + + private SuggestiveArgumentException(String wrongArgument, List suggestions) + { + this.wrongArgument = requireNonNull(wrongArgument); + this.suggestions = requireNonNull(suggestions); + } + + @Override + protected String getMessage(String argumentNameOrCommandName) + { + return String.format(UserErrors.SUGGESTION, wrongArgument, String.join(lineSeparator() + TAB, suggestions)); + } + + public List suggestions() + { + return suggestions; + } + + /** + * For {@link Serializable} + */ + private static final long serialVersionUID = 1L; + } } diff --git a/jargo/src/main/java/se/softhouse/jargo/ArgumentIterator.java b/jargo/src/main/java/se/softhouse/jargo/ArgumentIterator.java new file mode 100644 index 00000000..156765ae --- /dev/null +++ b/jargo/src/main/java/se/softhouse/jargo/ArgumentIterator.java @@ -0,0 +1,369 @@ +/* Copyright 2018 jonatanjonsson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package se.softhouse.jargo; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.Collections.emptyMap; +import static java.util.Objects.requireNonNull; +import static se.softhouse.common.guavaextensions.Preconditions2.checkNulls; +import static se.softhouse.jargo.ArgumentExceptions.withMessage; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import javax.annotation.concurrent.NotThreadSafe; +import javax.annotation.concurrent.ThreadSafe; + +import se.softhouse.jargo.CommandLineParserInstance.FoundArgumentHandler; +import se.softhouse.jargo.internal.Texts.UsageTexts; + +/** + * Wraps a list of given arguments and remembers + * which argument that is currently being parsed. Plays a key role in making + * {@link CommandLineParserInstance} {@link ThreadSafe} as it holds the current state of a parse + * invocation. Internal class. + */ +@NotThreadSafe +final class ArgumentIterator implements Iterator +{ + private final List arguments; + + /** + * Stored here to allow help arguments to be preferred over indexed arguments + */ + private final Map> helpArguments; + + /** + * Corresponds to one of the {@link Argument#names()} that has been given from the command + * line. This is updated as soon as the parsing of a new argument begins. + * For indexed arguments this will be the meta description instead. + */ + private String currentArgumentName; + private int currentArgumentIndex; + private boolean endOfOptionsReceived; + + private final LinkedList commandInvocations = new LinkedList<>(); + + /** + * Allows commands to rejoin parsing of arguments if main arguments are specified in-between + */ + boolean temporaryRepitionAllowedForCommand; + + /** + * Hints that the word to complete was not actually given yet + */ + boolean isCompletingGeneratedSuggestion; + + private ParsedArguments currentHolder; + + private FoundArgumentHandler handler; + + /** + * Arguments have been manipulated and not yet consumed + */ + private boolean dirty; + + /** + * @param actualArguments a list of arguments, will be modified + */ + private ArgumentIterator(Iterable actualArguments, Map> helpArguments, FoundArgumentHandler handler) + { + this.arguments = checkNulls(actualArguments, "Argument strings may not be null"); + this.helpArguments = requireNonNull(helpArguments); + this.handler = requireNonNull(handler); + } + + Argument helpArgument(String currentArgument) + { + return helpArguments.get(currentArgument); + } + + ParsedArguments currentHolder() + { + return currentHolder; + } + + /** + * Returns true if {@link UsageTexts#END_OF_OPTIONS} hasn't been received yet. + */ + boolean allowsOptions() + { + return !endOfOptionsReceived; + } + + void setCurrentHolder(ParsedArguments currentHolder) + { + this.currentHolder = currentHolder; + } + + static final class CommandInvocation + { + final Command command; + final ParsedArguments args; + final Argument argumentSettingsForInvokedCommand; + + CommandInvocation(Command command, ParsedArguments args, Argument argumentSettingsForInvokedCommand) + { + this.command = command; + this.args = args; + this.argumentSettingsForInvokedCommand = argumentSettingsForInvokedCommand; + } + + void execute() + { + try + { + command.execute(args); + } + catch(ArgumentException exception) + { + exception.withUsage(argumentSettingsForInvokedCommand.usage()); + throw exception; + } + } + + @Override + public String toString() + { + return command.commandName() + "" + args; + } + } + + void rememberInvocationOfCommand(Command command, ParsedArguments argumentsToCommand, Argument argumentSettingsForInvokedCommand) + { + commandInvocations.add(new CommandInvocation(command, argumentsToCommand, argumentSettingsForInvokedCommand)); + } + + Optional lastCommand() + { + Iterator lastCommandInvocation = commandInvocations.descendingIterator(); + if(lastCommandInvocation.hasNext()) + return Optional.of(lastCommandInvocation.next()); + return Optional.empty(); + } + + void validateAndFinalize(Locale locale) + { + for(CommandInvocation invocation : commandInvocations) + { + invocation.command.parser().validateAndFinalize(this, invocation.argumentSettingsForInvokedCommand, invocation.args, locale); + } + } + + void executeAnyCommandsInTheOrderTheyWereReceived() + { + for(CommandInvocation invocation : commandInvocations) + { + invocation.execute(); + } + } + + /** + * Returns any non-parsed arguments to the last command that was to be executed + */ + Set nonParsedArguments() + { + HashSet result = commandInvocations.stream().map(ci -> ci.args.nonParsedArguments()).collect( HashSet::new, (l, r) -> l.addAll(r), + (l, r) -> l.addAll(r)); + result.addAll(currentHolder.nonParsedArguments()); + return result; + } + + /** + * For indexed arguments in commands the used command name is returned so that when + * multiple commands (or multiple command names) are used it's clear which command the + * offending argument is part of + */ + Optional usedCommandName() + { + return lastCommand().map(invocation -> invocation.argumentSettingsForInvokedCommand.toString()); + } + + static ArgumentIterator forArguments(Iterable arguments, Map> helpArguments, FoundArgumentHandler handler) + { + return new ArgumentIterator(arguments, helpArguments, handler); + } + + static ArgumentIterator forArguments(Iterable arguments) + { + FoundArgumentHandler noOpHandler = (d, p, a, l) -> { + }; + return new ArgumentIterator(arguments, emptyMap(), noOpHandler); + } + + /** + * Returns the string that was given by the previous {@link #next()} invocation. + */ + String current() + { + return arguments.get(currentArgumentIndex - 1); + } + + @Override + public boolean hasNext() + { + return currentArgumentIndex < arguments.size(); + } + + @Override + public String next() + { + String nextArgument = arguments.get(currentArgumentIndex++); + nextArgument = skipAheadIfEndOfOptions(nextArgument); + nextArgument = readArgumentsFromFile(nextArgument); + dirty = false; + + return nextArgument; + } + + /** + * Skips {@link UsageTexts#END_OF_OPTIONS} if the parser hasn't received it yet. + * This is to allow the string {@link UsageTexts#END_OF_OPTIONS} as an indexed argument + * itself. + */ + private String skipAheadIfEndOfOptions(String nextArgument) + { + if(!endOfOptionsReceived && nextArgument.equals(UsageTexts.END_OF_OPTIONS) && hasNext()) + { + endOfOptionsReceived = true; + return next(); + } + return nextArgument; + } + + /** + * Reads arguments from files if the argument starts with a + * {@link UsageTexts#FILE_REFERENCE_PREFIX}. + */ + private String readArgumentsFromFile(String nextArgument) + { + // TODO(jontejj): add possibility to disable this feature? It has some security + // implications as the caller can input any files and if this parser was exposed from a + // server... + if(nextArgument.startsWith(UsageTexts.FILE_REFERENCE_PREFIX)) + { + String filename = nextArgument.substring(1); + File fileWithArguments = new File(filename); + if(fileWithArguments.exists()) + { + try + { + List lines = Files.readAllLines(fileWithArguments.toPath(), UTF_8); + appendArgumentsAtCurrentPosition(lines); + } + catch(IOException errorWhileReadingFile) + { + throw withMessage("Failed while reading arguments from: " + filename, errorWhileReadingFile); + } + // Recursive call adds support for file references from within the file itself + return next(); + } + } + return nextArgument; + } + + void appendArgumentsAtCurrentPosition(List argumentsToAppend) + { + arguments.addAll(currentArgumentIndex, argumentsToAppend); + } + + @Override + public String toString() + { + List parsed = arguments.subList(0, currentArgumentIndex); + List remaining = arguments.subList(currentArgumentIndex, arguments.size()); + return "Parsed: " + parsed + ", Current: " + currentArgumentName + ", Remaining: " + remaining; + } + + /** + * The opposite of {@link #next()}. In short, it makes this iterator return what + * {@link #next()} returned last time once again. + * + * @return the {@link #current()} argument + */ + String previous() + { + return arguments.get(--currentArgumentIndex); + } + + int nrOfRemainingArguments() + { + return arguments.size() - currentArgumentIndex; + } + + void setNextArgumentTo(String newNextArgumentString) + { + arguments.set(--currentArgumentIndex, newNextArgumentString); + dirty = true; + } + + boolean hasPrevious() + { + return currentArgumentIndex > 0; + } + + void setCurrentArgumentName(String argumentName) + { + currentArgumentName = argumentName; + } + + String getCurrentArgumentName() + { + return currentArgumentName; + } + + FoundArgumentHandler handler() + { + return handler; + } + + void setHandler(FoundArgumentHandler handler) + { + this.handler = handler; + } + + /** + * Because {@link CommandLineParserInstance#lookupByName} might use + * {@link #setNextArgumentTo(String)} and leave the iterator behind in a bad state + */ + void removeCurrentIfDirty() + { + if(dirty) + { + arguments.remove(currentArgumentIndex); + dirty = false; + } + } + + ParsedArguments findParentHolderFor(Argument argument) + { + for(CommandInvocation invocation : commandInvocations) + { + Optional parentHolder = invocation.args.findParentHolderFor(argument); + if(parentHolder.isPresent()) + return parentHolder.get(); + } + return currentHolder().findParentHolderFor(argument).get(); + } +} diff --git a/jargo/src/main/java/se/softhouse/jargo/Command.java b/jargo/src/main/java/se/softhouse/jargo/Command.java index 9ca6a4b2..ed96eb72 100644 --- a/jargo/src/main/java/se/softhouse/jargo/Command.java +++ b/jargo/src/main/java/se/softhouse/jargo/Command.java @@ -14,11 +14,17 @@ import static java.util.Arrays.asList; import static java.util.Collections.unmodifiableList; +import static se.softhouse.jargo.Arguments.command; +import static se.softhouse.jargo.CommandLineParser.STANDARD_COMPLETER; import static se.softhouse.jargo.CommandLineParser.US_BY_DEFAULT; +import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Locale; +import java.util.Optional; +import java.util.SortedSet; +import java.util.TreeSet; import java.util.function.Supplier; import javax.annotation.CheckReturnValue; @@ -27,7 +33,8 @@ import se.softhouse.common.guavaextensions.Suppliers2; import se.softhouse.common.strings.Describable; -import se.softhouse.jargo.CommandLineParserInstance.ArgumentIterator; +import se.softhouse.jargo.Argument.ParameterArity; +import se.softhouse.jargo.ArgumentIterator.CommandInvocation; import se.softhouse.jargo.StringParsers.InternalStringParser; import se.softhouse.jargo.internal.Texts.UsageTexts; @@ -39,18 +46,15 @@ * {@link Command}s have a {@link CommandLineParser} themselves (and thereby sub-commands are allowed as well), that is, * they execute a command and may support contextual arguments as specified by the constructor {@link Command#Command(Argument...)}. * - * Sub-commands are executed before their parent {@link Command}. + * Sub-commands are executed after their parent {@link Command}. * * To integrate your {@link Command} into an {@link Argument} use {@link Arguments#command(Command)} * or {@link CommandLineParser#withCommands(Command...)} if you have several commands. * * If you support several commands and a user enters several of them at the same * time they will be executed in the order given to {@link CommandLineParser#parse(String...)}. - * If any {@link StringParser#parse(String, Locale) parse} errors occurs (for {@link Command#Command(Argument...) command arguments}) the {@link Command} - * will not be executed. However, if given multiple commands and {@link StringParser#parse(String, Locale) parse errors} occurs, - * all {@link Command}s given before the {@link Command} with {@link StringParser#parse(String, Locale) parse errors} will have been executed. - * This is so because {@link Command#Command(Argument...) command arguments} are allowed to be dependent on earlier {@link Command}s being executed. - * So it's recommended to let the user know when you've executed a {@link Command}. + * If any {@link StringParser#parse(String, Locale) parse} errors occurs (for {@link Command#Command(Argument...) command arguments}) no {@link Command} + * will be executed. So all arguments, for all commands given, are parsed before any execution occurs. * * Mutability note: although a {@link Command} should be {@link Immutable} * the objects it handles doesn't have to be. So repeated invocations of execute @@ -106,7 +110,7 @@ public abstract class Command extends InternalStringParser impl @Override public CommandLineParserInstance get() { - return new CommandLineParserInstance(commandArguments, ProgramInformation.AUTO, US_BY_DEFAULT, true); + return new CommandLineParserInstance(commandArguments, ProgramInformation.AUTO, US_BY_DEFAULT, true, STANDARD_COMPLETER); } }); @@ -126,6 +130,23 @@ protected Command(List> commandArguments) this.commandArguments = Collections.unmodifiableList(commandArguments); } + /** + * Useful if your {@link Command} has subcommands that you'll want to pass into the + * {@link Command#Command(List)} constructor + * + * @param commands the subcommands + * @return the subcommands as an argument list + */ + public static List> subCommands(final Command ... commands) + { + List> commandsAsArguments = new ArrayList<>(commands.length); + for(Command c : commands) + { + commandsAsArguments.add(command(c).build()); + } + return commandsAsArguments; + } + /** * The name that triggers this command. Defaults to {@link Class#getSimpleName()} in lower * case. For several names override this with {@link ArgumentBuilder#names(String...)} @@ -168,19 +189,27 @@ public String toString() final ParsedArguments parse(final ArgumentIterator arguments, final ParsedArguments previousOccurance, final Argument argumentSettings, Locale locale) throws ArgumentException { - arguments.rememberAsCommand(); - - ParsedArguments parsedArguments = parser().parse(arguments, locale); - - arguments.rememberInvocationOfCommand(this, parsedArguments); + // TODO: how to support rejoining for repeated commands? How to pick which invocation that should get the arg? + if(arguments.currentHolder().wasGiven(argumentSettings) && !argumentSettings.isAllowedToRepeat()) + return resumeParsing(arguments, previousOccurance, locale); + ParsedArguments holder = new ParsedArguments(parser(), arguments.currentHolder()); + arguments.rememberInvocationOfCommand(this, holder, argumentSettings); + parser().parseArguments(holder, arguments, locale); + return holder; + } - return parsedArguments; + final ParsedArguments resumeParsing(final ArgumentIterator arguments, final ParsedArguments previousOccurance, Locale locale) + throws ArgumentException + { + parser().parseArguments(previousOccurance, arguments, locale); + arguments.temporaryRepitionAllowedForCommand = false; + return previousOccurance; } /** * The parser for parsing the {@link Argument}s passed to {@link Command#Command(Argument...)} */ - private CommandLineParserInstance parser() + CommandLineParserInstance parser() { return commandArgumentParser.get(); } @@ -205,7 +234,7 @@ final String describeValue(ParsedArguments value) } @Override - final String metaDescription(Argument argumentSettings) + String metaDescription(Argument argumentSettings) { return ""; } @@ -215,4 +244,30 @@ String metaDescriptionInRightColumn(Argument argumentSettings) { return UsageTexts.ARGUMENT_HEADER; } + + @Override + ParameterArity parameterArity() + { + if(parser().allArguments().isEmpty()) + return ParameterArity.NO_ARGUMENTS; + return ParameterArity.AT_LEAST_ONE_ARGUMENT; + } + + @Override + Iterable complete(Argument argument, String partOfWord, ArgumentIterator iterator) + { + iterator.setCurrentArgumentName(argument.toString()); + Optional lastCommand = iterator.lastCommand(); + if(lastCommand.isPresent() && lastCommand.get().argumentSettingsForInvokedCommand == argument) + { + iterator.setCurrentHolder(lastCommand.get().args); + } + else + { + iterator.setCurrentHolder(new ParsedArguments(parser(), iterator.findParentHolderFor(argument))); + } + + SortedSet suggestions = new TreeSet<>(); + return iterator.currentHolder().rootParser().completer().complete(parser(), partOfWord, suggestions, iterator); + } } diff --git a/jargo/src/main/java/se/softhouse/jargo/CommandLineParser.java b/jargo/src/main/java/se/softhouse/jargo/CommandLineParser.java index 41160a50..a17c0951 100644 --- a/jargo/src/main/java/se/softhouse/jargo/CommandLineParser.java +++ b/jargo/src/main/java/se/softhouse/jargo/CommandLineParser.java @@ -14,8 +14,8 @@ import static java.util.Arrays.asList; import static java.util.Objects.requireNonNull; -import static se.softhouse.jargo.Arguments.command; +import java.io.File; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -30,8 +30,6 @@ import javax.annotation.concurrent.ThreadSafe; import se.softhouse.common.strings.Describable; -import se.softhouse.jargo.ArgumentBuilder.SimpleArgumentBuilder; -import se.softhouse.jargo.StringParsers.RunnableParser; /** * Manages multiple {@link Argument}s and/or {@link Command}s. The brain of this API. @@ -91,7 +89,25 @@ * it will be described by the ArgumentException. Use {@link ArgumentException#getMessageAndUsage()} if you * want to explain what went wrong to the user. * - * + * + * Tab-completion (for Bash) + * Your users need to put this in their ~/.bash_profile file: + * + *
+ * {@code 
+ * alias my-app="java -jar .jar"
+ * complete -o default -o bashdefault -o nospace -C my-app "my-app"
+ * }
+ * 
+ * + * Where my-app is the name of your app. It's highly recommended to teach your users how to configure bash-completion + * as jargo comes with a pretty feature-rich completion function. To customize completions for an {@link Argument}, + * use {@link ArgumentBuilder#completer(java.util.function.Function)}. By default, the {@link StringParser#complete(String)} is used. + * That means that {@link Enum}, {@link File}, {@link Arguments#booleanArgument(String...)} all works out of the box. + * {@link Command}s and sub-commands also get their arguments completed which automatically generates + * git-like bash-completion for your program. + * If you for some reason don't like this, you can disable this functionality with {@link CommandLineParser#noCompleter()}. + *
* Internationalization By default {@link Locale#US} is used for parsing strings and printing * usages. To change this use {@link #locale(Locale)}.
* Thread safety concerns: If there is a parsing occurring while any modifying method is @@ -101,14 +117,16 @@ * externally. */ @ThreadSafe -// TODO(jontejj): make this Immutable public final class CommandLineParser { - // Internally CommandLineParser is a builder for CommandLineParserInstance but the idea is to - // keep this idea hidden in the API to lessen the API complexity - static final Locale US_BY_DEFAULT = Locale.US; + static final Completer STANDARD_COMPLETER = Completers.bashCompleter( () -> System.getenv(), // + (suggestions) -> suggestions.forEach(System.out::println), // + () -> System.exit(0)); + + // Internally CommandLineParser is a builder for CommandLineParserInstance but the idea is to + // keep this idea hidden in the API to lessen the API complexity @GuardedBy("modifyGuard") private volatile CommandLineParserInstance cachedParser; /** * Use of this lock makes sure that there's no race condition if several concurrent calls @@ -119,7 +137,7 @@ public final class CommandLineParser private CommandLineParser(Iterable> argumentDefinitions) { - this.cachedParser = new CommandLineParserInstance(argumentDefinitions); + this.cachedParser = new CommandLineParserInstance(argumentDefinitions, STANDARD_COMPLETER); } CommandLineParserInstance parser() @@ -170,7 +188,7 @@ public static CommandLineParser withArguments(final Iterable> argume @Nonnull public static CommandLineParser withCommands(final Command ... commands) { - return new CommandLineParser(commandsToArguments(commands)); + return new CommandLineParser(Command.subCommands(commands)); } /** @@ -184,12 +202,10 @@ public static CommandLineParser withCommands(final Command ... commands) * public enum Service implements Runnable, Describable * { * START{ - * @Override * public void run(){ * //Start service here * } * - * @Override * public String description(){ * return "Starts the service"; * } @@ -253,7 +269,7 @@ public Usage usage() @Nonnull public CommandLineParser andCommands(final Command ... commandsToAlsoSupport) { - verifiedAdd(commandsToArguments(commandsToAlsoSupport)); + verifiedAdd(Command.subCommands(commandsToAlsoSupport)); return this; } @@ -277,7 +293,7 @@ public CommandLineParser andArguments(final Argument ... argumentsToAlsoSuppo /** * Verify that the parser would be usable after adding {@code argumentsToAdd} to it. - * This allows future parse operations to still use the unaffected old parser. + * This allows future parse operations to still use the unaffected old parser in case of errors with the new argument. */ private void verifiedAdd(Collection> argumentsToAdd) { @@ -286,7 +302,11 @@ private void verifiedAdd(Collection> argumentsToAdd) modifyGuard.lock(); List> newDefinitions = new ArrayList<>(parser().allArguments()); newDefinitions.addAll(argumentsToAdd); - cachedParser = new CommandLineParserInstance(newDefinitions, parser().programInformation(), parser().locale(), false); + cachedParser = new CommandLineParserInstance(newDefinitions, + parser().programInformation(), + parser().locale(), + false, + parser().completer()); } finally { @@ -305,7 +325,7 @@ public CommandLineParser programName(String programName) { modifyGuard.lock(); ProgramInformation programInformation = parser().programInformation().programName(programName); - cachedParser = new CommandLineParserInstance(parser().allArguments(), programInformation, parser().locale(), false); + cachedParser = new CommandLineParserInstance(parser().allArguments(), programInformation, parser().locale(), false, parser().completer()); } finally { @@ -325,7 +345,7 @@ public CommandLineParser programDescription(String programDescription) { modifyGuard.lock(); ProgramInformation programInformation = parser().programInformation().programDescription(programDescription); - cachedParser = new CommandLineParserInstance(parser().allArguments(), programInformation, parser().locale(), false); + cachedParser = new CommandLineParserInstance(parser().allArguments(), programInformation, parser().locale(), false, parser().completer()); } finally { @@ -351,7 +371,8 @@ public CommandLineParser locale(Locale localeToUse) try { modifyGuard.lock(); - cachedParser = new CommandLineParserInstance(parser().allArguments(), parser().programInformation(), requireNonNull(localeToUse), false); + cachedParser = new CommandLineParserInstance(parser() + .allArguments(), parser().programInformation(), requireNonNull(localeToUse), false, parser().completer()); } finally { @@ -360,6 +381,14 @@ public CommandLineParser locale(Locale localeToUse) return this; } + /** + * Disables the completion functionality. + */ + public CommandLineParser noCompleter() + { + return completer(Completers.noCompleter()); + } + /** * Returns the {@link #usage()} for this {@link CommandLineParser} */ @@ -369,14 +398,18 @@ public String toString() return usage().toString(); } - private static List> commandsToArguments(final Command ... commands) + CommandLineParser completer(Completer completer) { - List> commandsAsArguments = new ArrayList<>(commands.length); - for(Command c : commands) + try { - commandsAsArguments.add(command(c).build()); + modifyGuard.lock(); + cachedParser = new CommandLineParserInstance(parser().allArguments(), parser().programInformation(), parser().locale(), false, completer); } - return commandsAsArguments; + finally + { + modifyGuard.unlock(); + } + return this; } private static & Runnable & Describable> List> commandsToArguments(Class commandEnum) @@ -384,11 +417,11 @@ private static & Runnable & Describable> List> co List> commandsAsArguments = new ArrayList<>(); for(E command : commandEnum.getEnumConstants()) { - Argument commandAsArgument = new SimpleArgumentBuilder(new RunnableParser(command)) // - .names(command.name().toLowerCase(Locale.US)) // - .description(command) // - .build(); - commandsAsArguments.add(commandAsArgument); + Argument commandArgument = Arguments.optionArgument(command.name().toLowerCase(Locale.US)).ignoreCase().transform((a) -> { + command.run(); + return null; + }).description(command).defaultValueDescription("N/A").build(); + commandsAsArguments.add(commandArgument); } return commandsAsArguments; } diff --git a/jargo/src/main/java/se/softhouse/jargo/CommandLineParserInstance.java b/jargo/src/main/java/se/softhouse/jargo/CommandLineParserInstance.java index 45dc0d08..f5120747 100644 --- a/jargo/src/main/java/se/softhouse/jargo/CommandLineParserInstance.java +++ b/jargo/src/main/java/se/softhouse/jargo/CommandLineParserInstance.java @@ -12,57 +12,49 @@ */ package se.softhouse.jargo; -import se.softhouse.common.collections.CharacterTrie; -import se.softhouse.common.guavaextensions.Lists2; -import se.softhouse.common.strings.StringsUtil; -import se.softhouse.jargo.ArgumentExceptions.UnexpectedArgumentException; -import se.softhouse.jargo.StringParsers.InternalStringParser; -import se.softhouse.jargo.internal.Texts.ProgrammaticErrors; -import se.softhouse.jargo.internal.Texts.UsageTexts; -import se.softhouse.jargo.internal.Texts.UserErrors; +import static se.softhouse.common.guavaextensions.Lists2.size; +import static se.softhouse.common.guavaextensions.Preconditions2.check; +import static se.softhouse.common.strings.Describables.format; +import static se.softhouse.common.strings.StringsUtil.startsWithAndHasMore; +import static se.softhouse.jargo.Argument.IS_OF_VARIABLE_ARITY; +import static se.softhouse.jargo.Argument.IS_REPEATED; +import static se.softhouse.jargo.Argument.IS_REQUIRED; +import static se.softhouse.jargo.Argument.ParameterArity.NO_ARGUMENTS; +import static se.softhouse.jargo.ArgumentBuilder.DEFAULT_SEPARATOR; +import static se.softhouse.jargo.ArgumentExceptions.forMissingArguments; +import static se.softhouse.jargo.ArgumentExceptions.forUnallowedRepetitionArgument; +import static se.softhouse.jargo.ArgumentExceptions.withMessage; +import static se.softhouse.jargo.ArgumentExceptions.wrapException; +import static se.softhouse.jargo.CommandLineParser.US_BY_DEFAULT; -import javax.annotation.CheckReturnValue; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import javax.annotation.concurrent.Immutable; -import javax.annotation.concurrent.NotThreadSafe; -import javax.annotation.concurrent.ThreadSafe; -import java.io.File; -import java.io.IOException; -import java.nio.file.Files; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; -import java.util.Collections; import java.util.HashMap; import java.util.HashSet; -import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Map.Entry; +import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; -import static java.util.Collections.emptySet; -import static se.softhouse.common.guavaextensions.Lists2.size; -import static se.softhouse.common.guavaextensions.Preconditions2.checkNulls; -import static se.softhouse.common.guavaextensions.Preconditions2.check; -import static se.softhouse.common.guavaextensions.Sets2.union; -import static se.softhouse.common.strings.Describables.format; -import static se.softhouse.common.strings.StringsUtil.NEWLINE; -import static se.softhouse.common.strings.StringsUtil.TAB; -import static se.softhouse.common.strings.StringsUtil.startsWithAndHasMore; -import static se.softhouse.jargo.Argument.IS_OF_VARIABLE_ARITY; -import static se.softhouse.jargo.Argument.IS_REQUIRED; -import static se.softhouse.jargo.Argument.ParameterArity.NO_ARGUMENTS; -import static se.softhouse.jargo.ArgumentBuilder.DEFAULT_SEPARATOR; -import static se.softhouse.jargo.ArgumentExceptions.forMissingArguments; -import static se.softhouse.jargo.ArgumentExceptions.forUnallowedRepetitionArgument; -import static se.softhouse.jargo.ArgumentExceptions.withMessage; -import static se.softhouse.jargo.ArgumentExceptions.wrapException; -import static se.softhouse.jargo.CommandLineParser.US_BY_DEFAULT; +import javax.annotation.CheckReturnValue; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +import se.softhouse.common.collections.CharacterTrie; +import se.softhouse.common.guavaextensions.Lists2; +import se.softhouse.common.strings.StringsUtil; +import se.softhouse.jargo.Argument.ParameterArity; +import se.softhouse.jargo.ArgumentExceptions.UnexpectedArgumentException; +import se.softhouse.jargo.ArgumentIterator.CommandInvocation; +import se.softhouse.jargo.StringParsers.InternalStringParser; +import se.softhouse.jargo.internal.Texts.ProgrammaticErrors; +import se.softhouse.jargo.internal.Texts.UserErrors; /** * A snapshot view of a {@link CommandLineParser} configuration. @@ -106,7 +98,10 @@ final class CommandLineParserInstance private final Locale locale; - CommandLineParserInstance(Iterable> argumentDefinitions, ProgramInformation information, Locale locale, boolean isCommandParser) + private final Completer completer; + + CommandLineParserInstance(Iterable> argumentDefinitions, ProgramInformation information, Locale locale, boolean isCommandParser, + Completer completer) { int nrOfArgumentsToHandle = size(argumentDefinitions); this.indexedArguments = new ArrayList<>(nrOfArgumentsToHandle); @@ -118,6 +113,7 @@ final class CommandLineParserInstance this.programInformation = information; this.locale = locale; this.isCommandParser = isCommandParser; + this.completer = completer; for(Argument definition : argumentDefinitions) { addArgumentDefinition(definition); @@ -125,11 +121,12 @@ final class CommandLineParserInstance verifyThatIndexedAndRequiredArgumentsWasGivenBeforeAnyOptionalArguments(); verifyUniqueMetasForRequiredAndIndexedArguments(); verifyThatOnlyOneArgumentIsOfVariableArity(); + verifyThatNoIndexedArgumentIsRepeated(); } - CommandLineParserInstance(Iterable> argumentDefinitions) + CommandLineParserInstance(Iterable> argumentDefinitions, Completer completer) { - this(argumentDefinitions, ProgramInformation.AUTO, US_BY_DEFAULT, false); + this(argumentDefinitions, ProgramInformation.AUTO, US_BY_DEFAULT, false, completer); } private void addArgumentDefinition(final Argument definition) @@ -218,84 +215,146 @@ private void verifyThatOnlyOneArgumentIsOfVariableArity() check(indexedVariableArityArguments.size() <= 1, ProgrammaticErrors.SEVERAL_VARIABLE_ARITY_PARSERS, indexedVariableArityArguments); } + private void verifyThatNoIndexedArgumentIsRepeated() + { + Collection> indexedRepeatedArguments = indexedArguments.stream().filter(IS_REPEATED).collect(Collectors.toList()); + check(indexedRepeatedArguments.isEmpty(), ProgrammaticErrors.INDEXED_AND_REPEATED_ARGUMENT, indexedRepeatedArguments); + } + @Nonnull ParsedArguments parse(final Iterable actualArguments) throws ArgumentException { - ArgumentIterator arguments = ArgumentIterator.forArguments(actualArguments, helpArguments); - return parse(arguments, locale()); + completer.completeIfApplicable(this); + + ParsedArguments holder = new ParsedArguments(this); + ArgumentIterator arguments = ArgumentIterator.forArguments(actualArguments, helpArguments, this::handleArgument); + parseArguments(holder, arguments, locale()); + + return holder; } @Nonnull - ParsedArguments parse(ArgumentIterator arguments, Locale inLocale) throws ArgumentException + void parseArguments(ParsedArguments holder, final ArgumentIterator iterator, Locale inLocale) throws ArgumentException { - ParsedArguments holder = parseArguments(arguments, inLocale); - - Collection> missingArguments = holder.requiredArgumentsLeft(); - if(missingArguments.size() > 0) - throw forMissingArguments(missingArguments).withUsage(usage(inLocale)); + iterator.setCurrentHolder(holder); - for(Argument arg : holder.parsedArguments()) + while(iterator.hasNext()) { - holder.finalize(arg); - limitArgument(arg, holder, inLocale); + iterator.setCurrentArgumentName(iterator.next()); + parseArgument(iterator, holder); } if(!isCommandParser()) { - arguments.executeLastCommand(); + validateAndFinalize(iterator, null, holder, inLocale); + iterator.validateAndFinalize(inLocale); + iterator.executeAnyCommandsInTheOrderTheyWereReceived(); } - return holder; } - private ParsedArguments parseArguments(final ArgumentIterator iterator, Locale inLocale) throws ArgumentException + void validateAndFinalize(ArgumentIterator iterator, @Nullable Argument command, ParsedArguments holder, Locale inLocale) { - ParsedArguments holder = new ParsedArguments(allArguments()); - iterator.setCurrentParser(this); - while(iterator.hasNext()) + try { - Argument definition = null; - try + Collection> missingArguments = holder.requiredArgumentsLeft(); + if(!missingArguments.isEmpty()) + throw forMissingArguments(missingArguments).withUsage(usage(inLocale)); + + for(Argument arg : holder.parsedArguments()) { - iterator.setCurrentArgumentName(iterator.next()); - definition = getDefinitionForCurrentArgument(iterator, holder); - if(definition == null) - { - break; - } - parseArgument(iterator, holder, definition, inLocale); + holder.finalize(arg); + limitArgument(arg, holder, inLocale); } - catch(ArgumentException e) + } + catch(ArgumentException exception) + { + if(command != null) { - e.withUsedArgumentName(iterator.getCurrentArgumentName()); - if(definition != null) - { - e.withUsageReference(definition); - } - throw e.withUsage(usage(inLocale)); + exception.withUsedArgumentName(command.names().get(0)); + exception.withUsageReference(command); + } + else + { + exception.withUsedArgumentName(iterator.getCurrentArgumentName()); } + throw exception; } - return holder; } - private void parseArgument(final ArgumentIterator arguments, final ParsedArguments parsedArguments, final Argument definition, - Locale inLocale) throws ArgumentException + @FunctionalInterface + interface FoundArgumentHandler { - if(parsedArguments.wasGiven(definition) && !definition.isAllowedToRepeat() && !definition.isPropertyMap()) - throw forUnallowedRepetitionArgument(arguments.current()); - - InternalStringParser parser = definition.parser(); - T oldValue = parsedArguments.getValue(definition); - - T parsedValue = parser.parse(arguments, oldValue, definition, inLocale); - parsedArguments.put(definition, parsedValue); + void handle(final Argument definition, ParsedArguments parsedArguments, final ArgumentIterator arguments, Locale inLocale); } /** - * @return a definition that defines how to handle the current argument * @throws UnexpectedArgumentException if no definition could be found * for the current argument */ - @Nullable - private Argument getDefinitionForCurrentArgument(final ArgumentIterator iterator, final ParsedArguments holder) throws ArgumentException + void parseArgument(final ArgumentIterator iterator, final ParsedArguments holder) throws ArgumentException + { + Argument definition = null; + try + { + definition = getDefinition(iterator, holder); + if(definition != null) + { + iterator.handler().handle(definition, holder, iterator, locale()); + return; + } + + if(!isCommandParser()) + { + // If we have previously rollbacked(to parse main args in the middle of a command's arguments), + // then let's also try to rejoin parsing of that command's arguments again + Optional lastCommandInvocation = iterator.lastCommand(); + if(lastCommandInvocation.isPresent()) + { + CommandInvocation invocation = lastCommandInvocation.get(); + definition = invocation.command.parser().getDefinition(iterator, invocation.args); + if(definition == null) + { + definition = invocation.command.parser().indexed(iterator, invocation.args); + } + if(definition != null) + { + iterator.temporaryRepitionAllowedForCommand = true; + iterator.handler().handle(definition, invocation.args, iterator, locale()); + return; + } + } + } + + Optional parentHolder = holder.parentHolder(); + if(parentHolder.isPresent()) + { + parentHolder.get().parser().parseArgument(iterator, parentHolder.get()); + return; + } + + definition = indexed(iterator, holder); + if(definition != null) + { + iterator.handler().handle(definition, holder, iterator, locale()); + return; + } + + guessAndSuggestIfCloseMatch(iterator); + + // We're out of order, tell the user what we didn't like + throw ArgumentExceptions.forUnexpectedArgument(iterator); + } + catch(ArgumentException e) + { + e.withUsedArgumentName(iterator.getCurrentArgumentName()); + if(definition != null) + { + e.withUsageReference(definition); + } + throw e.withUsage(usage(locale())); + } + } + + Argument getDefinition(ArgumentIterator iterator, ParsedArguments holder) { if(iterator.allowsOptions()) { @@ -307,23 +366,21 @@ private Argument getDefinitionForCurrentArgument(final ArgumentIterator itera if(option != null) return option; } + return null; + } - Argument indexedArgument = indexedArgument(iterator, holder); - if(indexedArgument != null) - return indexedArgument; - - if(isCommandParser()) + Argument indexed(ArgumentIterator iterator, ParsedArguments holder) + { + Optional> indexedArgument = indexedArgument(holder); + if(indexedArgument.isPresent()) { - // Rolling back here means that the parent parser/command will receive the argument - // instead, maybe it can handle it + Argument arg = indexedArgument.get(); iterator.previous(); - return null; + // This helps the error messages explain which of the indexed arguments that failed + iterator.setCurrentArgumentName(iterator.usedCommandName().orElseGet(() -> arg.metaDescriptionInRightColumn())); + return arg; } - - guessAndSuggestIfCloseMatch(iterator, holder); - - // We're out of order, tell the user what we didn't like - throw ArgumentExceptions.forUnexpectedArgument(iterator); + return null; } /** @@ -341,9 +398,10 @@ private Argument lookupByName(ArgumentIterator arguments) Entry> entry = specialArguments.get(currentArgument); if(entry != null) { + arguments.setCurrentArgumentName(entry.getKey()); // Remove "-D" from "-Dkey=value" - String valueAfterSeparator = currentArgument.substring(entry.getKey().length()); - arguments.setNextArgumentTo(valueAfterSeparator); + String keyValue = currentArgument.substring(entry.getKey().length()); + arguments.setNextArgumentTo(keyValue); return entry.getValue(); } definition = arguments.helpArgument(currentArgument); @@ -383,7 +441,7 @@ private Argument batchOfShortNamedArguments(ArgumentIterator arguments, Parse // worth it, the result is the same at least for(Argument option : foundOptions) { - parseArgument(arguments, holder, option, null); + handleArgument(option, holder, arguments, holder.parser().locale()); } return lastOption; } @@ -394,41 +452,56 @@ private Argument batchOfShortNamedArguments(ArgumentIterator arguments, Parse /** * Looks for {@link ArgumentBuilder#names(String...) indexed arguments} */ - private Argument indexedArgument(ArgumentIterator arguments, ParsedArguments holder) + Optional> indexedArgument(ParsedArguments holder) { - if(holder.indexedArgumentsParsed() < indexedArguments.size()) + if(holder.indexedArgumentsParsed() < indexedArguments.size() && !holder.hasNonIndexedRequiredArgumentsLeft()) { Argument definition = indexedArguments.get(holder.indexedArgumentsParsed()); - arguments.previous(); - // This helps the error messages explain which of the indexed arguments that failed - if(isCommandParser()) - { - arguments.setCurrentArgumentName(arguments.usedCommandName()); - } - else - { - arguments.setCurrentArgumentName(definition.metaDescriptionInRightColumn()); - } - return definition; + return Optional.of(definition); } - return null; + return Optional.empty(); } - private static final int ONLY_REALLY_CLOSE_MATCHES = 4; + private static final int ONLY_REALLY_CLOSE_MATCHES = 3; + + void handleArgument(final Argument definition, ParsedArguments parsedArguments, final ArgumentIterator arguments, Locale inLocale) + throws ArgumentException + { + arguments.setCurrentHolder(parsedArguments); + InternalStringParser parser = definition.parser(); + + boolean commandOverride = (parser instanceof Command) && arguments.temporaryRepitionAllowedForCommand; + if(parsedArguments.wasGiven(definition) && !definition.isAllowedToRepeat() && !definition.isPropertyMap() && !commandOverride) + throw forUnallowedRepetitionArgument(arguments.current()); + + T oldValue = parsedArguments.getValue(definition); + + T parsedValue = parser.parse(arguments, oldValue, definition, inLocale); + + oldValue = parsedArguments.put(definition, parsedValue); + // Re-check as parse is recursive + if(oldValue != null && !definition.isAllowedToRepeat() && !definition.isPropertyMap() && !commandOverride) + throw forUnallowedRepetitionArgument(arguments.current()); + + if(definition.isIndexed() && definition.parser().parameterArity() != ParameterArity.VARIABLE_AMOUNT) + { + parsedArguments.incrementIndexedArgumentsParsed(); + } + } /** * Suggests probable, valid, alternatives for a faulty argument, based on the * {@link StringsUtil#levenshteinDistance(String, String)} */ - private void guessAndSuggestIfCloseMatch(ArgumentIterator arguments, ParsedArguments holder) throws ArgumentException + private void guessAndSuggestIfCloseMatch(ArgumentIterator arguments) throws ArgumentException { - Set availableArguments = union(holder.nonParsedArguments(), arguments.nonParsedArguments()); + Set availableArguments = arguments.nonParsedArguments(); if(!availableArguments.isEmpty()) { - List suggestions = StringsUtil.closestMatches(arguments.current(), availableArguments, ONLY_REALLY_CLOSE_MATCHES); + List suggestions = StringsUtil.closestMatches(arguments.getCurrentArgumentName(), availableArguments, ONLY_REALLY_CLOSE_MATCHES); if(!suggestions.isEmpty()) - throw withMessage(format(UserErrors.SUGGESTION, arguments.current(), String.join(NEWLINE + TAB, suggestions))); + throw ArgumentExceptions.withSuggestions(arguments.getCurrentArgumentName(), suggestions); } } @@ -471,6 +544,11 @@ Locale locale() return locale; } + Completer completer() + { + return completer; + } + @CheckReturnValue @Nonnull Usage usage(Locale inLocale) @@ -488,12 +566,21 @@ ArgumentException helpFor(ArgumentIterator arguments, Locale inLocale) throws Ar { arguments.next(); Argument argument = lookupByName(arguments); + ParsedArguments matchedHolder = arguments.currentHolder(); + while(argument == null && matchedHolder.parentHolder().isPresent()) + { + matchedHolder = matchedHolder.parentHolder().get(); + argument = matchedHolder.parser().lookupByName(arguments); + } + if(argument == null) throw withMessage(format(UserErrors.UNKNOWN_ARGUMENT, arguments.current())); - usage = new Usage(Arrays.>asList(argument), inLocale, programInformation(), isCommandParser()); - if(isCommandParser()) + + boolean commandParser = matchedHolder.parser().isCommandParser(); + usage = new Usage(Arrays.>asList(argument), inLocale, programInformation(), commandParser); + if(commandParser) { - String withCommandReference = ". Usage for " + argument + " (argument to " + arguments.usedCommandName() + "):"; + String withCommandReference = ". Usage for " + argument + " (argument to " + arguments.usedCommandName().get() + "):"; e.withUsageReference(withCommandReference); } else @@ -506,7 +593,7 @@ ArgumentException helpFor(ArgumentIterator arguments, Locale inLocale) throws Ar usage = usage(inLocale); if(isCommandParser()) { - e.withUsageReference(". See usage for " + arguments.usedCommandName() + " below:"); + e.withUsageReference(". See usage for " + arguments.usedCommandName().get() + " below:"); } else { @@ -522,239 +609,9 @@ public String toString() return usage(locale()).toString(); } - /** - * Wraps a list of given arguments and remembers - * which argument that is currently being parsed. Plays a key role in making - * {@link CommandLineParserInstance} {@link ThreadSafe} as it holds the current state of a parse - * invocation. - */ - @NotThreadSafe - static final class ArgumentIterator implements Iterator + Map> helpArguments() { - private final List arguments; - - /** - * Corresponds to one of the {@link Argument#names()} that has been given from the command - * line. This is updated as soon as the parsing of a new argument begins. - * For indexed arguments this will be the meta description instead. - */ - private String currentArgumentName; - private int currentArgumentIndex; - private boolean endOfOptionsReceived; - - private int indexOfLastCommand = -1; - private Command lastCommandParsed; - private ParsedArguments argumentsToLastCommand; - - /** - * In case of {@link Command}s this may be the parser for a specific {@link Command} or just - * simply the main parser - */ - private CommandLineParserInstance currentParser; - private final Map> helpArguments; - - /** - * @param actualArguments a list of arguments, will be modified - */ - private ArgumentIterator(Iterable actualArguments, Map> helpArguments) - { - this.arguments = checkNulls(actualArguments, "Argument strings may not be null"); - this.helpArguments = helpArguments; - } - - Argument helpArgument(String currentArgument) - { - return helpArguments.get(currentArgument); - } - - /** - * Returns true if {@link UsageTexts#END_OF_OPTIONS} hasn't been received yet. - */ - boolean allowsOptions() - { - return !endOfOptionsReceived; - } - - void setCurrentParser(CommandLineParserInstance instance) - { - currentParser = instance; - } - - void rememberAsCommand() - { - executeLastCommand(); - // The command has moved the index by 1 therefore the -1 to get the index of the - // commandName - indexOfLastCommand = currentArgumentIndex - 1; - } - - void rememberInvocationOfCommand(Command command, ParsedArguments argumentsToCommand) - { - executeLastCommand(); - lastCommandParsed = command; - this.argumentsToLastCommand = argumentsToCommand; - } - - void executeLastCommand() - { - if(lastCommandParsed != null) - { - lastCommandParsed.execute(argumentsToLastCommand); - lastCommandParsed = null; - } - } - - /** - * Returns any non-parsed arguments to the last command that was executed - */ - Set nonParsedArguments() - { - if(lastCommandParsed != null) - return argumentsToLastCommand.nonParsedArguments(); - return emptySet(); - } - - /** - * For indexed arguments in commands the used command name is returned so that when - * multiple commands (or multiple command names) are used it's clear which command the - * offending argument is part of - */ - String usedCommandName() - { - return arguments.get(indexOfLastCommand); - } - - static ArgumentIterator forArguments(Iterable arguments, Map> helpArguments) - { - return new ArgumentIterator(arguments, helpArguments); - } - - static ArgumentIterator forArguments(Iterable arguments) - { - return new ArgumentIterator(arguments, Collections.>emptyMap()); - } - - /** - * Returns the string that was given by the previous {@link #next()} invocation. - */ - String current() - { - return arguments.get(currentArgumentIndex - 1); - } - - @Override - public boolean hasNext() - { - return currentArgumentIndex < arguments.size(); - } - - @Override - public String next() - { - String nextArgument = arguments.get(currentArgumentIndex++); - nextArgument = skipAheadIfEndOfOptions(nextArgument); - nextArgument = readArgumentsFromFile(nextArgument); - - return nextArgument; - } - - /** - * Skips {@link UsageTexts#END_OF_OPTIONS} if the parser hasn't received it yet. - * This is to allow the string {@link UsageTexts#END_OF_OPTIONS} as an indexed argument - * itself. - */ - private String skipAheadIfEndOfOptions(String nextArgument) - { - if(!endOfOptionsReceived && nextArgument.equals(UsageTexts.END_OF_OPTIONS)) - { - endOfOptionsReceived = true; - return next(); - } - return nextArgument; - } - - /** - * Reads arguments from files if the argument starts with a - * {@link UsageTexts#FILE_REFERENCE_PREFIX}. - */ - private String readArgumentsFromFile(String nextArgument) - { - // TODO(jontejj): add possibility to disable this feature? It has some security - // implications as the caller can input any files and if this parser was exposed from a - // server... - if(nextArgument.startsWith(UsageTexts.FILE_REFERENCE_PREFIX)) - { - String filename = nextArgument.substring(1); - File fileWithArguments = new File(filename); - if(fileWithArguments.exists()) - { - try - { - List lines = Files.readAllLines(fileWithArguments.toPath(), StringsUtil.UTF8); - appendArgumentsAtCurrentPosition(lines); - } - catch(IOException errorWhileReadingFile) - { - throw withMessage("Failed while reading arguments from: " + filename, errorWhileReadingFile); - } - // Recursive call adds support for file references from within the file itself - return next(); - } - } - return nextArgument; - } - - private void appendArgumentsAtCurrentPosition(List argumentsToAppend) - { - arguments.addAll(currentArgumentIndex, argumentsToAppend); - } - - @Override - public String toString() - { - return arguments.subList(currentArgumentIndex, arguments.size()).toString(); - } - - /** - * The opposite of {@link #next()}. In short it makes this iterator return what - * {@link #next()} returned last time once again. - * - * @return the {@link #current()} argument - */ - String previous() - { - return arguments.get(--currentArgumentIndex); - } - - int nrOfRemainingArguments() - { - return arguments.size() - currentArgumentIndex; - } - - void setNextArgumentTo(String newNextArgumentString) - { - arguments.set(--currentArgumentIndex, newNextArgumentString); - } - - boolean hasPrevious() - { - return currentArgumentIndex > 0; - } - - void setCurrentArgumentName(String argumentName) - { - currentArgumentName = argumentName; - } - - String getCurrentArgumentName() - { - return currentArgumentName; - } - - CommandLineParserInstance currentParser() - { - return currentParser; - } + return helpArguments; } private static final class NamedArguments diff --git a/jargo/src/main/java/se/softhouse/jargo/Completer.java b/jargo/src/main/java/se/softhouse/jargo/Completer.java new file mode 100644 index 00000000..5747bc1d --- /dev/null +++ b/jargo/src/main/java/se/softhouse/jargo/Completer.java @@ -0,0 +1,33 @@ +/* Copyright 2018 jonatanjonsson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package se.softhouse.jargo; + +import java.util.SortedSet; + +/** + * Provides a way to support tab-completions for terminals. + */ +interface Completer +{ + /** + * The first trigger of the completion + */ + void completeIfApplicable(CommandLineParserInstance parser); + + /** + * A continuation of a completion invocation + */ + SortedSet complete(CommandLineParserInstance parser, String partOfWord, SortedSet suggestions, ArgumentIterator iterator); +} diff --git a/jargo/src/main/java/se/softhouse/jargo/Completers.java b/jargo/src/main/java/se/softhouse/jargo/Completers.java new file mode 100644 index 00000000..963e962c --- /dev/null +++ b/jargo/src/main/java/se/softhouse/jargo/Completers.java @@ -0,0 +1,268 @@ +/* Copyright 2018 jonatanjonsson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package se.softhouse.jargo; + +import static java.util.Objects.requireNonNull; + +import java.util.Collections; +import java.util.LinkedList; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; +import java.util.function.Consumer; +import java.util.function.Supplier; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import se.softhouse.common.strings.StringsUtil; +import se.softhouse.jargo.Argument.ParameterArity; +import se.softhouse.jargo.ArgumentExceptions.MissingParameterException; +import se.softhouse.jargo.ArgumentExceptions.MissingRequiredArgumentException; +import se.softhouse.jargo.ArgumentExceptions.SuggestiveArgumentException; +import se.softhouse.jargo.ArgumentExceptions.UnexpectedArgumentException; +import se.softhouse.jargo.CommandLineParserInstance.FoundArgumentHandler; + +/** + * Implementations of the {@link Completer} interface. + */ +final class Completers +{ + private Completers() + { + } + + /** + * @return a {@link Completer} doing nothing + */ + static Completer noCompleter() + { + return new Completer(){ + @Override + public void completeIfApplicable(CommandLineParserInstance parser) + { + } + + @Override + public SortedSet complete(CommandLineParserInstance parser, String partOfWord, SortedSet suggestions, + ArgumentIterator iterator) + { + return null; + } + }; + } + + static Completer bashCompleter(Supplier> environmentVariables, Consumer> output, Runnable exitMethod) + { + return new BashCompleter(environmentVariables, output, exitMethod); + } + + static final class BashCompleter implements Completer + { + private final Supplier> environmentVariables; + + private final Runnable exitMethod; + + private final Consumer> output; + + private BashCompleter(Supplier> environmentVariables, Consumer> output, Runnable exitMethod) + { + this.environmentVariables = requireNonNull(environmentVariables); + this.output = requireNonNull(output); + this.exitMethod = requireNonNull(exitMethod); + } + + @Override + public void completeIfApplicable(CommandLineParserInstance parser) + { + Map environment = environmentVariables.get(); + if(environment.containsKey("COMP_LINE")) + { + String lineToComplete = environment.get("COMP_LINE"); + int editingPosition = Integer.parseInt(environment.get("COMP_POINT")); + // TODO: complete when editing in the middle of a line as well + if(editingPosition == lineToComplete.length()) + { + // TODO handle " and ' as bash does + Stream splits = Pattern.compile(" ").splitAsStream(lineToComplete); + LinkedList list = new LinkedList<>(splits.skip(1).collect(Collectors.toList())); + if(lineToComplete.endsWith(" ")) + { + list.add(""); + } + String partOfWord = list.removeLast(); + SortedSet suggestions = complete(parser, list, partOfWord); + output.accept(suggestions); + } + exitMethod.run(); + } + } + + // ThreadLocal so that tests can run concurrently, would have been a simple boolean otherwise + static ThreadLocal hasSuggestedForLastArg = ThreadLocal.withInitial(() -> false); + + SortedSet complete(CommandLineParserInstance parser, Iterable args, String partOfWord) + { + hasSuggestedForLastArg.set(false); + + SortedSet suggestions = new TreeSet<>(); + + ParsedArguments holder = new ParsedArguments(parser); + + FoundArgumentHandler handler = new FoundArgumentHandler(){ + @Override + public void handle(Argument definition, ParsedArguments parsedArguments, ArgumentIterator iter, Locale inLocale) + { + if(iter.hasNext() || definition.parser().parameterArity() == ParameterArity.NO_ARGUMENTS) + { + parser.handleArgument(definition, parsedArguments, iter, inLocale); + } + else + { + iter.currentHolder().put(definition, null); + definition.complete(partOfWord, iter).forEach(suggestions::add); + hasSuggestedForLastArg.set(true); + iter.removeCurrentIfDirty(); + } + } + }; + + ArgumentIterator iterator = ArgumentIterator.forArguments(args, parser.helpArguments(), handler); + iterator.setCurrentHolder(holder); + + return complete(parser, partOfWord, suggestions, iterator); + } + + public SortedSet complete(CommandLineParserInstance parser, String partOfWord, SortedSet suggestions, + ArgumentIterator iterator) + { + try + { + while(iterator.hasNext() && !hasSuggestedForLastArg.get()) + { + // TODO complete if arg starts with UsageTexts.FILE_REFERENCE_PREFIX and it's the last char + iterator.setCurrentArgumentName(iterator.next()); + parser.parseArgument(iterator, iterator.currentHolder()); + } + iterator.setCurrentArgumentName(partOfWord); + } + catch(SuggestiveArgumentException exception) + { + suggestions.addAll(exception.suggestions()); + } + catch(MissingRequiredArgumentException exception) + { + if(hasSuggestedForLastArg.get()) + return suggestions; + + // We are completing, so this is acceptable, suggest the missing args + for(Argument arg : exception.missingArguments()) + { + for(String name : arg.names()) + { + String namePlusSeparator = name + arg.separator(); + if(partOfWord.startsWith(namePlusSeparator)) + { + // The required argument is currently being completed, complete it + iterator.setCurrentArgumentName(namePlusSeparator); + arg.complete(partOfWord, iterator).forEach(suggestions::add); + iterator.currentHolder().put(arg, null); + hasSuggestedForLastArg.set(true); + } + else if(namePlusSeparator.startsWith(iterator.getCurrentArgumentName())) + { + suggestions.add(namePlusSeparator); + break; + } + } + } + } + catch(MissingParameterException exception) + { + suggestions.clear(); + exception.argumentWithMissingParameter().complete(partOfWord, iterator).forEach(suggestions::add); + return suggestions; + } + catch(ArgumentException exception) + { + throw exception; + } + + if(hasSuggestedForLastArg.get()) + return suggestions; + + iterator.lastCommand().ifPresent(command -> { + command.command.parser().indexedArgument(command.args).ifPresent(indexedArg -> { + boolean wasGivenBefore = command.args.wasGiven(indexedArg); + indexedArg.complete(partOfWord, iterator).forEach(suggestions::add); + if(indexedArg.parser().parameterArity() == ParameterArity.VARIABLE_AMOUNT && wasGivenBefore) + { + // No other args are allowed from here on + hasSuggestedForLastArg.set(true); + } + }); + }); + if(hasSuggestedForLastArg.get()) + return suggestions; + + // When all the previous arguments been parsed, it's time to complete the current arg + completeLastArg(parser, iterator, partOfWord, iterator.currentHolder(), suggestions); + Set options = iterator.nonParsedArguments(); + SortedSet matchingPrefix = StringsUtil.prefixesIgnoringCase(partOfWord, options, parser.locale()); + suggestions.addAll(matchingPrefix); + + if(suggestions.size() == 1 && !suggestions.first().equals(partOfWord)) + { + iterator.isCompletingGeneratedSuggestion = true; + // Try to be smart and predict the next suggestions as if the user would have pressed tab + completeLastArg(parser, iterator, suggestions.first(), iterator.currentHolder(), suggestions); + } + return suggestions; + } + + private void completeLastArg(CommandLineParserInstance parser, ArgumentIterator iterator, String partOfWord, ParsedArguments holder, + SortedSet suggestions) + { + iterator.setHandler(new FoundArgumentHandler(){ + @Override + public void handle(Argument definition, ParsedArguments parsedArguments, ArgumentIterator arguments, Locale inLocale) + { + // Avoid recursive completion if the arg to complete is a "finished" command + if(!(definition.parser() instanceof Command)) + { + definition.complete(partOfWord, iterator).forEach(suggestions::add); + } + + // Simulate that it was parsed to exclude the arg from further being suggested + holder.put(definition, null); + + arguments.removeCurrentIfDirty(); + } + }); + + iterator.appendArgumentsAtCurrentPosition(Collections.singletonList(partOfWord)); + iterator.setCurrentArgumentName(iterator.next()); + try + { + parser.parseArgument(iterator, holder); + } + catch(SuggestiveArgumentException | UnexpectedArgumentException exception) + { + } + } + } +} diff --git a/jargo/src/main/java/se/softhouse/jargo/ParsedArguments.java b/jargo/src/main/java/se/softhouse/jargo/ParsedArguments.java index 1fc49b7b..20d7d08a 100644 --- a/jargo/src/main/java/se/softhouse/jargo/ParsedArguments.java +++ b/jargo/src/main/java/se/softhouse/jargo/ParsedArguments.java @@ -12,24 +12,28 @@ */ package se.softhouse.jargo; -import se.softhouse.jargo.internal.Texts.ProgrammaticErrors; +import static java.util.Objects.requireNonNull; +import static java.util.stream.Collectors.toList; +import static se.softhouse.common.guavaextensions.Preconditions2.check; +import static se.softhouse.common.guavaextensions.Predicates2.in; +import static se.softhouse.jargo.Argument.IS_INDEXED; +import static se.softhouse.jargo.Argument.IS_REQUIRED; -import javax.annotation.CheckReturnValue; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import javax.annotation.concurrent.Immutable; import java.util.Collection; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.Locale; import java.util.Map; +import java.util.Optional; import java.util.Set; +import java.util.stream.Stream; -import static java.util.Objects.requireNonNull; -import static java.util.stream.Collectors.toList; -import static se.softhouse.common.guavaextensions.Preconditions2.check; -import static se.softhouse.common.guavaextensions.Predicates2.in; -import static se.softhouse.jargo.Argument.IS_REQUIRED; +import javax.annotation.CheckReturnValue; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +import se.softhouse.jargo.internal.Texts.ProgrammaticErrors; /** * Holds parsed arguments for a {@link CommandLineParser#parse(String...)} invocation. @@ -41,16 +45,36 @@ public final class ParsedArguments /** * Stores results from {@link StringParser#parse(String, Locale)} */ - @Nonnull private final Map, Object> parsedArguments = new LinkedHashMap<>(); + @Nonnull private final Map, Object> parsedArguments = new LinkedHashMap<>(4); @Nonnull private final Set> allArguments; + + /** + * This gives commands access to any args to the root/parent command arguments + */ + @Nonnull private final Optional parent; + + /** + * The parser that created this instance + */ + @Nonnull private final CommandLineParserInstance source; + /** * Keeps a running total of how many indexed arguments that have been parsed */ private int indexedArgumentsParsed = 0; - ParsedArguments(Set> arguments) + ParsedArguments(CommandLineParserInstance source, ParsedArguments parent) + { + this.allArguments = source.allArguments(); + this.source = source; + this.parent = Optional.of(parent); + } + + ParsedArguments(CommandLineParserInstance source) { - allArguments = arguments; + this.allArguments = source.allArguments(); + this.source = source; + this.parent = Optional.empty(); } /** @@ -66,7 +90,7 @@ public T get(final Argument argumentToFetch) { if(!wasGiven(argumentToFetch)) { - check(allArguments.contains(argumentToFetch), ProgrammaticErrors.ILLEGAL_ARGUMENT, argumentToFetch); + check(handlesArgument(argumentToFetch), ProgrammaticErrors.ILLEGAL_ARGUMENT, argumentToFetch); return argumentToFetch.defaultValue(); } return getValue(argumentToFetch); @@ -80,7 +104,12 @@ public T get(final Argument argumentToFetch) */ public boolean wasGiven(Argument argument) { - return parsedArguments.containsKey(requireNonNull(argument)); + return parsedArguments.containsKey(requireNonNull(argument)) || parent.map(args -> args.wasGiven(argument)).orElse(false); + } + + private boolean handlesArgument(Argument arg) + { + return allArguments.contains(arg) || parent.map(args -> args.handlesArgument(arg)).orElse(false); } @Override @@ -108,14 +137,10 @@ public int hashCode() // Publicly this class is Immutable, CommandLineParserInstance is only allowed to modify it // during parsing - - void put(final Argument definition, @Nullable final T value) + @SuppressWarnings("unchecked") + T put(final Argument definition, @Nullable final T value) { - if(definition.isIndexed()) - { - indexedArgumentsParsed++; - } - parsedArguments.put(definition, value); + return (T) parsedArguments.put(definition, value); } /** @@ -130,10 +155,14 @@ void finalize(final Argument definition) T getValue(final Argument definition) { - // Safe because put guarantees that the map is heterogeneous - @SuppressWarnings("unchecked") - T value = (T) parsedArguments.get(definition); - return value; + if(parsedArguments.containsKey(definition)) + { + // Safe because put guarantees that the map is heterogeneous + @SuppressWarnings("unchecked") + T value = (T) parsedArguments.get(definition); + return value; + } + return parent.map(args -> args.getValue(definition)).orElse(null); } Collection> requiredArgumentsLeft() @@ -141,27 +170,41 @@ Collection> requiredArgumentsLeft() return allArguments.stream().filter(IS_REQUIRED.and(in(parsedArguments.keySet()).negate())).collect(toList()); } + boolean hasNonIndexedRequiredArgumentsLeft() + { + return allArguments.stream().filter(IS_REQUIRED.and(IS_INDEXED.negate()).and(in(parsedArguments.keySet()).negate())).findFirst().isPresent(); + } + int indexedArgumentsParsed() { return indexedArgumentsParsed; } + void incrementIndexedArgumentsParsed() + { + indexedArgumentsParsed++; + } + Set> parsedArguments() { return parsedArguments.keySet(); } + Stream> allArgumentsRecursively() + { + return Stream.concat(allArguments.stream(), parent.map(r -> r.allArgumentsRecursively()).orElse(Stream.empty())); + } + Set nonParsedArguments() { Set validArguments = new HashSet<>(allArguments.size()); - for(Argument argument : allArguments) - { + allArgumentsRecursively().forEach(argument -> { boolean wasGiven = wasGiven(argument); if(!wasGiven || argument.isAllowedToRepeat()) { for(String name : argument.names()) { - if(argument.separator().equals(ArgumentBuilder.DEFAULT_SEPARATOR) || argument.isPropertyMap()) + if(argument.isPropertyMap()) { validArguments.add(name); } @@ -172,7 +215,33 @@ Set nonParsedArguments() } } - } + }); return validArguments; } + + CommandLineParserInstance parser() + { + return source; + } + + Optional parentHolder() + { + return parent; + } + + CommandLineParserInstance rootParser() + { + if(parent.isPresent()) + return parent.get().rootParser(); + return parser(); + } + + Optional findParentHolderFor(Argument argument) + { + if(allArguments.contains(argument)) + return Optional.of(this); + else if(parent.isPresent()) + return parent.get().findParentHolderFor(argument); + return Optional.empty(); + } } diff --git a/jargo/src/main/java/se/softhouse/jargo/ProgramInformation.java b/jargo/src/main/java/se/softhouse/jargo/ProgramInformation.java index 78221102..8eebce0e 100644 --- a/jargo/src/main/java/se/softhouse/jargo/ProgramInformation.java +++ b/jargo/src/main/java/se/softhouse/jargo/ProgramInformation.java @@ -12,16 +12,16 @@ */ package se.softhouse.jargo; -import se.softhouse.common.classes.Classes; -import se.softhouse.common.strings.Describable; -import se.softhouse.common.strings.Describables; +import static java.lang.System.lineSeparator; +import static java.util.Objects.requireNonNull; +import static se.softhouse.common.strings.Describables.cache; import javax.annotation.CheckReturnValue; import javax.annotation.concurrent.Immutable; -import static java.util.Objects.requireNonNull; -import static se.softhouse.common.strings.Describables.cache; -import static se.softhouse.common.strings.StringsUtil.NEWLINE; +import se.softhouse.common.classes.Classes; +import se.softhouse.common.strings.Describable; +import se.softhouse.common.strings.Describables; /** * Information about a program, printed in {@link Usage} before any {@link Argument}s are described. @@ -80,7 +80,7 @@ static ProgramInformation withProgramName(String programName) ProgramInformation programDescription(String aProgramDescription) { requireNonNull(aProgramDescription); - return new ProgramInformation(programName, NEWLINE + aProgramDescription + NEWLINE); + return new ProgramInformation(programName, lineSeparator() + aProgramDescription + lineSeparator()); } /** diff --git a/jargo/src/main/java/se/softhouse/jargo/StringParser.java b/jargo/src/main/java/se/softhouse/jargo/StringParser.java index 2724a4ff..f4036185 100644 --- a/jargo/src/main/java/se/softhouse/jargo/StringParser.java +++ b/jargo/src/main/java/se/softhouse/jargo/StringParser.java @@ -12,6 +12,7 @@ */ package se.softhouse.jargo; +import java.util.Collections; import java.util.Locale; import java.util.function.Function; @@ -19,6 +20,8 @@ import javax.annotation.Nullable; import javax.annotation.concurrent.Immutable; +import se.softhouse.common.strings.StringsUtil; + /** * Parses {@link String}s into values of the type {@code T}. * Create an {@link Argument} for your {@link StringParser} with @@ -147,4 +150,21 @@ default T apply(String argument) */ @Nonnull String metaDescription(); + + /** + * Generates a list of suggestions to make {@code partOfWord} a valid argument that could be passed to {@link #parse(String, Locale)} + * when the program is actually executed later on. + * Preferably this method should be really quick to enable a smooth user experience in bash/etc. + *
+ * Tip: When you have your valid options, you can use {@link StringsUtil#prefixes(String, java.util.Collection)} + * to filter out the options that still match {@code partOfWord}. + * + * @param partOfWord a potential part of an argument that this argument might receive + * @return a list of strings that would complete the parameter into a valid argument (defaults to an empty list) + */ + @Nonnull + default Iterable complete(String partOfWord) + { + return Collections.emptyList(); + } } diff --git a/jargo/src/main/java/se/softhouse/jargo/StringParsers.java b/jargo/src/main/java/se/softhouse/jargo/StringParsers.java index fb4aa5c8..8c1ebba0 100644 --- a/jargo/src/main/java/se/softhouse/jargo/StringParsers.java +++ b/jargo/src/main/java/se/softhouse/jargo/StringParsers.java @@ -12,7 +12,9 @@ */ package se.softhouse.jargo; +import static java.util.Arrays.asList; import static java.util.Collections.emptyList; +import static java.util.Collections.singleton; import static java.util.Objects.requireNonNull; import static se.softhouse.common.guavaextensions.Preconditions2.check; import static se.softhouse.common.strings.Describables.format; @@ -23,17 +25,28 @@ import static se.softhouse.jargo.ArgumentExceptions.wrapException; import java.io.File; +import java.io.IOException; import java.math.BigDecimal; import java.math.BigInteger; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Optional; +import java.util.SortedSet; import java.util.StringJoiner; +import java.util.TreeSet; +import java.util.function.Function; import java.util.function.Predicate; import java.util.function.Supplier; import java.util.regex.Pattern; @@ -47,8 +60,9 @@ import se.softhouse.common.numbers.NumberType; import se.softhouse.common.strings.StringBuilders; import se.softhouse.jargo.Argument.ParameterArity; +import se.softhouse.jargo.ArgumentBuilder.MapArgumentBuilder; import se.softhouse.jargo.ArgumentExceptions.MissingParameterException; -import se.softhouse.jargo.CommandLineParserInstance.ArgumentIterator; +import se.softhouse.jargo.ArgumentIterator.CommandInvocation; import se.softhouse.jargo.internal.Texts.UserErrors; /** @@ -87,32 +101,35 @@ enum StringStringParser implements StringParser */ STRING { - @Override - public String parse(String value, Locale locale) throws ArgumentException - { - return value; - } - }; - // Put other StringParser parsers here + @Override + public String parse(String value, Locale locale) throws ArgumentException + { + return value; + } - @Override - public String descriptionOfValidValues(Locale locale) - { - return "any string"; - } + }; - @Override - public String defaultValue() - { - return ""; - } + // Put other StringParser parsers here + + @Override + public String descriptionOfValidValues(Locale locale) + { + return "any string"; + } + + @Override + public String defaultValue() + { + return ""; + } + + @Override + public String metaDescription() + { + return ""; + } - @Override - public String metaDescription() - { - return ""; - } } /** @@ -155,6 +172,19 @@ public String metaDescription() { return ""; } + + @Override + public Iterable complete(String partOfWord) + { + if(partOfWord.equals("")) + return asList("true", "false"); + else if("true".startsWith(partOfWord)) + return singleton("true"); + else if("false".startsWith(partOfWord)) + return singleton("false"); + else + return Collections.emptyList(); + } } /** @@ -196,6 +226,27 @@ public String metaDescription() { return ""; } + + @Override + public Iterable complete(String partOfWord) + { + // TODO: consider security if the cli is running on a server + List suggestions = new ArrayList<>(); + try(DirectoryStream currentDir = Files.newDirectoryStream(Paths.get("."))) + { + currentDir.forEach(file -> { + if(file.getFileName().toString().startsWith(partOfWord)) + { + suggestions.add(file.getFileName().toString()); + } + }); + } + catch(IOException e) + { + return suggestions; + } + return suggestions; + } } /** @@ -329,6 +380,28 @@ public String metaDescription() { return "<" + enumType.getSimpleName() + ">"; } + + @Override + public Iterable complete(String partOfWord) + { + List suggestions = new ArrayList<>(); + for(E enumValue : enumType.getEnumConstants()) + { + if(enumValue.name().startsWith(partOfWord)) + { + suggestions.add(enumValue.name()); + } + else + { + String enumInLowerCase = enumValue.name().toLowerCase(Locale.US); + if(enumInLowerCase.startsWith(partOfWord)) + { + suggestions.add(enumInLowerCase); + } + } + } + return suggestions; + } } /** @@ -447,6 +520,81 @@ public String toString() } } + static final class TransformingParser extends InternalStringParser + { + private final Function transformer; + private final InternalStringParser firstParser; + private final Predicate limiter; + + TransformingParser(InternalStringParser firstParser, Function transformer, Predicate limiter) + { + this.firstParser = requireNonNull(firstParser); + this.transformer = requireNonNull(transformer); + this.limiter = requireNonNull(limiter); + } + + @SuppressWarnings("unchecked") + @Override + F parse(ArgumentIterator arguments, F previousOccurance, Argument argumentSettings, Locale locale) throws ArgumentException + { + T first = firstParser.parse(arguments, null, argumentSettings, locale); + if(!limiter.test(first)) + throw withMessage(format(UserErrors.DISALLOWED_VALUE, first, argumentSettings.descriptionOfValidValues(locale))); + + F result = transformer.apply(first); + if(previousOccurance instanceof Collection && result instanceof Collection && argumentSettings.isAllowedToRepeat()) + { + ((Collection) previousOccurance).addAll((Collection) result); + return previousOccurance; + } + return result; + } + + @Override + String descriptionOfValidValues(Argument argumentSettings, Locale locale) + { + if(limiter != Predicates2.alwaysTrue()) + return limiter.toString(); + return firstParser.descriptionOfValidValues(argumentSettings, locale); + } + + @Override + String metaDescriptionInLeftColumn(Argument argumentSettings) + { + return firstParser.metaDescriptionInLeftColumn(argumentSettings); + } + + @Override + String metaDescriptionInRightColumn(Argument argumentSettings) + { + return firstParser.metaDescriptionInRightColumn(argumentSettings); + } + + @Override + String metaDescription(Argument argumentSettings) + { + return firstParser.metaDescription(argumentSettings); + } + + @Override + F defaultValue() + { + return transformer.apply(firstParser.defaultValue()); + } + + @Override + ParameterArity parameterArity() + { + return firstParser.parameterArity(); + } + + @Override + Iterable complete(Argument argument, String partOfWord, ArgumentIterator iterator) + { + return firstParser.complete(null, partOfWord, iterator); + } + } + /** *
 	 * Makes it possible to convert several (or zero) {@link String}s into a single {@code T} value.
@@ -525,6 +673,16 @@ ParameterArity parameterArity()
 		{
 			return ParameterArity.AT_LEAST_ONE_ARGUMENT;
 		}
+
+		/**
+		 * @param argument the argument that was matched
+		 * @param partOfWord a part of a parameter
+		 * @param iterator current iterator
+		 */
+		Iterable complete(@Nullable Argument argument, String partOfWord, ArgumentIterator iterator)
+		{
+			return Collections.emptyList();
+		}
 	}
 
 	@CheckReturnValue
@@ -596,7 +754,7 @@ static final class HelpParser extends InternalStringParser
 		@Override
 		String parse(ArgumentIterator arguments, String previousOccurance, Argument argumentSettings, Locale locale) throws ArgumentException
 		{
-			throw arguments.currentParser().helpFor(arguments, locale);
+			throw arguments.currentHolder().parser().helpFor(arguments, locale);
 		}
 
 		@Override
@@ -616,43 +774,21 @@ String metaDescription(Argument argumentSettings)
 		{
 			return "";
 		}
-	}
-
-	/**
-	 * Runs a {@link Runnable} when {@link StringParser#parse(String, Locale) parse} is invoked.
-	 */
-	static final class RunnableParser extends InternalStringParser
-	{
-		final Runnable target;
-
-		RunnableParser(Runnable target)
-		{
-			this.target = target;
-		}
 
 		@Override
-		Object parse(ArgumentIterator arguments, Object previousOccurance, Argument argumentSettings, Locale locale) throws ArgumentException
+		Iterable complete(Argument argument, String partOfWord, ArgumentIterator iterator)
 		{
-			target.run();
-			return null;
-		}
-
-		@Override
-		String descriptionOfValidValues(Argument argumentSettings, Locale locale)
-		{
-			return "";
-		}
-
-		@Override
-		Object defaultValue()
-		{
-			return null;
-		}
+			if(iterator.hasNext())
+			{
+				iterator.next();
+			}
+			CommandLineParserInstance rootParser = iterator.currentHolder().rootParser();
+			SortedSet suggestions = new TreeSet<>();
+			Optional lastCommand = iterator.lastCommand();
+			if(lastCommand.isPresent())
+				return rootParser.completer().complete(lastCommand.get().command.parser(), partOfWord, suggestions, iterator);
 
-		@Override
-		String metaDescription(Argument argumentSettings)
-		{
-			return "";
+			return rootParser.completer().complete(iterator.currentHolder().parser(), partOfWord, suggestions, iterator);
 		}
 	}
 
@@ -712,6 +848,12 @@ String describeValue(List value)
 			}
 			return sb.toString();
 		}
+
+		@Override
+		Iterable complete(Argument> argument, String partOfWord, ArgumentIterator iterator)
+		{
+			return elementParser.complete(null, partOfWord, iterator);
+		}
 	}
 
 	/**
@@ -868,12 +1010,19 @@ static final class RepeatedArgumentParser extends ListParser
 			super(parser);
 		}
 
+		@SuppressWarnings("unchecked")
 		@Override
 		List parse(final ArgumentIterator arguments, List previouslyCreatedList, final Argument argumentSettings, Locale locale)
 				throws ArgumentException
 		{
 			T parsedValue = elementParser().parse(arguments, null, argumentSettings, locale);
 
+			if(elementParser() instanceof Command)
+			{
+				// Re-check as parse is recursive in this case
+				previouslyCreatedList = (List) arguments.currentHolder().getValue(argumentSettings);
+			}
+
 			List listToStoreRepeatedValuesIn = previouslyCreatedList;
 			if(listToStoreRepeatedValuesIn == null)
 			{
@@ -942,9 +1091,10 @@ Map parse(final ArgumentIterator arguments, Map previousMap, final A
 			{
 				map = defaultValue();
 			}
-
 			String keyValue = arguments.next();
-			String key = getKey(keyValue, argumentSettings);
+			String separator = argumentSettings.separator();
+			String key = getKey(keyValue, separator)
+					.orElseThrow(() -> withMessage(format(UserErrors.MISSING_KEY_VALUE_SEPARATOR, argumentSettings, keyValue, separator)));
 			K parsedKey = keyParser.parse(key, locale);
 			V oldValue = map.get(parsedKey);
 
@@ -972,15 +1122,14 @@ Map parse(final ArgumentIterator arguments, Map previousMap, final A
 		/**
 		 * Fetch "key" from "key=value"
 		 */
-		private String getKey(String keyValue, Argument argumentSettings) throws ArgumentException
+		private Optional getKey(String keyValue, String separator) throws ArgumentException
 		{
 			if(valueParser.parameterArity() == ParameterArity.NO_ARGUMENTS)
-				return keyValue;// Consume the whole string as the key as there's no value to parse
-			String separator = argumentSettings.separator();
+				return Optional.of(keyValue);// Consume the whole string as the key as there's no value to parse
 			int keyEndIndex = keyValue.indexOf(separator);
 			if(keyEndIndex == -1)
-				throw withMessage(format(UserErrors.MISSING_KEY_VALUE_SEPARATOR, argumentSettings, keyValue, separator));
-			return keyValue.substring(0, keyEndIndex);
+				return Optional.empty();
+			return Optional.of(keyValue.substring(0, keyEndIndex));
 		}
 
 		/**
@@ -1024,6 +1173,41 @@ String metaDescription(Argument argumentSettings)
 			String valueMeta = valueParser.metaDescription(argumentSettings);
 			return keyMeta + separator + valueMeta;
 		}
+
+		@Override
+		Iterable complete(Argument> arg, String partOfWord, ArgumentIterator iterator)
+		{
+			List suggestions = new ArrayList<>();
+			String separator = arg.separator();
+			Optional maybeKey = getKey(partOfWord, separator);
+			if(!maybeKey.isPresent())
+			{
+				for(String suggestion : keyParser.complete(partOfWord))
+				{
+					suggestions.add(iterator.getCurrentArgumentName() + suggestion + arg.separator());
+				}
+			}
+			else
+			{
+				String key = maybeKey.get();
+				String value = getValue(key, partOfWord, arg);
+
+				for(String suggestion : valueParser.complete(null, value, iterator))
+				{
+					// Because = is a word boundary by default in bash
+					if(arg.separator().equals(MapArgumentBuilder.DEFAULT_KV_SEPARATOR)
+							&& (!value.isEmpty() || !iterator.isCompletingGeneratedSuggestion))
+					{
+						suggestions.add(suggestion);
+					}
+					else
+					{
+						suggestions.add(iterator.getCurrentArgumentName() + key + arg.separator() + suggestion);
+					}
+				}
+			}
+			return suggestions;
+		}
 	}
 
 	static final class StringParserBridge extends InternalStringParser implements StringParser
@@ -1090,5 +1274,17 @@ public String toString()
 		{
 			return descriptionOfValidValues(Locale.US);
 		}
+
+		@Override
+		public Iterable complete(String partOfWord)
+		{
+			return stringParser.complete(partOfWord);
+		}
+
+		@Override
+		Iterable complete(Argument argument, String partOfWord, ArgumentIterator iterator)
+		{
+			return stringParser.complete(partOfWord);
+		}
 	}
 }
diff --git a/jargo/src/main/java/se/softhouse/jargo/Usage.java b/jargo/src/main/java/se/softhouse/jargo/Usage.java
index 48f5db36..9b9bf26e 100644
--- a/jargo/src/main/java/se/softhouse/jargo/Usage.java
+++ b/jargo/src/main/java/se/softhouse/jargo/Usage.java
@@ -13,10 +13,10 @@
 package se.softhouse.jargo;
 
 import static java.lang.Math.max;
+import static java.lang.System.lineSeparator;
 import static java.util.Collections.unmodifiableList;
 import static java.util.Objects.requireNonNull;
 import static java.util.stream.Collectors.toList;
-import static se.softhouse.common.strings.StringsUtil.NEWLINE;
 import static se.softhouse.common.strings.StringsUtil.repeat;
 import static se.softhouse.jargo.Argument.IS_INDEXED;
 import static se.softhouse.jargo.Argument.IS_OF_VARIABLE_ARITY;
@@ -84,6 +84,7 @@ public String toString()
 	private final transient ProgramInformation program;
 	private final transient boolean forCommand;
 	// TODO(jontejj): try getting the correct value automatically, if not possible fall back to 80
+	// exec tput cols 2> /dev/tty
 	private transient int columnWidth = 80;
 
 	private transient String fromSerializedUsage = null;
@@ -119,7 +120,10 @@ public String toString()
 	private Usage(String fromSerializedUsage)
 	{
 		// All these are unused as the usage is already constructed
-		this(null, null, null, false);
+		this.unfilteredArguments = null;
+		this.locale = null;
+		this.program = null;
+		this.forCommand = false;
 		this.fromSerializedUsage = fromSerializedUsage;
 	}
 
@@ -224,7 +228,7 @@ private void appendUsageTo(Appendable builder) throws IOException
 	private String header()
 	{
 		if(forCommand) // Commands get their header from their meta description
-			return hasArguments() ? NEWLINE : "";
+			return hasArguments() ? lineSeparator() : "";
 
 		String mainUsage = UsageTexts.USAGE_HEADER + program.programName();
 
@@ -233,11 +237,11 @@ private String header()
 			mainUsage += UsageTexts.ARGUMENT_INDICATOR;
 		}
 
-		mainUsage += NEWLINE + Lines.wrap(program.programDescription(), columnWidth, locale);
+		mainUsage += lineSeparator() + Lines.wrap(program.programDescription(), columnWidth, locale);
 
 		if(hasArguments())
 		{
-			mainUsage += NEWLINE + UsageTexts.ARGUMENT_HEADER + ":" + NEWLINE;
+			mainUsage += lineSeparator() + UsageTexts.ARGUMENT_HEADER + ":" + lineSeparator();
 		}
 
 		return mainUsage;
@@ -340,7 +344,7 @@ private Row usageForArgument(final Argument arg)
 		{
 			row.descriptionColumn.append(Lines.wrap(description, indexOfDescriptionColumn, columnWidth, locale));
 			addIndicators(arg, row.descriptionColumn);
-			row.descriptionColumn.append(NEWLINE);
+			row.descriptionColumn.append(lineSeparator());
 			valueExplanation(arg, row.descriptionColumn);
 		}
 		else
@@ -379,15 +383,15 @@ private  void valueExplanation(final Argument arg, StringBuilder target)
 		{
 			if(!validValuesDescription.isEmpty())
 			{
-				target.append(NEWLINE);
+				target.append(lineSeparator());
 			}
 			String spaces = repeat(" ", UsageTexts.DEFAULT_VALUE_START.length());
-			descriptionOfDefaultValue = descriptionOfDefaultValue.replace(NEWLINE, NEWLINE + spaces);
+			descriptionOfDefaultValue = descriptionOfDefaultValue.replace(lineSeparator(), lineSeparator() + spaces);
 			target.append(UsageTexts.DEFAULT_VALUE_START).append(descriptionOfDefaultValue);
 		}
 	}
 
-	private static final Pattern BY_NEWLINE = Pattern.compile(NEWLINE);
+	private static final Pattern BY_NEWLINE = Pattern.compile(lineSeparator());
 
 	private void appendRowTo(Row row, Appendable target) throws IOException
 	{
@@ -406,13 +410,13 @@ private void appendRowTo(Row row, Appendable target) throws IOException
 				target.append(repeat(" ", paddingWidth));
 				target.append(descriptionLines.next());
 			}
-			target.append(NEWLINE);
+			target.append(lineSeparator());
 		}
 		while(descriptionLines.hasNext())
 		{
 			target.append(repeat(" ", indexOfDescriptionColumn));
 			target.append(descriptionLines.next());
-			target.append(NEWLINE);
+			target.append(lineSeparator());
 		}
 	}
 
diff --git a/jargo/src/main/java/se/softhouse/jargo/internal/Texts.java b/jargo/src/main/java/se/softhouse/jargo/internal/Texts.java
index c729de0e..8271c29c 100644
--- a/jargo/src/main/java/se/softhouse/jargo/internal/Texts.java
+++ b/jargo/src/main/java/se/softhouse/jargo/internal/Texts.java
@@ -12,7 +12,11 @@
  */
 package se.softhouse.jargo.internal;
 
-import se.softhouse.common.strings.StringsUtil;
+import static java.lang.System.lineSeparator;
+import static se.softhouse.common.strings.StringsUtil.TAB;
+
+import java.nio.charset.StandardCharsets;
+
 import se.softhouse.jargo.Argument;
 import se.softhouse.jargo.ArgumentBuilder;
 import se.softhouse.jargo.ArgumentException;
@@ -21,9 +25,6 @@
 import se.softhouse.jargo.StringParser;
 import se.softhouse.jargo.StringParsers;
 
-import static se.softhouse.common.strings.StringsUtil.NEWLINE;
-import static se.softhouse.common.strings.StringsUtil.TAB;
-
 /**
  * Contains {@link String#format(String, Object...)} ready strings.
  */
@@ -74,7 +75,7 @@ private UsageTexts()
 
 		/**
 		 * Indicates that if a file called with what comes after the @ sign exists, arguments should
-		 * be read from it (in {@link StringsUtil#UTF8}) before continuing on with the parsing.
+		 * be read from it (in {@link StandardCharsets#UTF_8}) before continuing on with the parsing.
 		 */
 		public static final String FILE_REFERENCE_PREFIX = "@";
 	}
@@ -100,7 +101,7 @@ private UserErrors()
 		 * Used by {@link CommandLineParser#parse(String...)}.
 		 * 
 		 */
-		public static final String SUGGESTION = "Didn't expect %s, did you mean one of these?" + NEWLINE + TAB + "%s";
+		public static final String SUGGESTION = "Didn't expect %s, did you mean one of these?" + lineSeparator() + TAB + "%s";
 
 		/**
 		 * 
@@ -243,6 +244,13 @@ private ProgrammaticErrors()
 		 * {@link ArgumentBuilder#variableArity()}
 		 */
 		public static final String SEVERAL_VARIABLE_ARITY_PARSERS = "Several unnamed arguments are configured to receive a variable arity of parameters: %s";
+
+		/**
+		 * Parameter %s = a list with all parameters that is configured with
+		 * {@link ArgumentBuilder#repeated()} and are {@link ArgumentBuilder#names(String...) indexed}
+		 */
+		public static final String INDEXED_AND_REPEATED_ARGUMENT = "Argument: %s is both indexed and repeated. If you expect more than one parameter, use arity(...) / variableArity() instead.";
+
 		/**
 		 * Parameter %s = a name that would cause ambiguous parsing
 		 */
@@ -280,6 +288,6 @@ private ProgrammaticErrors()
 		 * Parameter %s = the illegal argument that the {@link CommandLineParser} wasn't
 		 * configured to handle
 		 */
-		public static final String ILLEGAL_ARGUMENT = "%s was not found in this result at all. Did you perhaps forget to add it to withArguments(...)?";
+		public static final String ILLEGAL_ARGUMENT = "%s was not found in this result at all. Did you perhaps forget to add it to withArguments(...)? Another failure cause could be that you are expecting to access arguments to subcommands from a parent command, that is not enabled by default.";
 	}
 }
diff --git a/jargo/src/test/java/se/softhouse/common/classes/ClassesTest.java b/jargo/src/test/java/se/softhouse/common/classes/ClassesTest.java
index f6b30d6c..95563016 100644
--- a/jargo/src/test/java/se/softhouse/common/classes/ClassesTest.java
+++ b/jargo/src/test/java/se/softhouse/common/classes/ClassesTest.java
@@ -18,12 +18,12 @@
 
 import org.junit.Test;
 
-import se.softhouse.common.testlib.Launcher;
-import se.softhouse.common.testlib.Launcher.LaunchedProgram;
-
 import com.google.common.testing.NullPointerTester;
 import com.google.common.testing.NullPointerTester.Visibility;
 
+import se.softhouse.common.testlib.Launcher;
+import se.softhouse.common.testlib.Launcher.LaunchedProgram;
+
 /**
  * Tests for {@link Classes}
  */
@@ -34,11 +34,10 @@ public void testThatMainClassNameIsExampleProgram() throws IOException, Interrup
 	{
 		LaunchedProgram threadedProgram = Launcher.launch(ExampleProgram.class);
 
-		// TODO(jontejj): add assertion once
-		// http://bugs.java.com/bugdatabase/view_bug.do?bug_id=8021205 has been solved
-		// assertThat(threadedProgram.errors()).as("Errors detected in subprogram: " +
-		// threadedProgram.errors() + ". Debuginfo:"
-		// + threadedProgram.debugInformation()).isEmpty();
+		// If you get problems with this line, try to update your java environment. See
+		// http://bugs.java.com/bugdatabase/view_bug.do?bug_id=8021205 for more details.
+		assertThat(threadedProgram.errors())
+				.as("Errors detected in subprogram: " + threadedProgram.errors() + ". Debuginfo:" + threadedProgram.debugInformation()).isEmpty();
 		assertThat(threadedProgram.output()).isEqualTo("ExampleProgram");
 	}
 
@@ -47,11 +46,10 @@ public void testThatFetchingMainClassNameWorksFromANewThread() throws IOExceptio
 	{
 		LaunchedProgram threadedProgram = Launcher.launch(ThreadedProgram.class);
 
-		// TODO(jontejj): add assertion once
-		// http://bugs.java.com/bugdatabase/view_bug.do?bug_id=8021205 has been solved
-		// assertThat(threadedProgram.errors()).as("Errors detected in subprogram: " +
-		// threadedProgram.errors() + ". Debuginfo:"
-		// + threadedProgram.debugInformation()).isEmpty();
+		// If you get problems with this line, try to update your java environment. See
+		// http://bugs.java.com/bugdatabase/view_bug.do?bug_id=8021205 for more details.
+		assertThat(threadedProgram.errors())
+				.as("Errors detected in subprogram: " + threadedProgram.errors() + ". Debuginfo:" + threadedProgram.debugInformation()).isEmpty();
 		assertThat(threadedProgram.output()).isEqualTo("ThreadedProgram");
 	}
 
diff --git a/jargo/src/test/java/se/softhouse/common/classes/NoMainAvailable.java b/jargo/src/test/java/se/softhouse/common/classes/NoMainAvailable.java
index 42e44890..e8e565b1 100644
--- a/jargo/src/test/java/se/softhouse/common/classes/NoMainAvailable.java
+++ b/jargo/src/test/java/se/softhouse/common/classes/NoMainAvailable.java
@@ -12,6 +12,8 @@
  */
 package se.softhouse.common.classes;
 
+import static org.fest.assertions.Assertions.assertThat;
+
 public final class NoMainAvailable
 {
 	private NoMainAvailable()
@@ -38,8 +40,10 @@ public void run()
 				}
 				try
 				{
-					Classes.mainClassName();
-					System.err.print("Requesting name of mainClass after main thread has died should trigger an IllegalStateException");
+					String mainClassName = Classes.mainClassName();
+					assertThat(mainClassName)
+							.describedAs("Requesting name of mainClass after main thread has died should trigger an IllegalStateException") //
+							.isNull();
 				}
 				catch(IllegalStateException expected)
 				{
diff --git a/jargo/src/test/java/se/softhouse/common/collections/CharacterTrieBenchmark.java b/jargo/src/test/java/se/softhouse/common/collections/CharacterTrieBenchmark.java
index 411976df..fe95dbb2 100644
--- a/jargo/src/test/java/se/softhouse/common/collections/CharacterTrieBenchmark.java
+++ b/jargo/src/test/java/se/softhouse/common/collections/CharacterTrieBenchmark.java
@@ -15,7 +15,6 @@
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Map.Entry;
-import java.util.Set;
 import java.util.SortedMap;
 
 import com.google.caliper.Param;
@@ -88,7 +87,7 @@ public int timePrefixSearching(int reps)
 		int dummy = 0;
 		for(int i = 0; i < reps; i++)
 		{
-			for(Entry entry : type.entriesWithPrefix(elements, "100"))
+			for(Entry entry : type.entriesWithPrefix(elements, "100").entrySet())
 			{
 				if(entry != null)
 				{
@@ -103,37 +102,40 @@ private enum CollectionType
 	{
 		MAP
 		{
-			@Override
+
+	@Override
 			 Map createMap(Map elements)
 			{
 				return Maps.newTreeMap();
 			}
 
-			@Override
-			 Set> entriesWithPrefix(Map elements, String prefix)
+	@Override
+			 Map entriesWithPrefix(Map elements, String prefix)
 			{
-				return ((SortedMap) elements).tailMap(prefix).entrySet();
+				return ((SortedMap) elements).tailMap(prefix);
 			}
-		},
 
-		CHARACTER_TRIE
-		{
-			@Override
+	},
+
+	CHARACTER_TRIE{
+
+	@Override
 			 Map createMap(Map elements)
 			{
 				return CharacterTrie.newTrie(elements);
 			}
 
-			@Override
-			 Set> entriesWithPrefix(Map elements, String prefix)
+	@Override
+			 Map entriesWithPrefix(Map elements, String prefix)
 			{
 				return ((CharacterTrie) elements).getEntriesWithPrefix(prefix);
 			}
-		};
 
-		abstract  Map createMap(Map elements);
+	};
+
+	abstract  Map createMap(Map elements);
 
-		abstract  Set> entriesWithPrefix(Map elements, String prefix);
+	abstract  Map entriesWithPrefix(Map elements, String prefix);
 
 	}
 
diff --git a/jargo/src/test/java/se/softhouse/common/collections/CharacterTrieTest.java b/jargo/src/test/java/se/softhouse/common/collections/CharacterTrieTest.java
index bce16bab..f00d4c92 100644
--- a/jargo/src/test/java/se/softhouse/common/collections/CharacterTrieTest.java
+++ b/jargo/src/test/java/se/softhouse/common/collections/CharacterTrieTest.java
@@ -16,6 +16,7 @@
 import static org.fest.assertions.MapAssert.entry;
 import static org.junit.Assert.fail;
 
+import java.util.Collection;
 import java.util.ConcurrentModificationException;
 import java.util.Iterator;
 import java.util.Map.Entry;
@@ -160,7 +161,7 @@ public void testFindingAllEntriesWithPrefix() throws Exception
 		trie.put("fooo", BAR);
 		trie.put("fooos", ZOO);
 
-		Set actualEntries = valuesInSet(trie.getEntriesWithPrefix("fooo"));
+		Collection actualEntries = trie.getEntriesWithPrefix("fooo").values();
 		assertThat(actualEntries).containsOnly(BAR, ZOO);
 	}
 
@@ -188,7 +189,7 @@ public void testThatFindingAllEntriesWithEmptyPrefixReturnsTheWholeTrie() throws
 		// Make sure equals of Entry isn't fooling us
 		CharacterTrie copy = CharacterTrie.newTrie(trie);
 
-		assertThat(trie.getEntriesWithPrefix("")).isEqualTo(copy.entrySet());
+		assertThat(trie.getEntriesWithPrefix("")).isEqualTo(copy.getEntriesWithPrefix(""));
 	}
 
 	@Test
diff --git a/jargo/src/test/java/se/softhouse/common/guavaextensions/Suppliers2Test.java b/jargo/src/test/java/se/softhouse/common/guavaextensions/Suppliers2Test.java
index 27f499d2..01fe0660 100644
--- a/jargo/src/test/java/se/softhouse/common/guavaextensions/Suppliers2Test.java
+++ b/jargo/src/test/java/se/softhouse/common/guavaextensions/Suppliers2Test.java
@@ -24,6 +24,9 @@
 import com.google.common.testing.NullPointerTester;
 import com.google.common.testing.NullPointerTester.Visibility;
 
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+import se.softhouse.common.testlib.Explanation;
+
 /**
  * Tests for {@link Suppliers2}
  */
@@ -73,6 +76,7 @@ public void testThatRepeatedElementsReturnCorrectNumberOfInstances()
 	}
 
 	@Test(expected = IllegalArgumentException.class)
+	@SuppressFBWarnings(value = "RV_RETURN_VALUE_IGNORED", justification = Explanation.FAIL_FAST)
 	public void testThatNegativeRepeatsIsIllegal()
 	{
 		Suppliers2.ofRepeatedElements(FOO_SUPPLIER, -1);
diff --git a/jargo/src/test/java/se/softhouse/common/numbers/NumberTypeTest.java b/jargo/src/test/java/se/softhouse/common/numbers/NumberTypeTest.java
index 9d295d35..9b698ffe 100644
--- a/jargo/src/test/java/se/softhouse/common/numbers/NumberTypeTest.java
+++ b/jargo/src/test/java/se/softhouse/common/numbers/NumberTypeTest.java
@@ -13,6 +13,7 @@
 package se.softhouse.common.numbers;
 
 import static java.lang.String.format;
+import static java.lang.System.lineSeparator;
 import static org.fest.assertions.Assertions.assertThat;
 import static org.fest.assertions.Fail.fail;
 import static se.softhouse.common.numbers.NumberType.BIG_DECIMAL;
@@ -22,7 +23,6 @@
 import static se.softhouse.common.numbers.NumberType.LONG;
 import static se.softhouse.common.numbers.NumberType.OUT_OF_RANGE;
 import static se.softhouse.common.numbers.NumberType.SHORT;
-import static se.softhouse.common.strings.StringsUtil.NEWLINE;
 import static se.softhouse.common.testlib.Locales.SWEDISH;
 
 import java.math.BigDecimal;
@@ -36,6 +36,9 @@
 import com.google.common.testing.NullPointerTester;
 import com.google.common.testing.NullPointerTester.Visibility;
 
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+import se.softhouse.common.testlib.Explanation;
+
 /**
  * Tests for {@link NumberType}
  */
@@ -96,6 +99,7 @@ public void testNumbersAtBounds()
 		}
 	}
 
+	@SuppressFBWarnings(value = "RV_RETURN_VALUE_IGNORED", justification = Explanation.FAIL_FAST)
 	private void testUnlimitedType(NumberType type)
 	{
 		try
@@ -142,6 +146,7 @@ public void testThatDescriptionOfValidValuesReturnsHumanReadableStrings() throws
 	}
 
 	@Test(expected = IllegalArgumentException.class)
+	@SuppressFBWarnings(value = "RV_RETURN_VALUE_IGNORED", justification = Explanation.FAIL_FAST)
 	public void testThatEmptyInputThrows() throws Exception
 	{
 		BYTE.parse("");
@@ -192,6 +197,7 @@ public void testThatToStringReturnsName()
 	}
 
 	@Test
+	@SuppressFBWarnings(value = "RV_RETURN_VALUE_IGNORED", justification = Explanation.FAIL_FAST)
 	public void testInvalidShortNumbers()
 	{
 		List invalidInput = Arrays.asList(Short.MIN_VALUE - 1, Short.MAX_VALUE + 1);
@@ -210,6 +216,7 @@ public void testInvalidShortNumbers()
 	}
 
 	@Test
+	@SuppressFBWarnings(value = "RV_RETURN_VALUE_IGNORED", justification = Explanation.FAIL_FAST)
 	public void testInvalidIntegerNumbers()
 	{
 		List invalidInput = Arrays.asList((long) Integer.MIN_VALUE - 1, (long) Integer.MAX_VALUE + 1);
@@ -228,6 +235,7 @@ public void testInvalidIntegerNumbers()
 	}
 
 	@Test
+	@SuppressFBWarnings(value = "RV_RETURN_VALUE_IGNORED", justification = Explanation.FAIL_FAST)
 	public void testInvalidLongNumbers()
 	{
 		List invalidInput = Arrays.asList(BigInteger.valueOf(Long.MIN_VALUE).subtract(BigInteger.ONE), biggerThanLong);
@@ -257,6 +265,7 @@ public void testThatNullContractsAreFollowed()
 	}
 
 	@Test
+	@SuppressFBWarnings(value = "RV_RETURN_VALUE_IGNORED", justification = Explanation.FAIL_FAST)
 	public void testThatUnparsableIntegerGeneratesProperErrorMessage() throws Exception
 	{
 		try
@@ -269,7 +278,7 @@ public void testThatUnparsableIntegerGeneratesProperErrorMessage() throws Except
 			/**
 			 * @formatter.off
 			 */
-			assertThat(e).hasMessage("'123a' is not a valid integer (Localization: English)" + NEWLINE +
+			assertThat(e).hasMessage("'123a' is not a valid integer (Localization: English)" + lineSeparator() +
 			                         "    ^");
 			/**
 			 * @formatter.on
@@ -278,6 +287,7 @@ public void testThatUnparsableIntegerGeneratesProperErrorMessage() throws Except
 	}
 
 	@Test
+	@SuppressFBWarnings(value = "RV_RETURN_VALUE_IGNORED", justification = Explanation.FAIL_FAST)
 	public void testThatUnparsableBigIntegerGeneratesProperErrorMessage() throws Exception
 	{
 		try
@@ -290,7 +300,7 @@ public void testThatUnparsableBigIntegerGeneratesProperErrorMessage() throws Exc
 			/**
 			 * @formatter.off
 			 */
-			assertThat(e).hasMessage("'12.3' is not a valid big-integer (Localization: English)" + NEWLINE +
+			assertThat(e).hasMessage("'12.3' is not a valid big-integer (Localization: English)" + lineSeparator() +
 			                         "   ^");
 			/**
 			 * @formatter.on
@@ -299,6 +309,7 @@ public void testThatUnparsableBigIntegerGeneratesProperErrorMessage() throws Exc
 	}
 
 	@Test
+	@SuppressFBWarnings(value = "RV_RETURN_VALUE_IGNORED", justification = Explanation.FAIL_FAST)
 	public void testThatDecimalSeparatorCausesParseErrorForDiscreetTypes() throws Exception
 	{
 		List> discreetTypes = Arrays.>asList(BYTE, SHORT, INTEGER, LONG, BIG_INTEGER);
diff --git a/jargo/src/test/java/se/softhouse/common/strings/DescribersTest.java b/jargo/src/test/java/se/softhouse/common/strings/DescribersTest.java
index 96453a6a..d5747584 100644
--- a/jargo/src/test/java/se/softhouse/common/strings/DescribersTest.java
+++ b/jargo/src/test/java/se/softhouse/common/strings/DescribersTest.java
@@ -12,26 +12,11 @@
  */
 package se.softhouse.common.strings;
 
-import com.google.common.testing.NullPointerTester;
-import com.google.common.testing.NullPointerTester.Visibility;
-import org.junit.Test;
-import se.softhouse.common.strings.Describers.BooleanDescribers;
-import se.softhouse.common.testlib.Locales;
-import se.softhouse.common.testlib.ResourceLoader;
-
-import java.io.File;
-import java.math.BigDecimal;
-import java.math.BigInteger;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-import java.util.Locale;
-import java.util.Map;
-
 import static com.google.common.collect.Maps.newLinkedHashMap;
+import static java.lang.System.lineSeparator;
 import static java.util.Arrays.asList;
 import static java.util.stream.Collectors.toList;
-import static org.fest.assertions.Assertions.*;
+import static org.fest.assertions.Assertions.assertThat;
 import static org.junit.Assert.fail;
 import static se.softhouse.common.strings.Describers.asFunction;
 import static se.softhouse.common.strings.Describers.booleanAsEnabledDisabled;
@@ -42,9 +27,28 @@
 import static se.softhouse.common.strings.Describers.numberDescriber;
 import static se.softhouse.common.strings.Describers.toStringDescriber;
 import static se.softhouse.common.strings.Describers.withConstantString;
-import static se.softhouse.common.strings.StringsUtil.NEWLINE;
 import static se.softhouse.common.testlib.Locales.TURKISH;
 
+import java.io.File;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+import org.junit.Test;
+
+import com.google.common.testing.NullPointerTester;
+import com.google.common.testing.NullPointerTester.Visibility;
+
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+import se.softhouse.common.strings.Describers.BooleanDescribers;
+import se.softhouse.common.testlib.Explanation;
+import se.softhouse.common.testlib.Locales;
+import se.softhouse.common.testlib.ResourceLoader;
+
 /**
  * Tests for {@link Describers}
  */
@@ -188,7 +192,7 @@ public void testMapDescriberForValuesAndKeysWithCustomSeparator()
 		Describer> customValueKeySeparator = mapDescriber(	Describers.withConstantString("foo"),
 																				Describers.withConstantString("bar"), ":");
 		String describedMap = customValueKeySeparator.describe(values, locale);
-		assertThat(describedMap).isEqualTo("foo:bar" + NEWLINE + "foo:bar");
+		assertThat(describedMap).isEqualTo("foo:bar" + lineSeparator() + "foo:bar");
 
 		assertThat(customValueKeySeparator.describe(Collections.emptyMap(), locale)).isEqualTo("Empty map");
 
@@ -205,10 +209,11 @@ public void testMapDescriberForValuesAndKeysWithCustomSeparator()
 		// Test with custom valueDescriber, custom keyDescriber
 		Describer> customValueAndKey = mapDescriber(	Describers.withConstantString("foo"),
 																			Describers.withConstantString("bar"));
-		assertThat(customValueAndKey.describe(values, locale)).contains("foo=bar" + NEWLINE + "foo=bar");
+		assertThat(customValueAndKey.describe(values, locale)).contains("foo=bar" + lineSeparator() + "foo=bar");
 	}
 
 	@Test
+	@SuppressFBWarnings(value = "RV_RETURN_VALUE_IGNORED", justification = Explanation.FAIL_FAST)
 	public void testThatMissingKeyThrows()
 	{
 		Map map = newLinkedHashMap();
diff --git a/jargo/src/test/java/se/softhouse/common/strings/StringsUtilTest.java b/jargo/src/test/java/se/softhouse/common/strings/StringsUtilTest.java
index 095cc0a2..d115ae5f 100644
--- a/jargo/src/test/java/se/softhouse/common/strings/StringsUtilTest.java
+++ b/jargo/src/test/java/se/softhouse/common/strings/StringsUtilTest.java
@@ -19,32 +19,26 @@
 import static se.softhouse.common.strings.StringsUtil.closestMatches;
 import static se.softhouse.common.strings.StringsUtil.numberToPositionalString;
 import static se.softhouse.common.strings.StringsUtil.pointingAtIndex;
-import static se.softhouse.common.strings.StringsUtil.spaces;
 
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
+import java.util.Locale;
+import java.util.SortedSet;
 
 import org.junit.Test;
 
-import se.softhouse.common.testlib.Explanation;
-
 import com.google.common.testing.NullPointerTester;
 import com.google.common.testing.NullPointerTester.Visibility;
 
 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+import se.softhouse.common.testlib.Explanation;
 
 /**
  * Tests for {@link StringsUtil}
  */
 public class StringsUtilTest
 {
-	@Test
-	public void testThatSpacesCreatesFiveSpaces()
-	{
-		assertThat(spaces(5)).isEqualTo("     ");
-	}
-
 	@Test
 	public void testLevenshteinDistance()
 	{
@@ -59,6 +53,22 @@ public void testLevenshteinDistance()
 		assertThat(StringsUtil.levenshteinDistance("hello", "hallo")).isEqualTo(1);
 	}
 
+	@Test
+	public void testThatAllPrefixesAreReturned() throws Exception
+	{
+		List strings = asList("logging", "logger", "mologger");
+		SortedSet prefixes = StringsUtil.prefixes("log", strings);
+		assertThat(prefixes).containsOnly("logging", "logger");
+	}
+
+	@Test
+	public void testThatAllPrefixesAreReturnedIgnoringCase() throws Exception
+	{
+		List strings = asList("logging", "logger", "mologger");
+		SortedSet prefixes = StringsUtil.prefixesIgnoringCase("Log", strings, Locale.US);
+		assertThat(prefixes).containsOnly("Logging", "Logger");
+	}
+
 	@Test
 	public void testLevenshteinDistanceWithADistance()
 	{
@@ -150,6 +160,7 @@ public void testThatPointingAtIndexProducesSpacesBeforeThePointer()
 	}
 
 	@Test(expected = IllegalArgumentException.class)
+	@SuppressFBWarnings(value = "RV_RETURN_VALUE_IGNORED", justification = Explanation.FAIL_FAST)
 	public void testThatNegativeNumberCantBePositional()
 	{
 		numberToPositionalString(-1);
diff --git a/jargo/src/test/java/se/softhouse/jargo/ArityArgumentTest.java b/jargo/src/test/java/se/softhouse/jargo/ArityArgumentTest.java
index 8af90e13..598dff5f 100644
--- a/jargo/src/test/java/se/softhouse/jargo/ArityArgumentTest.java
+++ b/jargo/src/test/java/se/softhouse/jargo/ArityArgumentTest.java
@@ -23,14 +23,14 @@
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
+import java.util.Set;
 
 import org.junit.Test;
 
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
 import se.softhouse.common.testlib.Explanation;
-import se.softhouse.jargo.CommandLineParserInstance.ArgumentIterator;
 import se.softhouse.jargo.internal.Texts.ProgrammaticErrors;
 import se.softhouse.jargo.internal.Texts.UserErrors;
-import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
 
 /**
  * Tests for {@link ArgumentBuilder#arity(int)} and {@link ArgumentBuilder#variableArity()}
@@ -72,6 +72,7 @@ public void testThatArityAndSplitWithIncompabilityIsEnforced()
 	}
 
 	@Test
+	@SuppressFBWarnings(value = "RV_RETURN_VALUE_IGNORED", justification = Explanation.FAIL_FAST)
 	public void testThatArityOfOneIsForbidden()
 	{
 		try
@@ -105,8 +106,12 @@ public void testThatUsageTextForArityLooksGood()
 		Argument> foo = stringArgument("--foo").arity(3).description("MetaDescShouldBeDisplayedThreeTimes").build();
 		Argument> bar = integerArgument("--bar").arity(2).description("MetaDescShouldBeDisplayedTwoTimes").build();
 		Argument> zoo = integerArgument("--zoo").variableArity().description("MetaDescShouldIndicateVariableAmount").build();
+		Argument trans = integerArgument("--trans").variableArity().transform(l -> l.size())
+				.description("MetaDescShouldIndicateVariableAmount").build();
+		Argument transTwo = integerArgument("--trans-two").arity(2).transform(l -> l.size()).description("MetaDescShouldBeDisplayedTwoTimes")
+				.build();
 		Argument> boo = integerArgument().variableArity().description("MetaDescShouldIndicateVariableAmount").build();
-		Usage usage = CommandLineParser.withArguments(foo, bar, zoo, boo).usage();
+		Usage usage = CommandLineParser.withArguments(foo, bar, zoo, trans, transTwo, boo).usage();
 		assertThat(usage).isEqualTo(expected("metaDescriptionsForArityArgument"));
 	}
 
@@ -120,7 +125,7 @@ public void testUsageTextForEmptyList()
 	@Test
 	public void testThatNrOfRemainingArgumentsGivesTheCorrectCapacity()
 	{
-		ArgumentIterator args = ArgumentIterator.forArguments(Arrays.asList("foo"), Collections.>emptyMap());
+		ArgumentIterator args = ArgumentIterator.forArguments(Arrays.asList("foo"));
 		assertThat(args.nrOfRemainingArguments()).isEqualTo(1);
 		args.next(); // Consume one argument
 		assertThat(args.nrOfRemainingArguments()).isEqualTo(0);
@@ -145,4 +150,11 @@ public void testThatTwoUnnamedVariableArityArgumentsIsIllegal()
 			assertThat(expected).hasMessage(String.format(ProgrammaticErrors.SEVERAL_VARIABLE_ARITY_PARSERS, "[, ]"));
 		}
 	}
+
+	@Test
+	public void testThatArityCanBeTransformedToUniqueValues()
+	{
+		Set numbers = integerArgument().variableArity().unique().parse("123", "24", "123");
+		assertThat(numbers).containsOnly(123, 24);
+	}
 }
diff --git a/jargo/src/test/java/se/softhouse/jargo/BashCompletionTest.java b/jargo/src/test/java/se/softhouse/jargo/BashCompletionTest.java
new file mode 100644
index 00000000..9f6e2190
--- /dev/null
+++ b/jargo/src/test/java/se/softhouse/jargo/BashCompletionTest.java
@@ -0,0 +1,221 @@
+/* Copyright 2018 jonatanjonsson
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ */
+package se.softhouse.jargo;
+
+import static java.util.Collections.emptyList;
+import static org.fest.assertions.Assertions.assertThat;
+import static se.softhouse.jargo.Arguments.enumArgument;
+import static se.softhouse.jargo.Arguments.stringArgument;
+
+import java.io.IOException;
+import java.nio.file.Paths;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.SortedSet;
+import java.util.TreeSet;
+import java.util.stream.Collectors;
+
+import org.junit.Test;
+
+import com.google.common.io.MoreFiles;
+
+import se.softhouse.common.strings.StringsUtil;
+import se.softhouse.common.testlib.Launcher;
+import se.softhouse.common.testlib.Launcher.LaunchedProgram;
+import se.softhouse.jargo.commands.Build.BuildTarget;
+import se.softhouse.jargo.commands.CommandWithArgument;
+import se.softhouse.jargo.commands.Commit.Repository;
+import se.softhouse.jargo.commands.Git;
+import se.softhouse.jargo.commands.Mvn;
+import se.softhouse.jargo.stringparsers.EnumArgumentTest.Action;
+
+/**
+ * Tests that the correct words are suggested for
+ * {@link Completers#bashCompleter(java.util.function.Supplier, java.util.function.Consumer, Runnable)
+ * bash-completion} support
+ */
+public class BashCompletionTest
+{
+	final CommandLineParser parser = CommandLineParser.withCommands(new Git(new Repository())).andArguments(Git.MESSAGE);
+
+	final List currentDirFiles;
+
+	public BashCompletionTest() throws IOException
+	{
+		currentDirFiles = MoreFiles.listFiles(Paths.get(".")).stream().map(p -> p.getFileName().toString()).collect(Collectors.toList());
+	}
+
+	@Test
+	public void testThatNoCompletionsDoesNothing() throws Exception
+	{
+		Argument arg = stringArgument("-j").build();
+		ParsedArguments parsedArguments = CommandLineParser.withArguments(arg).noCompleter().parse("-j", "hello");
+		assertThat(parsedArguments.get(arg)).isEqualTo("hello");
+	}
+
+	@Test
+	public void testThatCommandNameIsCompletedCorrectly() throws Exception
+	{
+		SortedSet suggestions = FakeCompleter.complete(parser, "git", "commi");
+		assertThat(suggestions).containsOnly("commit ");
+
+		suggestions = FakeCompleter.complete(parser, "git", "com");
+		assertThat(suggestions).containsOnly("commit ");
+
+		suggestions = FakeCompleter.complete(parser, "git", "lo");
+		assertThat(suggestions).containsOnly("log ");
+
+		suggestions = FakeCompleter.complete(parser, "--");
+		assertThat(suggestions).containsOnly("--message ");
+
+		suggestions = FakeCompleter.complete(parser, "");
+		assertThat(suggestions).containsOnly("--message ", "-m ", "git ");
+
+		suggestions = FakeCompleter.complete(parser, "git", "commit", "--author=j", "--no");
+		assertThat(suggestions).isEmpty();
+	}
+
+	@Test
+	public void testThatCompletingAnArgToACommandWorks() throws Exception
+	{
+		SortedSet suggestions = FakeCompleter.complete(parser, "git", "commit", "--am");
+		assertThat(suggestions).containsOnly("--amend ");
+	}
+
+	@Test
+	public void testThatCompletingARequiredArgForACommandWorks() throws Exception
+	{
+		SortedSet suggestions = FakeCompleter.complete(parser, "git", "commit", "--auth");
+		assertThat(suggestions).containsOnly("--author=");
+	}
+
+	@Test
+	public void testThatBothSubCommandsAndRegularArgAreSuggested() throws Exception
+	{
+		SortedSet suggestions = FakeCompleter.complete(parser, "git", "");
+		assertThat(suggestions).containsOnly("commit ", "log ", "merge ", "--message ", "-m ");
+	}
+
+	@Test
+	public void testThatArgsWithEqualsSeparatorAreNotSuggestedTwice() throws Exception
+	{
+		SortedSet suggestions = FakeCompleter.complete(parser, "git", "commit", "--author=");
+		assertThat(suggestions).isEmpty();
+	}
+
+	@Test
+	public void testThatCommandsWithRequiredArgsBeingCompletedAreCompletedCorrectly() throws Exception
+	{
+		Argument action = enumArgument(Action.class, "-a").separator("=").required().build();
+		CommandWithArgument commandWithArgument = new CommandWithArgument<>("command", action);
+
+		CommandLineParser commandParser = CommandLineParser.withCommands(commandWithArgument);
+		SortedSet suggestions = FakeCompleter.complete(commandParser, "command", "-a=");
+		assertThat(suggestions).containsOnly("start", "stop", "restart");
+	}
+
+	@Test
+	public void testThatBothArgsAndVariableArgsAreSuggestedBeforeEnteringVariableArgs() throws Exception
+	{
+		TreeSet expectedSuggestions = new TreeSet<>(currentDirFiles);
+		// Does not contain --author as it's already parsed
+		expectedSuggestions.addAll(Arrays.asList("--amend ", "--message ", "-m ", "log ", "merge "));
+
+		SortedSet suggestions = FakeCompleter.complete(parser, "git", "commit", "--author=j", "");
+		assertThat(suggestions).isEqualTo(expectedSuggestions);
+
+		// Once variable args have been given, no other args should be suggested
+		suggestions = FakeCompleter.complete(parser, "git", "commit", "--author=j", "file", "");
+		assertThat(suggestions).isEqualTo(new TreeSet<>(currentDirFiles));
+	}
+
+	@Test
+	public void testThatEmptyParameterWithNoCompletionsLeadsToNoSuggestions() throws Exception
+	{
+		SortedSet suggestions = FakeCompleter.complete(parser, "git", "commit", "--message", "");
+		assertThat(suggestions).isEmpty();
+	}
+
+	@Test
+	public void testThatEmptyParameterWithNoCompletionsLeadsToNoSuggestionsAlsoAfterRequiredArgs() throws Exception
+	{
+		SortedSet suggestions = FakeCompleter.complete(parser, "git", "commit", "--author=joj", "--message", "");
+		assertThat(suggestions).isEmpty();
+	}
+
+	@Test
+	public void testThatVariableArityArgsCanBeCompleted() throws Exception
+	{
+		CommandLineParser p = CommandLineParser.withArguments(enumArgument(Action.class).variableArity().build());
+		SortedSet suggestions = FakeCompleter.complete(p, "stop", "re");
+		assertThat(suggestions).containsOnly("restart");
+	}
+
+	@Test
+	public void testThatVariableArityArgsCanBeSuggestedSeveralTimes() throws Exception
+	{
+		SortedSet suggestions = FakeCompleter.complete(parser, "git", "commit", "--author=j", "pom.xml", "");
+		assertThat(suggestions).isEqualTo(new TreeSet<>(currentDirFiles));
+	}
+
+	@Test
+	public void testThatCustomCompleterIsUsedInsteadOfDefault() throws Exception
+	{
+		CommandLineParser p = CommandLineParser.withArguments(enumArgument(Action.class)
+				.completer((str) -> StringsUtil.prefixes(str, Collections.singleton("reab"))).variableArity().build());
+		SortedSet suggestions = FakeCompleter.complete(p, "stop", "re");
+		assertThat(suggestions).containsOnly("reab");
+
+		suggestions = FakeCompleter.complete(p, "stop", "restart", "r");
+		assertThat(suggestions).containsOnly("reab");
+	}
+
+	@Test
+	public void testThatSeveralCommandHierarchiesInTheSameParserSupportCompletions() throws Exception
+	{
+		Repository repo = new Repository();
+		BuildTarget target = new BuildTarget();
+		CommandLineParser twoCommandsParser = CommandLineParser.withCommands(new Git(repo), new Mvn(target));
+
+		SortedSet suggestions = FakeCompleter.complete(twoCommandsParser, "git", "log", "mvn", "log");
+		assertThat(suggestions).containsOnly("log ");
+	}
+
+	public static void main(String[] args)
+	{
+		Argument shouldNotParse = stringArgument("--comp").transform((a) -> {
+			if(a != null)
+			{
+				System.out.println(a);
+			}
+			return a;
+		}).build();
+		Argument stuff = stringArgument("--complete-me").build();
+		CommandLineParser.withArguments(shouldNotParse, stuff).parse(args);
+	}
+
+	@Test
+	public void testThatCompleterParsesArgumentsBeforeTheArgumentThatShouldBeCompleted() throws Exception
+	{
+		String[] args = {"--comp", "hello", "--compl"};
+		Map fakeEnv = FakeCompleter.fakeEnv("ProgramName", " ", args);
+		LaunchedProgram launchedProgram = Launcher.launch(emptyList(), fakeEnv, BashCompletionTest.class, args);
+		assertThat(launchedProgram.errors()).isEmpty();
+		assertThat(launchedProgram.exitCode()).isEqualTo(0);
+		assertThat(launchedProgram.output()).isEqualTo("hello" + System.lineSeparator() + "--complete-me " + System.lineSeparator());
+	}
+}
diff --git a/jargo/src/test/java/se/softhouse/jargo/CombinedOptionsTest.java b/jargo/src/test/java/se/softhouse/jargo/CombinedOptionsTest.java
index faab41d4..ac8cc056 100644
--- a/jargo/src/test/java/se/softhouse/jargo/CombinedOptionsTest.java
+++ b/jargo/src/test/java/se/softhouse/jargo/CombinedOptionsTest.java
@@ -19,6 +19,8 @@
 
 import org.junit.Test;
 
+import se.softhouse.jargo.commands.CommandWithTwoArguments;
+
 /**
  * Tests for a batch of short-named optional arguments.
  * For instance when "-fs" is used instead of "-f -s"
@@ -93,4 +95,15 @@ protected String commandName()
 		// Also verify that the argument is parsed by it's rightful recipient
 		assertThat(args.get(programArgument)).isEqualTo(1);
 	}
+
+	@Test
+	public void testThatBatchOfOptionFlagsWorksForCommand() throws Exception
+	{
+		Argument option = Arguments.optionArgument("-o").build();
+		Argument secondOption = Arguments.optionArgument("-s").build();
+		CommandWithTwoArguments runWithOptions = new CommandWithTwoArguments<>("run", option, secondOption);
+		CommandLineParser.withCommands(runWithOptions).parse("run", "-so");
+		assertThat(runWithOptions.parsedObject).isTrue();
+		assertThat(runWithOptions.parsedObjectTwo).isTrue();
+	}
 }
diff --git a/jargo/src/test/java/se/softhouse/jargo/CommandLineParserTest.java b/jargo/src/test/java/se/softhouse/jargo/CommandLineParserTest.java
index b9af3ea0..5ee472a2 100644
--- a/jargo/src/test/java/se/softhouse/jargo/CommandLineParserTest.java
+++ b/jargo/src/test/java/se/softhouse/jargo/CommandLineParserTest.java
@@ -141,7 +141,7 @@ public void testThatCloseMatchIsSuggestedForTypos()
 		}
 		catch(ArgumentException expected)
 		{
-			assertThat(expected).hasMessage(String.format(UserErrors.SUGGESTION, "-number", "--number"));
+			assertThat(expected).hasMessage(String.format(UserErrors.SUGGESTION, "-number", "--number "));
 		}
 	}
 
@@ -348,6 +348,7 @@ public void testThatAllArgumentsAreTreatedAsIndexedArgumentsAfterEndOfOptions()
 	}
 
 	@Test
+	@SuppressFBWarnings(value = "RV_RETURN_VALUE_IGNORED", justification = Explanation.FAIL_FAST)
 	public void testThatInvalidArgumentsAddedLaterOnDoesNotWreckTheExistingParser() throws Exception
 	{
 		Argument number = integerArgument("-n").build();
@@ -370,6 +371,7 @@ public void testThatInvalidArgumentsAddedLaterOnDoesNotWreckTheExistingParser()
 	}
 
 	@Test
+	@SuppressFBWarnings(value = "RV_RETURN_VALUE_IGNORED", justification = Explanation.FAIL_FAST)
 	public void testThatInvalidCommandsAddedLaterOnDoesNotWreckTheExistingParser() throws Exception
 	{
 		BuildTarget target = new BuildTarget();
@@ -419,6 +421,7 @@ public void testThatParserIsModifiableAfterFailedAddArgumentOperation() throws E
 	{
 		testThatNullDoesNotCauseOtherConcurrentUpdatesToFail(new ParserInvocation>(){
 			@Override
+			@SuppressFBWarnings(value = "RV_RETURN_VALUE_IGNORED", justification = Explanation.FAIL_FAST)
 			public void invoke(CommandLineParser parser, Argument value)
 			{
 				parser.andArguments(value);
@@ -522,6 +525,7 @@ public void testThatArgumentBuilderTransfersPropertiesWhenBuilderIsChanged() thr
 	}
 
 	@Test
+	@SuppressFBWarnings(value = "RV_RETURN_VALUE_IGNORED", justification = Explanation.FAIL_FAST)
 	public void testThatNamesAreNotAllowedToHaveSpacesInThem() throws Exception
 	{
 		try
diff --git a/jargo/src/test/java/se/softhouse/jargo/FakeCompleter.java b/jargo/src/test/java/se/softhouse/jargo/FakeCompleter.java
new file mode 100644
index 00000000..20ef5547
--- /dev/null
+++ b/jargo/src/test/java/se/softhouse/jargo/FakeCompleter.java
@@ -0,0 +1,70 @@
+/* Copyright 2018 jonatanjonsson
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ */
+package se.softhouse.jargo;
+
+import static org.fest.assertions.Assertions.assertThat;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.SortedSet;
+import java.util.concurrent.atomic.AtomicReference;
+
+import se.softhouse.jargo.ArgumentException;
+import se.softhouse.jargo.ArgumentExceptions;
+import se.softhouse.jargo.CommandLineParser;
+import se.softhouse.jargo.Completers;
+
+public final class FakeCompleter
+{
+	private FakeCompleter()
+	{
+	}
+
+	public static SortedSet completeWithSeparator(CommandLineParser parser, String separator, String ... args)
+	{
+		String programName = "program";
+		Map fakeEnv = fakeEnv(programName, separator, args);
+		AtomicReference> suggestions = new AtomicReference>(null);
+		try
+		{
+			parser.completer(Completers.bashCompleter(() -> fakeEnv, suggestions::set, () -> {
+				throw ArgumentExceptions.withMessage("Done with completions");
+			})).parse();
+		}
+		catch(ArgumentException expected)
+		{
+			if(!"Done with completions".equals(expected.getMessage()))
+				throw expected;
+		}
+		assertThat(suggestions.get()).describedAs("No suggestions set. Completer failure").isNotNull();
+		return suggestions.get();
+	}
+
+	public static SortedSet complete(CommandLineParser parser, String ... args)
+	{
+		return completeWithSeparator(parser, " ", args);
+	}
+
+	public static Map fakeEnv(String programName, String separator, String ... args)
+	{
+		Objects.requireNonNull(programName);
+		Map fakeEnv = new HashMap<>();
+		String compLine = programName + " " + String.join(separator, args);
+		fakeEnv.put("COMP_LINE", compLine);
+		fakeEnv.put("COMP_POINT", "" + compLine.length());
+		return fakeEnv;
+	}
+}
diff --git a/jargo/src/test/java/se/softhouse/jargo/IgnoringCaseTest.java b/jargo/src/test/java/se/softhouse/jargo/IgnoringCaseTest.java
index 867865ac..8f07f3de 100644
--- a/jargo/src/test/java/se/softhouse/jargo/IgnoringCaseTest.java
+++ b/jargo/src/test/java/se/softhouse/jargo/IgnoringCaseTest.java
@@ -15,9 +15,11 @@
 import static org.fest.assertions.Assertions.assertThat;
 import static se.softhouse.jargo.Arguments.integerArgument;
 import static se.softhouse.jargo.Arguments.optionArgument;
+import static se.softhouse.jargo.Arguments.stringArgument;
 import static se.softhouse.jargo.CommandLineParser.withArguments;
 
 import java.util.Map;
+import java.util.SortedSet;
 
 import org.junit.Test;
 
@@ -46,6 +48,13 @@ public void testWithPropertyMap() throws ArgumentException
 		assertThat(numbers.get("foo")).isNull();
 	}
 
+	@Test
+	public void testThatIgnoringCaseWorksForEmptySeparator() throws ArgumentException
+	{
+		String hello = stringArgument("-a").separator("").ignoreCase().parse("-Ahello");
+		assertThat(hello).isEqualTo("hello");
+	}
+
 	@Test
 	public void testWithPropertyMapNotIgnoringCase() throws ArgumentException
 	{
@@ -60,6 +69,16 @@ public void testWithPropertyMapNotIgnoringCase() throws ArgumentException
 		assertThat(parsedArguments.get(indexed)).isEqualTo("-Bbig=5");
 	}
 
+	@Test
+	public void testThatCompletingWorksForIgnoreCaseArguments() throws Exception
+	{
+		Argument name = stringArgument("--name").ignoreCase().build();
+		CommandLineParser parser = CommandLineParser.withArguments(name);
+
+		SortedSet suggestions = FakeCompleter.complete(parser, "--Na");
+		assertThat(suggestions).containsOnly("--Name ");
+	}
+
 	@Test(expected = ArgumentException.class)
 	public void testThatLowerCaseArgumentIsNotReturnedWhenNotIgnoringCase() throws ArgumentException
 	{
diff --git a/jargo/src/test/java/se/softhouse/jargo/IndexedArgumentTest.java b/jargo/src/test/java/se/softhouse/jargo/IndexedArgumentTest.java
index 13b7d8b8..0835b4ac 100644
--- a/jargo/src/test/java/se/softhouse/jargo/IndexedArgumentTest.java
+++ b/jargo/src/test/java/se/softhouse/jargo/IndexedArgumentTest.java
@@ -15,23 +15,22 @@
 import static org.fest.assertions.Assertions.assertThat;
 import static org.fest.assertions.Fail.fail;
 import static se.softhouse.jargo.Arguments.booleanArgument;
+import static se.softhouse.jargo.Arguments.enumArgument;
 import static se.softhouse.jargo.Arguments.integerArgument;
 import static se.softhouse.jargo.Arguments.stringArgument;
 import static se.softhouse.jargo.Arguments.withParser;
 import static se.softhouse.jargo.StringParsers.stringParser;
 
+import java.util.List;
 import java.util.Locale;
+import java.util.SortedSet;
 
 import org.junit.Test;
 
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
 import se.softhouse.common.testlib.Explanation;
-import se.softhouse.jargo.Argument;
-import se.softhouse.jargo.ArgumentException;
-import se.softhouse.jargo.CommandLineParser;
-import se.softhouse.jargo.ForwardingStringParser;
-import se.softhouse.jargo.ParsedArguments;
 import se.softhouse.jargo.internal.Texts.ProgrammaticErrors;
-import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+import se.softhouse.jargo.stringparsers.EnumArgumentTest.Action;
 
 /**
  * 
@@ -47,17 +46,17 @@
  */
 public class IndexedArgumentTest
 {
+	Argument enableLogging = booleanArgument().description("Output debug information to standard out").build();
+
+	Argument port = integerArgument().defaultValue(8080).description("The port to start the server on.").build();
+
+	Argument greetingPhrase = stringArgument().description("A greeting phrase to greet new connections with").build();
+
 	@Test
 	public void testIndexedArguments() throws ArgumentException
 	{
 		String[] args = {"true", "8090", "Hello"};
 
-		Argument enableLogging = booleanArgument().description("Output debug information to standard out").build();
-
-		Argument port = integerArgument().defaultValue(8080).description("The port to start the server on.").build();
-
-		Argument greetingPhrase = stringArgument().description("A greeting phrase to greet new connections with").build();
-
 		ParsedArguments arguments = CommandLineParser.withArguments(enableLogging, port, greetingPhrase).parse(args);
 
 		assertThat(arguments.get(enableLogging)).isTrue();
@@ -65,6 +64,35 @@ public void testIndexedArguments() throws ArgumentException
 		assertThat(arguments.get(greetingPhrase)).isEqualTo("Hello");
 	}
 
+	@Test
+	public void testThatOnlyCurrentlyIndexedArgumentIsSuggested() throws Exception
+	{
+		Argument action = enumArgument(Action.class).description("Output debug information to standard out").build();
+
+		CommandLineParser parser = CommandLineParser.withArguments(port, greetingPhrase, enableLogging, action);
+
+		SortedSet suggestions = FakeCompleter.complete(parser, "8080", "Hello!", "");
+		assertThat(suggestions).containsOnly("false", "true");
+
+		suggestions = FakeCompleter.complete(parser, "8080", "Hello!", "true", "");
+		assertThat(suggestions).containsOnly("start", "stop", "restart");
+	}
+
+	@Test
+	public void testThatSeveralIndexedArgumentsCanBeFinalized() throws Exception
+	{
+		Argument> firstActions = enumArgument(Action.class).arity(2).description("first").build();
+		Argument> secondActions = enumArgument(Action.class).arity(2).description("second").build();
+
+		CommandLineParser parser = CommandLineParser.withArguments(firstActions, secondActions);
+
+		ParsedArguments parsedArguments = parser.parse("start", "stop", "stop", "start");
+		assertThat(parsedArguments.get(firstActions)).containsExactly(Action.start, Action.stop);
+		assertThat(parsedArguments.get(secondActions)).containsExactly(Action.stop, Action.start);
+		SortedSet suggestions = FakeCompleter.complete(parser, "start", "stop", "stop", "sta");
+		assertThat(suggestions).containsOnly("start");
+	}
+
 	@Test
 	@SuppressFBWarnings(value = "RV_RETURN_VALUE_IGNORED", justification = Explanation.FAIL_FAST)
 	public void testThatIndexedArgumentThatIsRequiredIsGivenFirstBeforeAnyOptionalIndexedArguments()
@@ -90,17 +118,17 @@ public void testThatIndexedArgumentThatIsRequiredIsGivenFirstBeforeAnyOptionalIn
 	@SuppressFBWarnings(value = "RV_RETURN_VALUE_IGNORED", justification = Explanation.FAIL_FAST)
 	public void testThatRequiredIndexedArgumentsHaveUniqueMetaDescriptions()
 	{
-		Argument port = integerArgument().required().build();
+		Argument portArg = integerArgument().required().build();
 		Argument number = integerArgument().required().build();
 
 		try
 		{
-			CommandLineParser.withArguments(port, number);
+			CommandLineParser.withArguments(portArg, number);
 			fail("Non-unique meta description not detected");
 		}
 		catch(IllegalArgumentException expected)
 		{
-			assertThat(expected).hasMessage(String.format(ProgrammaticErrors.UNIQUE_METAS, port.metaDescriptionInRightColumn()));
+			assertThat(expected).hasMessage(String.format(ProgrammaticErrors.UNIQUE_METAS, portArg.metaDescriptionInRightColumn()));
 		}
 	}
 
diff --git a/jargo/src/test/java/se/softhouse/jargo/exceptions/NullPointerTest.java b/jargo/src/test/java/se/softhouse/jargo/NullPointerTest.java
similarity index 93%
rename from jargo/src/test/java/se/softhouse/jargo/exceptions/NullPointerTest.java
rename to jargo/src/test/java/se/softhouse/jargo/NullPointerTest.java
index e66c7552..4296ae10 100644
--- a/jargo/src/test/java/se/softhouse/jargo/exceptions/NullPointerTest.java
+++ b/jargo/src/test/java/se/softhouse/jargo/NullPointerTest.java
@@ -10,7 +10,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package se.softhouse.jargo.exceptions;
+package se.softhouse.jargo;
 
 import static org.fest.assertions.Assertions.assertThat;
 import static org.junit.Assert.fail;
@@ -24,19 +24,15 @@
 
 import org.junit.Test;
 
-import se.softhouse.jargo.Argument;
-import se.softhouse.jargo.ArgumentBuilder.DefaultArgumentBuilder;
-import se.softhouse.jargo.ArgumentException;
-import se.softhouse.jargo.CommandLineParser;
-import se.softhouse.jargo.ParsedArguments;
-import se.softhouse.jargo.commands.ProfilingExecuteCommand;
-
 import com.google.common.collect.ImmutableSet;
 import com.google.common.reflect.ClassPath;
 import com.google.common.reflect.ClassPath.ClassInfo;
 import com.google.common.testing.NullPointerTester;
 import com.google.common.testing.NullPointerTester.Visibility;
 
+import se.softhouse.jargo.ArgumentBuilder.DefaultArgumentBuilder;
+import se.softhouse.jargo.commands.ProfilingExecuteCommand;
+
 /**
  * Tests for {@link Nonnull} and {@link Nullable} arguments
  */
@@ -48,6 +44,8 @@ public void testThatNullContractsAreCheckedEagerly() throws ArgumentException, I
 		String packageName = Argument.class.getPackage().getName();
 		ImmutableSet classes = ClassPath.from(getClass().getClassLoader()).getTopLevelClasses(packageName);
 		NullPointerTester npeTester = new NullPointerTester();
+		npeTester.setDefault(CommandLineParser.class, CommandLineParser.withArguments());
+		npeTester.setDefault(ParsedArguments.class, new ParsedArguments(CommandLineParser.withArguments().parser()));
 		for(ClassInfo klazz : classes)
 		{
 			npeTester.testStaticMethods(klazz.load(), Visibility.PACKAGE);
diff --git a/jargo/src/test/java/se/softhouse/jargo/PackagePrivateTest.java b/jargo/src/test/java/se/softhouse/jargo/PackagePrivateTest.java
index 6a5ef093..a04848ee 100644
--- a/jargo/src/test/java/se/softhouse/jargo/PackagePrivateTest.java
+++ b/jargo/src/test/java/se/softhouse/jargo/PackagePrivateTest.java
@@ -12,33 +12,37 @@
  */
 package se.softhouse.jargo;
 
+import static java.lang.System.lineSeparator;
+import static org.fest.assertions.Assertions.assertThat;
+import static se.softhouse.common.testlib.UtilityClassTester.testUtilityClassDesign;
+import static se.softhouse.common.testlib.UtilityClassTester.testUtilityClassDesignForAllClassesAround;
+import static se.softhouse.jargo.Arguments.integerArgument;
+import static se.softhouse.jargo.Arguments.stringArgument;
+import static se.softhouse.jargo.ProgramInformation.withProgramName;
+import static se.softhouse.jargo.StringParsers.optionParser;
+import static se.softhouse.jargo.utils.Assertions2.assertThat;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Locale;
+import java.util.function.Function;
+
 import org.junit.Test;
+
+import se.softhouse.common.guavaextensions.Predicates2;
 import se.softhouse.common.numbers.NumberType;
 import se.softhouse.common.testlib.EnumTester;
 import se.softhouse.jargo.Argument.ParameterArity;
-import se.softhouse.jargo.CommandLineParserInstance.ArgumentIterator;
+import se.softhouse.jargo.StringParsers.StringParserBridge;
 import se.softhouse.jargo.StringParsers.StringStringParser;
+import se.softhouse.jargo.StringParsers.TransformingParser;
 import se.softhouse.jargo.Usage.Row;
 import se.softhouse.jargo.commands.Build;
 import se.softhouse.jargo.internal.Texts.ProgrammaticErrors;
 import se.softhouse.jargo.internal.Texts.UsageTexts;
 import se.softhouse.jargo.internal.Texts.UserErrors;
 
-import java.io.IOException;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.Locale;
-
-import static org.fest.assertions.Assertions.assertThat;
-import static se.softhouse.common.strings.StringsUtil.NEWLINE;
-import static se.softhouse.common.testlib.UtilityClassTester.testUtilityClassDesign;
-import static se.softhouse.common.testlib.UtilityClassTester.testUtilityClassDesignForAllClassesAround;
-import static se.softhouse.jargo.Arguments.integerArgument;
-import static se.softhouse.jargo.Arguments.stringArgument;
-import static se.softhouse.jargo.ProgramInformation.withProgramName;
-import static se.softhouse.jargo.StringParsers.optionParser;
-import static se.softhouse.jargo.utils.Assertions2.assertThat;
-
 /**
  * Tests implementation details that has no meaning in the public API but can serve other purposes
  * such as to ease debugging. These tests can't reside in the internal package for (obvious)
@@ -70,7 +74,8 @@ public void testParserToString()
 	@Test
 	public void testProgramInformationToString()
 	{
-		assertThat(withProgramName("name").programDescription("description").toString()).isEqualTo("name:" + NEWLINE + "description" + NEWLINE);
+		assertThat(withProgramName("name").programDescription("description").toString())
+				.isEqualTo("name:" + lineSeparator() + "description" + lineSeparator());
 	}
 
 	@Test
@@ -91,8 +96,7 @@ public void testCommandLineParserInstanceToString()
 	@Test
 	public void testArgumentIteratorToString()
 	{
-		assertThat(ArgumentIterator.forArguments(Arrays.asList("foobar"), Collections.>emptyMap()).toString())
-				.isEqualTo("[foobar]");
+		assertThat(ArgumentIterator.forArguments(Arrays.asList("foobar")).toString()).isEqualTo("Parsed: [], Current: null, Remaining: [foobar]");
 	}
 
 	@Test
@@ -112,7 +116,7 @@ public void testCommandToString()
 	public void testUsageToString()
 	{
 		assertThat(new Usage(Collections.>emptySet(), Locale.getDefault(), withProgramName("Program"), false).toString())
-				.isEqualTo("Usage: Program" + NEWLINE);
+				.isEqualTo("Usage: Program" + lineSeparator());
 	}
 
 	@Test
@@ -122,6 +126,16 @@ public void testThatOptionalArgumentDefaultsToTheGivenValue()
 		assertThat(optionParser(false).defaultValue()).isFalse();
 	}
 
+	@Test
+	public void testOtherwiseUnusedMethodsForTransformerParser() throws Exception
+	{
+		Argument arg = stringArgument("-s").build();
+		StringParserBridge parser = new StringParserBridge(StringParsers.stringParser());
+		TransformingParser transformingParser = new TransformingParser<>(parser, Function.identity(), Predicates2.alwaysTrue());
+		assertThat(transformingParser.defaultValue()).isEqualTo("");
+		assertThat(transformingParser.metaDescription(arg)).isEqualTo("");
+	}
+
 	@Test
 	public void testThatUsageReferenceCanOnlyBeSetOneTime()
 	{
diff --git a/jargo/src/test/java/se/softhouse/jargo/PropertyMapTest.java b/jargo/src/test/java/se/softhouse/jargo/PropertyMapTest.java
index 2844ce3d..682ded01 100644
--- a/jargo/src/test/java/se/softhouse/jargo/PropertyMapTest.java
+++ b/jargo/src/test/java/se/softhouse/jargo/PropertyMapTest.java
@@ -12,22 +12,6 @@
  */
 package se.softhouse.jargo;
 
-import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.Range;
-import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
-import org.junit.Test;
-import se.softhouse.common.testlib.Explanation;
-import se.softhouse.jargo.internal.Texts.ProgrammaticErrors;
-import se.softhouse.jargo.internal.Texts.UserErrors;
-import se.softhouse.jargo.limiters.LimiterTest;
-import se.softhouse.jargo.stringparsers.custom.LimitedKeyParser;
-import se.softhouse.jargo.stringparsers.custom.ObjectParser;
-
-import java.util.Arrays;
-import java.util.List;
-import java.util.Map;
-import java.util.function.Predicate;
-
 import static com.google.common.collect.Lists.newArrayList;
 import static com.google.common.collect.Maps.newLinkedHashMap;
 import static java.lang.String.format;
@@ -36,14 +20,35 @@
 import static org.fest.assertions.Fail.fail;
 import static se.softhouse.common.strings.Describers.withConstantString;
 import static se.softhouse.jargo.Arguments.byteArgument;
+import static se.softhouse.jargo.Arguments.enumArgument;
 import static se.softhouse.jargo.Arguments.integerArgument;
 import static se.softhouse.jargo.Arguments.stringArgument;
 import static se.softhouse.jargo.StringParsers.byteParser;
 import static se.softhouse.jargo.StringParsers.integerParser;
 import static se.softhouse.jargo.internal.Texts.UserErrors.DISALLOWED_PROPERTY_VALUE;
 import static se.softhouse.jargo.limiters.FooLimiter.foos;
-import static se.softhouse.jargo.utils.ExpectedTexts.expected;
 import static se.softhouse.jargo.utils.Assertions2.assertThat;
+import static se.softhouse.jargo.utils.ExpectedTexts.expected;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.SortedSet;
+import java.util.function.Predicate;
+
+import org.junit.Test;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Range;
+
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+import se.softhouse.common.testlib.Explanation;
+import se.softhouse.jargo.internal.Texts.ProgrammaticErrors;
+import se.softhouse.jargo.internal.Texts.UserErrors;
+import se.softhouse.jargo.limiters.LimiterTest;
+import se.softhouse.jargo.stringparsers.EnumArgumentTest.Action;
+import se.softhouse.jargo.stringparsers.custom.LimitedKeyParser;
+import se.softhouse.jargo.stringparsers.custom.ObjectParser;
 
 /**
  * Tests for {@link ArgumentBuilder#asPropertyMap()} and
@@ -325,6 +330,15 @@ public void testSeparatorInName() throws ArgumentException
 		assertThat(ten).isEqualTo(10);
 	}
 
+	@Test
+	public void testCompleteWhenSeparatorInName() throws ArgumentException
+	{
+		Argument> argument = enumArgument(Action.class, "-N").separator("-")
+				.asKeyValuesWithKeyParser(new LimitedKeyParser("foo", "bar")).build();
+		SortedSet suggestions = FakeCompleter.complete(CommandLineParser.withArguments(argument), "-N");
+		assertThat(suggestions).containsOnly("-Nfoo-", "-Nbar-");
+	}
+
 	@Test
 	public void testLimitWithForRepeatedPropertyValues()
 	{
@@ -399,4 +413,35 @@ public void testThatSystemPropertiesCanBeUsedAsTargetMap() throws Exception
 		assertThat(map.get("os.name")).as("Should delegate to system properties when not specified") //
 				.isEqualTo(System.getProperty("os.name"));
 	}
+
+	@Test
+	public void testThatOnlyValidKeysAreCompleted() throws ArgumentException
+	{
+		Argument> limited = integerArgument("-I") //
+				.asKeyValuesWithKeyParser(new LimitedKeyParser("foo", "bar")).build();
+		CommandLineParser parser = CommandLineParser.withArguments(limited);
+		SortedSet suggestions = FakeCompleter.complete(parser, "-If");
+		assertThat(suggestions).containsOnly("-Ifoo=");
+
+		suggestions = FakeCompleter.complete(parser, "-");
+		assertThat(suggestions).containsOnly("-I", "-Ifoo=", "-Ibar=");
+	}
+
+	@Test
+	public void testThatOnlyValidValuesAreCompleted() throws ArgumentException
+	{
+		Argument> limited = enumArgument(Action.class, "-I") //
+				.asKeyValuesWithKeyParser(new LimitedKeyParser("foo", "bar")).build();
+		SortedSet suggestions = FakeCompleter.completeWithSeparator(CommandLineParser.withArguments(limited), "=", "-Ifoo", "");
+		assertThat(suggestions).containsOnly("start", "stop", "restart");
+	}
+
+	@Test
+	public void testThatOnlyOneValidKeyContinuesToSuggestValuesForKey() throws ArgumentException
+	{
+		Argument> limited = enumArgument(Action.class, "-I") //
+				.asKeyValuesWithKeyParser(new LimitedKeyParser("foo")).build();
+		SortedSet suggestions = FakeCompleter.complete(CommandLineParser.withArguments(limited), "-If");
+		assertThat(suggestions).containsOnly("-Ifoo=", "-Ifoo=start", "-Ifoo=stop", "-Ifoo=restart");
+	}
 }
diff --git a/jargo/src/test/java/se/softhouse/jargo/RepeatedArgumentTest.java b/jargo/src/test/java/se/softhouse/jargo/RepeatedArgumentTest.java
index 47b9797d..fcce7b85 100644
--- a/jargo/src/test/java/se/softhouse/jargo/RepeatedArgumentTest.java
+++ b/jargo/src/test/java/se/softhouse/jargo/RepeatedArgumentTest.java
@@ -15,6 +15,7 @@
 import static com.google.common.collect.ImmutableList.of;
 import static org.fest.assertions.Assertions.assertThat;
 import static org.fest.assertions.Fail.fail;
+import static se.softhouse.jargo.Arguments.enumArgument;
 import static se.softhouse.jargo.Arguments.integerArgument;
 import static se.softhouse.jargo.Arguments.stringArgument;
 import static se.softhouse.jargo.limiters.FooLimiter.foos;
@@ -22,12 +23,15 @@
 import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
+import java.util.SortedSet;
 
 import org.junit.Test;
 
 import com.google.common.collect.ImmutableList;
 
 import se.softhouse.jargo.internal.Texts.UserErrors;
+import se.softhouse.jargo.stringparsers.EnumArgumentTest.Action;
 
 /**
  * Tests for {@link ArgumentBuilder#repeated()}
@@ -82,6 +86,25 @@ public void testRepeatedAndSplitPropertyValues() throws ArgumentException
 		assertThat(numberMap.get("number")).isEqualTo(expected);
 	}
 
+	@Test
+	public void testThatRepeatedCanBeTransformedToUniqueValues()
+	{
+		Set numbers = integerArgument("-n").arity(3).unique().parse("-n", "123", "24", "123");
+		assertThat(numbers).containsOnly(123, 24);
+		numbers = integerArgument("-n").variableArity().unique().parse("-n", "123", "24", "123", "1");
+		assertThat(numbers).containsOnly(123, 24, 1);
+		Set repeatedNumbers = integerArgument("-n").repeated().unique().parse("-n", "123", "-n", "24", "-n", "123");
+		assertThat(repeatedNumbers).containsOnly(123, 24);
+		try
+		{
+			repeatedNumbers.add(4);
+			fail("Returned data types should be immutable/unmodifiable");
+		}
+		catch(UnsupportedOperationException expected)
+		{
+		}
+	}
+
 	@Test
 	public void testRepeatedValuesWithoutHandling()
 	{
@@ -124,6 +147,24 @@ public void testThatListsWithRepeatedValuesAreUnmodifiable() throws ArgumentExce
 		}
 	}
 
+	@Test
+	public void testThatRepeatedValuesAreSuggestedEvenIfAlreadyGiven()
+	{
+		Argument> action = enumArgument(Action.class, "-a").repeated().build();
+		CommandLineParser parser = CommandLineParser.withArguments(action);
+		SortedSet suggestions = FakeCompleter.complete(parser, "-a", "start", "");
+		assertThat(suggestions).containsOnly("-a ");
+	}
+
+	@Test
+	public void testThatNonRepeatedValuesAreNotSuggestedIfAlreadyGiven()
+	{
+		Argument action = enumArgument(Action.class, "-a").build();
+		CommandLineParser parser = CommandLineParser.withArguments(action);
+		SortedSet suggestions = FakeCompleter.complete(parser, "-a", "start", "");
+		assertThat(suggestions).isEmpty();
+	}
+
 	@SuppressWarnings("deprecation")
 	// This is what's tested
 	@Test(expected = IllegalStateException.class)
diff --git a/jargo/src/test/java/se/softhouse/jargo/SeparatorTest.java b/jargo/src/test/java/se/softhouse/jargo/SeparatorTest.java
index 5f8aa4e6..3891820a 100644
--- a/jargo/src/test/java/se/softhouse/jargo/SeparatorTest.java
+++ b/jargo/src/test/java/se/softhouse/jargo/SeparatorTest.java
@@ -14,16 +14,19 @@
 
 import static org.fest.assertions.Assertions.assertThat;
 import static org.fest.assertions.Fail.fail;
+import static se.softhouse.jargo.Arguments.enumArgument;
 import static se.softhouse.jargo.Arguments.integerArgument;
 import static se.softhouse.jargo.Arguments.stringArgument;
 import static se.softhouse.jargo.utils.Assertions2.assertThat;
 
 import java.util.Arrays;
 import java.util.List;
+import java.util.SortedSet;
 
 import org.junit.Test;
 
 import se.softhouse.jargo.internal.Texts.UserErrors;
+import se.softhouse.jargo.stringparsers.EnumArgumentTest.Action;
 
 /**
  * Tests for {@link ArgumentBuilder#separator(String)}
@@ -127,4 +130,18 @@ public void testThatCustomSeparatorIsInSuggestions() throws Exception
 			assertThat(expected).hasMessage(String.format(UserErrors.SUGGESTION, "-n", "-n/"));
 		}
 	}
+
+	@Test
+	public void testThatEqualsAsSeparatorSuggestsForCurrentArg() throws Exception
+	{
+		Argument action = enumArgument(Action.class, "-a").separator("=").build();
+		CommandLineParser parser = CommandLineParser.withArguments(action);
+		SortedSet suggestions = FakeCompleter.completeWithSeparator(parser, "=", "-a", "");
+		assertThat(suggestions).containsOnly("start", "stop", "restart");
+
+		Argument s = stringArgument("-s").separator("=").build();
+		parser = CommandLineParser.withArguments(s);
+		suggestions = FakeCompleter.completeWithSeparator(parser, "", "-s=");
+		assertThat(suggestions).isEmpty();
+	}
 }
diff --git a/jargo/src/test/java/se/softhouse/jargo/TransformTest.java b/jargo/src/test/java/se/softhouse/jargo/TransformTest.java
new file mode 100644
index 00000000..eb1618f9
--- /dev/null
+++ b/jargo/src/test/java/se/softhouse/jargo/TransformTest.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright 2018 jonatanjonsson
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package se.softhouse.jargo;
+
+import static org.fest.assertions.Assertions.assertThat;
+import static org.fest.assertions.Fail.fail;
+import static se.softhouse.jargo.Arguments.integerArgument;
+
+import java.util.List;
+import java.util.Map;
+
+import org.junit.Test;
+
+import com.google.common.collect.ImmutableList;
+
+import se.softhouse.jargo.internal.Texts.UserErrors;
+import se.softhouse.jargo.limiters.ShortString;
+
+/**
+ * Tests for {@link ArgumentBuilder#transform(java.util.function.Function)}
+ */
+public class TransformTest
+{
+	@Test
+	public void testThatStringCanEasilyBeTransformed() throws Exception
+	{
+		int size = Arguments.stringArgument("--foo").transform(String::length).parse("--foo", "abcd");
+		assertThat(size).isEqualTo(4);
+	}
+
+	@Test
+	public void testThatStringCanBeLimitedAndThenTransformed() throws Exception
+	{
+		try
+		{
+			Arguments.stringArgument("--foo").limitTo(str -> str.length() < 10, "a string of max 10 characters").transform(String::length)
+					.parse("--foo", "abcdsdasdasdas");
+			fail("abcdsdasdasdas should be rejected as it is longer than 10 chars");
+		}
+		catch(ArgumentException expected)
+		{
+			assertThat(expected).hasMessage(String.format(UserErrors.DISALLOWED_VALUE, "abcdsdasdasdas", "a string of max 10 characters"));
+		}
+	}
+
+	@Test
+	public void testThatStringCanBeLimitedAndThenTransformedAndThenLimited() throws Exception
+	{
+		Argument tightInteger = Arguments.stringArgument("--foo").limitTo(str -> str.length() < 10) //
+				.transform(String::length) //
+				.limitTo((i) -> i >= 5).defaultValue(6).build();
+		try
+		{
+			tightInteger.parse("--foo", "abcdsdasasdad");
+			fail("abcdsdasasdad should be rejected as it is longer than 10 chars");
+		}
+		catch(ArgumentException expected)
+		{
+			assertThat(expected.getMessage()).contains("'abcdsdasasdad' is not se.softhouse.jargo.TransformTest$$Lambda$");
+		}
+		try
+		{
+			tightInteger.parse("--foo", "asdd");
+			fail("asdd should be rejected as it is smaller than 5 chars");
+		}
+		catch(ArgumentException expectedTwo)
+		{
+		}
+	}
+
+	@Test
+	public void testThatStringCanBeDefaultedAndThenTransformed() throws Exception
+	{
+		int defaultSize = Arguments.stringArgument("--foo").defaultValue("hej").transform(String::length).parse();
+		assertThat(defaultSize).isEqualTo(3);
+	}
+
+	@Test
+	public void testThatDefaultValuesCanBeDescribedAndTransformed() throws Exception
+	{
+		Usage usage = Arguments.stringArgument("--foo").defaultValue("hej") //
+				.defaultValueDescription("hej is a good word") //
+				.transform(String::length) //
+				.usage();
+		assertThat(usage.toString()).contains("hej is a good word");
+	}
+
+	@Test
+	public void testThatLimitIsCheckedForDefaultValuesBeforeBeingTransformed() throws Exception
+	{
+		try
+		{
+			Arguments.stringArgument("--foo").defaultValue("hej-foo-bar-zoo").limitTo(new ShortString()).transform(String::length).parse();
+			fail("hej is not less than 3 chars");
+		}
+		catch(IllegalArgumentException expected)
+		{
+			assertThat(expected).hasMessage("'hej-foo-bar-zoo' is not a string of max 10 characters");
+		}
+	}
+
+	@Test
+	public void testThatMetaDescriptionCanBeSpecifiedAfterOrBeforeTransformer() throws Exception
+	{
+		Usage usage = Arguments.stringArgument("--foo").metaDescription("").transform(String::length).usage();
+		assertThat(usage.toString()).contains("");
+	}
+
+	@Test
+	public void testThatArityCanThenBeTransformed() throws Exception
+	{
+		int size = Arguments.stringArgument("--foo").arity(2).transform(list -> list.stream().map(String::length).reduce(0, Integer::sum))
+				.parse("--foo", "bar", "zooo");
+		assertThat(size).isEqualTo(7);
+	}
+
+	@Test
+	public void testThatVariableArityCanBeTransformed() throws Exception
+	{
+		int size = Arguments.stringArgument("--foo").variableArity().transform(list -> list.stream().map(String::length).reduce(0, Integer::sum))
+				.parse("--foo", "bar", "zooo", "punk");
+		assertThat(size).isEqualTo(11);
+	}
+
+	@Test
+	public void testThatArityCanBeUsedAfterTransform() throws Exception
+	{
+		List sizes = Arguments.stringArgument("--foo").transform(String::length).arity(3).parse("--foo", "bar", "zooo", "punk");
+		assertThat(sizes).containsExactly(3, 4, 4);
+	}
+
+	@Test
+	public void testThatVariableArityCanBeUsedAfterTransform() throws Exception
+	{
+		List sizes = Arguments.stringArgument("--foo").transform(String::length).variableArity().parse("--foo", "bar", "zooo", "punk");
+		assertThat(sizes).containsExactly(3, 4, 4);
+	}
+
+	@Test
+	public void testThatDefaultValueIsCopiedWhenTransformIsRequested() throws Exception
+	{
+		Integer size = integerArgument("--trans").variableArity().transform(l -> l.size()).parse();
+		assertThat(size).isEqualTo(0);
+		size = integerArgument("--trans").variableArity().defaultValue(ImmutableList.of(3)).transform(l -> l.size()).parse();
+		assertThat(size).isEqualTo(1);
+	}
+
+	@Test
+	public void testThatSeparatorIsPreservedForAsPropertyMap() throws Exception
+	{
+		Argument> sysProps = Arguments.stringArgument("-D") //
+				.asPropertyMap() //
+				.transform(map -> {
+					map.forEach((key, value) -> System.setProperty(key, value));
+					return map;
+				}).build();
+		sysProps.parse("-Dhello=world");
+		assertThat(System.getProperty("hello")).isEqualTo("world");
+	}
+
+	// TODO(jontejj): test to transform with co-covariant types
+}
diff --git a/jargo/src/test/java/se/softhouse/jargo/UsageTest.java b/jargo/src/test/java/se/softhouse/jargo/UsageTest.java
index 722e1786..f01e5884 100644
--- a/jargo/src/test/java/se/softhouse/jargo/UsageTest.java
+++ b/jargo/src/test/java/se/softhouse/jargo/UsageTest.java
@@ -12,11 +12,10 @@
  */
 package se.softhouse.jargo;
 
+import static java.lang.System.lineSeparator;
 import static org.fest.assertions.Assertions.assertThat;
 import static org.fest.assertions.Fail.fail;
 import static org.fest.assertions.Fail.failure;
-import static se.softhouse.common.strings.StringsUtil.NEWLINE;
-import static se.softhouse.common.strings.StringsUtil.TAB;
 import static se.softhouse.common.testlib.Thrower.asUnchecked;
 import static se.softhouse.jargo.Arguments.integerArgument;
 import static se.softhouse.jargo.Arguments.stringArgument;
@@ -34,8 +33,10 @@
 
 import org.junit.Test;
 
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
 import se.softhouse.common.classes.Classes;
 import se.softhouse.common.strings.Describable;
+import se.softhouse.common.testlib.Explanation;
 import se.softhouse.common.testlib.Serializer;
 import se.softhouse.common.testlib.SimulatedException;
 import se.softhouse.jargo.ArgumentExceptions.UnexpectedArgumentException;
@@ -109,14 +110,14 @@ public void testUsageWithRepeatedArguments()
 	public void testUsageForNoArguments()
 	{
 		Usage usage = CommandLineParser.withArguments().programName("NoArguments").usage();
-		assertThat(usage).isEqualTo("Usage: NoArguments" + NEWLINE);
+		assertThat(usage).isEqualTo("Usage: NoArguments" + lineSeparator());
 	}
 
 	@Test
 	public void testUsageForNoVisibleArguments()
 	{
 		Usage usage = CommandLineParser.withArguments(integerArgument().hideFromUsage().build()).programName("NoVisibleArguments").usage();
-		assertThat(usage).isEqualTo("Usage: NoVisibleArguments" + NEWLINE);
+		assertThat(usage).isEqualTo("Usage: NoVisibleArguments" + lineSeparator());
 	}
 
 	@Test
@@ -147,7 +148,7 @@ public void testThatHiddenArgumentsAreParsable() throws ArgumentException
 	@Test
 	public void testUsageTextForRepeatedArgumentWithDefaultValueSet()
 	{
-		Usage usage = integerArgument().defaultValue(1).repeated().usage();
+		Usage usage = integerArgument("-n").defaultValue(1).repeated().usage();
 		assertThat(usage).contains(UsageTexts.DEFAULT_VALUE_START + "1").contains(UsageTexts.ALLOWS_REPETITIONS);
 	}
 
@@ -163,7 +164,7 @@ public void testArgumentNameSuggestions()
 		}
 		catch(ArgumentException expected)
 		{
-			assertThat(expected).hasMessage(String.format(UserErrors.SUGGESTION, "--namr", "--name" + NEWLINE + TAB + "--number"));
+			assertThat(expected).hasMessage(String.format(UserErrors.SUGGESTION, "--namr", "--name "));
 		}
 	}
 
@@ -240,7 +241,8 @@ public void testProgramDescriptionInUsage()
 	{
 		Usage usage = CommandLineParser.withArguments().programName("ProgramName").programDescription("Program description of ProgramName").usage();
 
-		assertThat(usage).isEqualTo("Usage: ProgramName" + NEWLINE + NEWLINE + "Program description of ProgramName" + NEWLINE);
+		assertThat(usage)
+				.isEqualTo("Usage: ProgramName" + lineSeparator() + lineSeparator() + "Program description of ProgramName" + lineSeparator());
 	}
 
 	@Test
@@ -257,6 +259,7 @@ public String description()
 	}
 
 	@Test
+	@SuppressFBWarnings(value = "RV_RETURN_VALUE_IGNORED", justification = Explanation.FAIL_FAST)
 	public void testThatUsageInformationIsLazilyInitialized() throws ArgumentException
 	{
 		Argument argument = withParser(new FailingMetaDescription()).names("-n").build();
@@ -334,7 +337,7 @@ public void testThatMoreNameRowsThanDescriptionRowsWorks() throws Exception
 	{
 		String extremelyLongArgumentName = longTextWithoutNewlines.replace(" ", "-");
 		Usage usage = integerArgument(extremelyLongArgumentName).usage();
-		assertThat(usage).endsWith("-est-laborum. " + NEWLINE);
+		assertThat(usage).endsWith("-est-laborum. " + lineSeparator());
 	}
 
 	private static final String longTextWithoutNewlines = "Lorem ipsum dolor sit amet, consectetur adipisicing elit, "
diff --git a/jargo/src/test/java/se/softhouse/jargo/commands/Build.java b/jargo/src/test/java/se/softhouse/jargo/commands/Build.java
index 88229002..dd7e0b24 100644
--- a/jargo/src/test/java/se/softhouse/jargo/commands/Build.java
+++ b/jargo/src/test/java/se/softhouse/jargo/commands/Build.java
@@ -13,6 +13,7 @@
 package se.softhouse.jargo.commands;
 
 import static org.fest.assertions.Assertions.assertThat;
+
 import se.softhouse.jargo.Command;
 import se.softhouse.jargo.ParsedArguments;
 
@@ -46,6 +47,7 @@ public static class BuildTarget
 	{
 		private boolean cleaned;
 		private boolean built;
+		private boolean logged;
 
 		void build()
 		{
@@ -70,10 +72,22 @@ boolean isBuilt()
 			return built;
 		}
 
+		boolean isLogged()
+		{
+			return logged;
+		}
+
+		void log()
+		{
+			this.logged = true;
+		}
+
 		void reset()
 		{
 			cleaned = false;
 			built = false;
+			logged = false;
 		}
+
 	}
 }
diff --git a/jargo/src/test/java/se/softhouse/jargo/commands/CommandTest.java b/jargo/src/test/java/se/softhouse/jargo/commands/CommandTest.java
index 954f4d64..ebf96425 100644
--- a/jargo/src/test/java/se/softhouse/jargo/commands/CommandTest.java
+++ b/jargo/src/test/java/se/softhouse/jargo/commands/CommandTest.java
@@ -15,8 +15,6 @@
 import static java.util.Collections.emptyList;
 import static org.fest.assertions.Assertions.assertThat;
 import static org.junit.Assert.fail;
-import static se.softhouse.common.strings.StringsUtil.NEWLINE;
-import static se.softhouse.common.strings.StringsUtil.TAB;
 import static se.softhouse.jargo.Arguments.command;
 import static se.softhouse.jargo.Arguments.stringArgument;
 import static se.softhouse.jargo.utils.Assertions2.assertThat;
@@ -25,6 +23,8 @@
 import java.io.File;
 import java.util.Arrays;
 import java.util.List;
+import java.util.SortedSet;
+import java.util.concurrent.atomic.AtomicReference;
 
 import org.junit.Test;
 
@@ -37,13 +37,17 @@
 import se.softhouse.jargo.Argument;
 import se.softhouse.jargo.ArgumentBuilder.CommandBuilder;
 import se.softhouse.jargo.ArgumentException;
+import se.softhouse.jargo.ArgumentExceptions;
+import se.softhouse.jargo.Arguments;
 import se.softhouse.jargo.Command;
 import se.softhouse.jargo.CommandLineParser;
+import se.softhouse.jargo.FakeCompleter;
 import se.softhouse.jargo.ParsedArguments;
 import se.softhouse.jargo.Usage;
 import se.softhouse.jargo.commands.Build.BuildTarget;
 import se.softhouse.jargo.commands.Commit.Repository;
 import se.softhouse.jargo.commands.Commit.Revision;
+import se.softhouse.jargo.internal.Texts.ProgrammaticErrors;
 import se.softhouse.jargo.internal.Texts.UserErrors;
 
 /**
@@ -125,24 +129,25 @@ public void testThatCombinedCommandsFromTheSameCommandLineBothAreExecuted() thro
 	/**
 	 * An alternative to {@link Command} that is based on interfaces instead
 	 */
-	public enum Service implements Runnable, Describable
+	public enum Service implements Runnable,Describable
 	{
 		START
 		{
-			@Override
-			public void run()
-			{
-				didStart = true;
-			}
 
-			@Override
-			public String description()
-			{
-				return "Starts the service";
-			}
-		};
+	@Override
+	public void run()
+	{
+		didStart = true;
+	}
+
+	@Override
+	public String description()
+	{
+		return "Starts the service";
 	}
 
+	}}
+
 	@Test
 	public void testMapOfCommands() throws Exception
 	{
@@ -172,6 +177,20 @@ public void testCommandWithMissingRequiredArgument()
 		}
 	}
 
+	@Test
+	public void testCommandWithRequiredArgumentAfterOtherArgs()
+	{
+		Repository repo = new Repository();
+		CommandLineParser parser = CommandLineParser.withCommands(new Git(repo)).andArguments(Git.MESSAGE);
+		ParsedArguments parsedArguments = parser.parse("git", "commit", "--amend", "--message", "b", "--author=a");
+
+		assertThat(parsedArguments.get(Git.MESSAGE)).isEqualTo("b");
+		Revision commit = repo.commits.get(0);
+		assertThat(commit.amend).isTrue();
+		assertThat(commit.author).isEqualTo("a");
+		assertThat(commit.files).isEqualTo(emptyList());
+	}
+
 	@Test
 	public void testThatUnhandledArgumentIsCaught()
 	{
@@ -323,7 +342,7 @@ public void testThatCorrectCommandIsMentionedInErrorMessage()
 		catch(ArgumentException expected)
 		{
 			assertThat(expected).hasMessage("Missing second  parameter for two_args");
-			assertThat(executedCommands).containsExactly(first, third);
+			assertThat(executedCommands).isEmpty();
 		}
 	}
 
@@ -358,7 +377,9 @@ public void testThatCommandIsExecutedOnlyOnce() throws ArgumentException
 	@Test
 	public void testRepeatedCommands() throws ArgumentException
 	{
-		assertThat(command(new Clean()).repeated().parse("clean", "clean", "clean")).hasSize(3);
+		Argument> repeatedCommand = command(new Clean()).repeated().build();
+		ParsedArguments parsedArguments = CommandLineParser.withArguments(repeatedCommand).parse("clean", "clean", "clean");
+		assertThat(parsedArguments.get(repeatedCommand)).hasSize(3);
 	}
 
 	@Test
@@ -380,7 +401,7 @@ public void testThatSuitableCommandArgumentAreSuggestedForMissspelling() throws
 		}
 		catch(ArgumentException expected)
 		{
-			assertThat(expected).hasMessage(String.format(UserErrors.SUGGESTION, "-limit", "--limit" + NEWLINE + TAB + "-l"));
+			assertThat(expected).hasMessage(String.format(UserErrors.SUGGESTION, "-limit", "--limit "));
 		}
 	}
 
@@ -394,24 +415,24 @@ public void testThatArgumentsToSubcommandsAreSuggested() throws Exception
 		}
 		catch(ArgumentException expected)
 		{
-			assertThat(expected).hasMessage(String.format(UserErrors.SUGGESTION, "cm", "cmd"));
+			assertThat(expected).hasMessage(String.format(UserErrors.SUGGESTION, "cm", "cmd "));
 		}
 	}
 
 	@Test
-	public void testThatMissspelledArgumentIsNotSuggestedForAlreadyExecutedCommand() throws Exception
+	public void testThatMissspelledArgumentIsSuggestedForAlreadyParsedCommand() throws Exception
 	{
 		CommandLineParser parser = CommandLineParser.withCommands(new CommandWithOneIndexedArgument(), new CommandWithTwoIndexedArguments());
 		try
 		{
-			// As one_arg already has been executed, by the time two_args is seen,
-			// suggesting --bool (optional argument to one_arg) would be an error
+			// As one_arg already has been parsed, by the time two_args is seen,
+			// suggesting --bool should be okay
 			parser.parse("one_arg", "1", "two_args", "1", "2", "-boo");
 			fail("-boo not detected as unhandled argument");
 		}
 		catch(ArgumentException expected)
 		{
-			assertThat(expected).hasMessage("Unexpected argument: -boo, previous argument: 2");
+			assertThat(expected).hasMessage(String.format(UserErrors.SUGGESTION, "-boo", "--bool "));
 		}
 
 	}
@@ -435,30 +456,13 @@ public void testThatInvalidParameterStopsExecuteFromBeingCalled() throws Excepti
 	}
 
 	@Test
-	public void testThatInvalidParameterDoesNotStopEarlierCommandsFromBeingExecuted() throws Exception
-	{
-		ProfilingExecuteCommand profilingCommand = new ProfilingExecuteCommand();
-		try
-		{
-			CommandLineParser.withArguments(command(profilingCommand).repeated().build()).parse("profile", "profile", "-limit");
-			fail("-limit should not be handled by profile command");
-		}
-		catch(ArgumentException expected)
-		{
-			assertThat(expected.getMessage()).startsWith("Unexpected argument: -limit");
-			assertThat(profilingCommand.numberOfCallsToExecute)
-					.as("profile should have been called once since previous commands should be executed once a new command is given").isEqualTo(1);
-		}
-	}
-
-	@Test
-	public void testThatSubcommandsAreExecutedBeforeMainCommands() throws Exception
+	public void testThatMainCommandsAreExecutedBeforeSubcommands() throws Exception
 	{
 		List executedCommands = Lists.newLinkedList();
 		ProfilingSubcommand profilingSubcommand = new ProfilingSubcommand(executedCommands);
 		CommandLineParser parser = CommandLineParser.withCommands(profilingSubcommand);
 		parser.parse("main", "c", "one_arg", "1");
-		assertThat(executedCommands).containsExactly(ProfilingSubcommand.subCommand, profilingSubcommand);
+		assertThat(executedCommands).containsExactly(profilingSubcommand, ProfilingSubcommand.subCommand);
 
 		assertThat(parser.usage()).isEqualTo(expected("commandWithSubCommand"));
 	}
@@ -466,8 +470,9 @@ public void testThatSubcommandsAreExecutedBeforeMainCommands() throws Exception
 	@Test
 	public void testThatCommandArgumentsCanBeFetchedAfterExecution() throws Exception
 	{
-		ParsedArguments commitArguments = COMMIT.parse("commit", "--author=joj");
-		assertThat(commitArguments.get(Commit.AUTHOR)).isEqualTo("joj");
+		ParsedArguments rootArgs = COMMIT.parse("commit", "--author=joj");
+
+		assertThat(rootArgs.get(COMMIT).get(Commit.AUTHOR)).isEqualTo("joj");
 	}
 
 	@Test
@@ -599,4 +604,305 @@ public void testThatInvalidArgumentPropertiesOnCommandIsDeprecated()
 		{
 		}
 	}
+
+	public static Argument MAIN_ARG = Arguments.stringArgument("-t").build();
+
+	@Test
+	public void testThatSubcommandsCanAccessArgumentsToMainCommandLineParser() throws Exception
+	{
+		SubcommandAccessingParentArgument subCommand = new SubcommandAccessingParentArgument();
+		CommandLineParser.withArguments(MAIN_ARG).andCommands(subCommand).parse("-t", "test", "sub");
+		assertThat(subCommand.mainArg).isEqualTo("test");
+	}
+
+	@Test
+	public void testThatSubcommandsCanAccessArgumentsToParentCommand() throws Exception
+	{
+		SubcommandAccessingParentArgument subCommand = new SubcommandAccessingParentArgument();
+		List> subCommandsOrArgs = Command.subCommands(subCommand);
+		subCommandsOrArgs.add(MAIN_ARG);
+		Command mainCommand = new Command(subCommandsOrArgs){
+			@Override
+			protected void execute(ParsedArguments parsedArguments)
+			{
+
+			}
+
+			@Override
+			protected String commandName()
+			{
+				return "main";
+			}
+		};
+
+		CommandLineParser.withCommands(mainCommand).parse("main", "-t", "test", "sub");
+		assertThat(subCommand.mainArg).isEqualTo("test");
+	}
+
+	@Test
+	public void testThatInvalidParameterStopsEarlierCommandsFromBeingExecuted() throws Exception
+	{
+		ProfilingExecuteCommand profilingCommand = new ProfilingExecuteCommand();
+		try
+		{
+			CommandLineParser.withArguments(command(profilingCommand).repeated().build()).parse("profile", "profile", "-limit");
+			fail("-limit should not be handled by profile command");
+		}
+		catch(ArgumentException expected)
+		{
+			assertThat(expected.getMessage()).startsWith("Unexpected argument: -limit");
+			assertThat(profilingCommand.numberOfCallsToExecute)
+					.as("profile should not have been called since either all commands should be executed or none").isZero();
+		}
+	}
+
+	@Test
+	public void testThatMissingArgsForMainStopsSubSubcommandFromBeingExecuted() throws Exception
+	{
+		Argument mainArg = Arguments.stringArgument("-a").required().build();
+		ProfilingExecuteCommand subcommand = new ProfilingExecuteCommand();
+		ProfilingExecuteCommand command = new ProfilingExecuteCommand(Command.subCommands(subcommand));
+
+		try
+		{
+			CommandLineParser.withArguments(mainArg).andCommands(command).parse("profile", "profile");
+			fail("Missing arg not detected");
+		}
+		catch(ArgumentException expected)
+		{
+			assertThat(subcommand.numberOfCallsToExecute).isZero();
+		}
+	}
+
+	@Test
+	public void testArgumentVisibilityBetweenMainArgAndSubArgAndThatRepeatedValuesAreRejected() throws Exception
+	{
+		SubcommandAccessingParentArgument subCommand = new SubcommandAccessingParentArgument();
+		List> subCommandsOrArgs = Command.subCommands(subCommand);
+		subCommandsOrArgs.add(MAIN_ARG);
+		AtomicReference mainCommandParsedArguments = new AtomicReference(null);
+		Command mainCommand = new Command(subCommandsOrArgs){
+			@Override
+			protected void execute(ParsedArguments parsedArguments)
+			{
+				mainCommandParsedArguments.set(parsedArguments);
+			}
+
+			@Override
+			protected String commandName()
+			{
+				return "main";
+			}
+		};
+
+		CommandLineParser parser = CommandLineParser.withArguments(MAIN_ARG).andCommands(mainCommand);
+		ParsedArguments parsedArguments = parser.parse("main", "-t", "1", "sub");
+		assertThat(subCommand.mainArg).isEqualTo("1");
+		assertThat(mainCommandParsedArguments.get().get(MAIN_ARG)).isEqualTo("1");
+		// The root command does not intersect with multiple levels of commands
+		assertThat(parsedArguments.get(MAIN_ARG)).isEqualTo("");
+
+		// Test that default values for main arguments work
+		parsedArguments = parser.parse("main", "sub");
+		assertThat(subCommand.mainArg).isEqualTo("");
+
+		parsedArguments = parser.parse("main", "sub", "-t", "2");
+		assertThat(subCommand.mainArg).isEqualTo("2");
+		// argument needs to be repeated for each subcommand on the same level
+		assertThat(parsedArguments.get(MAIN_ARG)).isEqualTo("");
+
+		try
+		{
+			parsedArguments = parser.parse("main", "-t", "1", "sub", "-t", "2");
+			fail("-t is not repeated. Should not be allowed");
+		}
+		catch(ArgumentException expected)
+		{
+			assertThat(expected).hasMessage(String.format(UserErrors.DISALLOWED_REPETITION, "-t"));
+		}
+
+		try
+		{
+			parser = CommandLineParser.withCommands(mainCommand);
+			parser.parse("main", "sub", "-t", "2").get(MAIN_ARG);
+			fail("MAIN_ARG is not part of the root args, so it shouldn't be visible there");
+		}
+		catch(IllegalArgumentException expected)
+		{
+			assertThat(expected).hasMessage(String.format(ProgrammaticErrors.ILLEGAL_ARGUMENT, "-t"));
+		}
+	}
+
+	@Test
+	public void testThatTwoSubcommandsCanUseTheSameArgumentNameAndGetDifferentValues() throws Exception
+	{
+		Argument commonArg = Arguments.stringArgument("-t").build();
+		CommandWithArgument commandOne = new CommandWithArgument<>("command-one", commonArg);
+		CommandWithArgument commandTwo = new CommandWithArgument("command-two", commonArg);
+		CommandLineParser parser = CommandLineParser.withCommands(commandOne, commandTwo);
+		parser.parse("command-one", "-t", "1", "command-two", "-t", "2");
+		assertThat(commandOne.parsedObject).isEqualTo("1");
+		assertThat(commandTwo.parsedObject).isEqualTo("2");
+	}
+
+	@Test
+	public void testThatCorrectNameAppearsForMissingArgToCommand() throws Exception
+	{
+		Argument firstArg = Arguments.stringArgument("-s").build();
+		Argument secondArg = Arguments.stringArgument("-t").required().build();
+		CommandWithTwoArguments command = new CommandWithTwoArguments<>("command", firstArg, secondArg);
+		CommandLineParser parser = CommandLineParser.withCommands(command);
+		try
+		{
+			parser.parse("command", "-s", "hello");
+			fail("-t should be reported as missing");
+		}
+		catch(ArgumentException expected)
+		{
+			assertThat(expected).hasMessage(String.format(UserErrors.MISSING_COMMAND_ARGUMENTS, "command", "[-t]"));
+		}
+	}
+
+	@Test
+	public void testThatCorrectNameAppearsForMissingIndexedArgToCommand() throws Exception
+	{
+		Argument firstArg = Arguments.stringArgument("-s").build();
+		Argument secondArg = Arguments.stringArgument().required().build();
+		CommandWithTwoArguments command = new CommandWithTwoArguments<>("command", firstArg, secondArg);
+		CommandLineParser parser = CommandLineParser.withCommands(command);
+		try
+		{
+			parser.parse("command", "-s", "hello");
+			fail("secondArg should be reported as missing");
+		}
+		catch(ArgumentException expected)
+		{
+			assertThat(expected).hasMessage(String.format(UserErrors.MISSING_COMMAND_ARGUMENTS, "command", "[]"));
+		}
+	}
+
+	@Test
+	public void testThatMainArgsCanBeSpecifiedInTheMiddleOfCommandArguments() throws Exception
+	{
+		Argument mainFirstArg = Arguments.stringArgument("--main").build();
+		Argument mainSecondArg = Arguments.stringArgument("-m").build();
+
+		Argument firstCommandArg = Arguments.stringArgument("--sub").build();
+		Argument secondCommandArg = Arguments.stringArgument("-s").build();
+		CommandWithTwoArguments command = new CommandWithTwoArguments<>("command", firstCommandArg, secondCommandArg);
+		CommandLineParser parser = CommandLineParser.withArguments(mainFirstArg, mainSecondArg).andCommands(command);
+		ParsedArguments parsedArguments = parser.parse("command", "--main", "1", "--sub", "2", "-m", "3", "-s", "4");
+		assertThat(command.parsedObject).isEqualTo("2");
+		assertThat(command.parsedObjectTwo).isEqualTo("4");
+		assertThat(parsedArguments.get(mainFirstArg)).isEqualTo("1");
+		assertThat(parsedArguments.get(mainSecondArg)).isEqualTo("3");
+	}
+
+	@Test
+	public void testThatSubcommandsAreSuggested() throws Exception
+	{
+		Repository repo = new Repository();
+		CommandLineParser git = CommandLineParser.withCommands(new Git(repo));
+		try
+		{
+			git.parse("git", "lo");
+			fail("log should be suggested");
+		}
+		catch(ArgumentException expected)
+		{
+			assertThat(expected).hasMessage(String.format(UserErrors.SUGGESTION, "lo", "log "));
+		}
+	}
+
+	@Test
+	public void testThatCompletingWorksForIgnoreCaseArgumentsForCommands() throws Exception
+	{
+		Argument name = stringArgument("--name").ignoreCase().build();
+		CommandWithArgument command = new CommandWithArgument<>("cmd", name);
+		CommandLineParser parser = CommandLineParser.withCommands(command);
+
+		SortedSet suggestions = FakeCompleter.complete(parser, "cmd", "--Na");
+		assertThat(suggestions).containsOnly("--Name ");
+	}
+
+	@Test
+	public void testThatDefaultValuesForMainArgsCanBeAccessedFromSubSubcommand() throws Exception
+	{
+		Repository repo = new Repository();
+		CommandLineParser git = CommandLineParser.withCommands(new Git(repo)).andArguments(Git.MESSAGE);
+		git.parse("git", "merge", "--author=merge-person@company.org");
+		assertThat(repo.commits).hasSize(1);
+		assertThat(repo.commits.get(0).message).isEqualTo("");
+	}
+
+	@Test
+	public void testThatLimiterErrorMessageLooksGoodForCommandArguments() throws Exception
+	{
+		Argument likesP = stringArgument("-p").limitTo(s -> s.contains("p"), "something with p").defaultValue("pppp").build();
+		CommandWithArgument command = new CommandWithArgument<>("limit", likesP);
+		CommandLineParser parser = CommandLineParser.withCommands(command);
+		try
+		{
+			parser.parse("limit", "-p", "no");
+			fail("no does not contain p, limiter did not detect it");
+		}
+		catch(ArgumentException expected)
+		{
+			assertThat(expected).hasMessage(String.format(UserErrors.DISALLOWED_VALUE, "no", "something with p"));
+		}
+	}
+
+	@Test
+	public void testThatRepeatableWorksWithCommands() throws Exception
+	{
+		ProfilingExecuteCommand repeatable = new ProfilingExecuteCommand();
+		ProfilingExecuteCommand notRepeatable = new ProfilingExecuteCommand();
+		CommandLineParser repeater = CommandLineParser.withArguments(	Arguments.command(repeatable).repeated().build(),
+																		Arguments.command(notRepeatable).names("-p").build());
+		repeater.parse("profile", "profile", "-p");
+		assertThat(repeatable.numberOfCallsToExecute).isEqualTo(2);
+		assertThat(notRepeatable.numberOfCallsToExecute).isEqualTo(1);
+
+		repeatable.numberOfCallsToExecute = 0;
+		notRepeatable.numberOfCallsToExecute = 0;
+		try
+		{
+			repeater.parse("profile", "profile", "-p", "-p");
+			fail("-p should not be allowed to be repeated");
+		}
+		catch(ArgumentException expected)
+		{
+			assertThat(expected).hasMessage(String.format(UserErrors.DISALLOWED_REPETITION, "-p"));
+			assertThat(repeatable.numberOfCallsToExecute).isEqualTo(0);
+			assertThat(notRepeatable.numberOfCallsToExecute).isEqualTo(0);
+
+		}
+	}
+
+	@Test
+	public void testThatThrownArgumentExceptionsFromExecuteHasUsageAttachedToThem() throws Exception
+	{
+		try
+		{
+			CommandLineParser.withCommands(new Command(){
+
+				@Override
+				protected void execute(ParsedArguments parsedArguments)
+				{
+					throw ArgumentExceptions.withMessage("Catch me");
+				}
+
+				@Override
+				protected String commandName()
+				{
+					return "catcher";
+				}
+			}).parse("catcher");
+			fail("Thrown exception was suppressed");
+		}
+		catch(ArgumentException expected)
+		{
+			assertThat(expected.getMessageAndUsage()).isEqualTo(expected("exceptionFromCommand"));
+		}
+	}
 }
diff --git a/jargo/src/test/java/se/softhouse/jargo/commands/CommandWithArgument.java b/jargo/src/test/java/se/softhouse/jargo/commands/CommandWithArgument.java
new file mode 100644
index 00000000..59634dd0
--- /dev/null
+++ b/jargo/src/test/java/se/softhouse/jargo/commands/CommandWithArgument.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2013 Jonatan Jönsson
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package se.softhouse.jargo.commands;
+
+import se.softhouse.jargo.Argument;
+import se.softhouse.jargo.Command;
+import se.softhouse.jargo.ParsedArguments;
+
+public class CommandWithArgument extends Command
+{
+	private final Argument arg;
+
+	public T parsedObject;
+
+	private final String name;
+
+	public CommandWithArgument(String name, Argument arg)
+	{
+		super(arg);
+		this.name = name;
+		this.arg = arg;
+	}
+
+	@Override
+	protected String commandName()
+	{
+		return name;
+	}
+
+	@Override
+	public String description()
+	{
+		return "A command that parses a single argument";
+	}
+
+	@Override
+	protected void execute(ParsedArguments args)
+	{
+		parsedObject = args.get(arg);
+	}
+}
diff --git a/jargo/src/test/java/se/softhouse/jargo/commands/CommandWithTwoArguments.java b/jargo/src/test/java/se/softhouse/jargo/commands/CommandWithTwoArguments.java
new file mode 100644
index 00000000..49763a3c
--- /dev/null
+++ b/jargo/src/test/java/se/softhouse/jargo/commands/CommandWithTwoArguments.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2013 Jonatan Jönsson
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package se.softhouse.jargo.commands;
+
+import se.softhouse.jargo.Argument;
+import se.softhouse.jargo.Command;
+import se.softhouse.jargo.ParsedArguments;
+
+public class CommandWithTwoArguments extends Command
+{
+	private final Argument arg;
+
+	private final Argument arg2;
+
+	public T parsedObject;
+
+	public T2 parsedObjectTwo;
+
+	private final String name;
+
+	public CommandWithTwoArguments(String name, Argument arg, Argument arg2)
+	{
+		super(arg, arg2);
+		this.name = name;
+		this.arg = arg;
+		this.arg2 = arg2;
+	}
+
+	@Override
+	protected String commandName()
+	{
+		return name;
+	}
+
+	@Override
+	public String description()
+	{
+		return "A command that parses two arguments";
+	}
+
+	@Override
+	protected void execute(ParsedArguments args)
+	{
+		parsedObject = args.get(arg);
+		parsedObjectTwo = args.get(arg2);
+	}
+}
diff --git a/jargo/src/test/java/se/softhouse/jargo/commands/Commit.java b/jargo/src/test/java/se/softhouse/jargo/commands/Commit.java
index 70b01854..f497c29b 100644
--- a/jargo/src/test/java/se/softhouse/jargo/commands/Commit.java
+++ b/jargo/src/test/java/se/softhouse/jargo/commands/Commit.java
@@ -19,12 +19,12 @@
 import java.io.File;
 import java.util.List;
 
+import com.google.common.collect.Lists;
+
 import se.softhouse.jargo.Argument;
 import se.softhouse.jargo.Command;
 import se.softhouse.jargo.ParsedArguments;
 
-import com.google.common.collect.Lists;
-
 public class Commit extends Command
 {
 	public final Repository repository;
@@ -42,7 +42,7 @@ public Commit(final Repository repo)
 	@Override
 	protected void execute(final ParsedArguments parsedArguments)
 	{
-		repository.commits.add(new Revision(parsedArguments));
+		repository.commits.add(new Revision("Commited stuff", parsedArguments));
 	}
 
 	@Override
@@ -56,6 +56,12 @@ public static class Repository
 		List commits = Lists.newArrayList();
 
 		int logLimit = 10;
+
+		@Override
+		public String toString()
+		{
+			return "Limit: " + logLimit + "\n" + commits;
+		}
 	}
 
 	public static class Revision
@@ -63,12 +69,20 @@ public static class Revision
 		final List files;
 		final boolean amend;
 		final String author;
+		final String message;
 
-		public Revision(final ParsedArguments arguments)
+		public Revision(String message, final ParsedArguments arguments)
 		{
 			amend = arguments.get(AMEND);
 			files = arguments.get(FILES);
 			author = arguments.get(AUTHOR);
+			this.message = message;
+		}
+
+		@Override
+		public String toString()
+		{
+			return author + ":" + message + ":amend:" + amend + ":files:" + files;
 		}
 	}
 }
diff --git a/jargo/src/test/java/se/softhouse/jargo/commands/Git.java b/jargo/src/test/java/se/softhouse/jargo/commands/Git.java
new file mode 100644
index 00000000..7c8db5e6
--- /dev/null
+++ b/jargo/src/test/java/se/softhouse/jargo/commands/Git.java
@@ -0,0 +1,46 @@
+/* Copyright 2018 jonatanjonsson
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ */
+package se.softhouse.jargo.commands;
+
+import se.softhouse.jargo.Argument;
+import se.softhouse.jargo.Arguments;
+import se.softhouse.jargo.Command;
+import se.softhouse.jargo.ParsedArguments;
+import se.softhouse.jargo.commands.Commit.Repository;
+
+public class Git extends Command
+{
+	/**
+	 * An example of a globally defined argument that can be available in the sub commands that wants it
+	 */
+	public static final Argument MESSAGE = Arguments.stringArgument("--message", "-m").build();
+
+	public Git(Repository repo)
+	{
+		super(subCommands(new Commit(repo), new Log(repo), new Merge(repo)));
+	}
+
+	@Override
+	protected void execute(ParsedArguments parsedArguments)
+	{
+
+	}
+
+	@Override
+	protected String commandName()
+	{
+		return "git";
+	}
+}
diff --git a/jargo/src/test/java/se/softhouse/jargo/commands/Merge.java b/jargo/src/test/java/se/softhouse/jargo/commands/Merge.java
new file mode 100644
index 00000000..b8c6ed36
--- /dev/null
+++ b/jargo/src/test/java/se/softhouse/jargo/commands/Merge.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2013 Jonatan Jönsson
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package se.softhouse.jargo.commands;
+
+import se.softhouse.jargo.ParsedArguments;
+
+public class Merge extends Commit
+{
+	public Merge(final Repository repo)
+	{
+		super(repo);
+	}
+
+	@Override
+	protected void execute(final ParsedArguments parsedArguments)
+	{
+		repository.commits.add(new Revision(parsedArguments.get(Git.MESSAGE), parsedArguments));
+	}
+
+	@Override
+	public String description()
+	{
+		return "Merges to a repository";
+	}
+}
diff --git a/jargo/src/test/java/se/softhouse/jargo/commands/Mvn.java b/jargo/src/test/java/se/softhouse/jargo/commands/Mvn.java
new file mode 100644
index 00000000..5e0f5d89
--- /dev/null
+++ b/jargo/src/test/java/se/softhouse/jargo/commands/Mvn.java
@@ -0,0 +1,34 @@
+/* Copyright 2018 jonatanjonsson
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ */
+package se.softhouse.jargo.commands;
+
+import se.softhouse.jargo.Command;
+import se.softhouse.jargo.ParsedArguments;
+import se.softhouse.jargo.commands.Build.BuildTarget;
+
+public class Mvn extends Command
+{
+	public Mvn(BuildTarget target)
+	{
+		super(subCommands(new Build(target), new Clean(target), new MvnLog(target)));
+	}
+
+	@Override
+	protected void execute(ParsedArguments parsedArguments)
+	{
+
+	}
+
+}
diff --git a/jargo/src/test/java/se/softhouse/jargo/commands/MvnLog.java b/jargo/src/test/java/se/softhouse/jargo/commands/MvnLog.java
new file mode 100644
index 00000000..04dd3ee6
--- /dev/null
+++ b/jargo/src/test/java/se/softhouse/jargo/commands/MvnLog.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2013 Jonatan Jönsson
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package se.softhouse.jargo.commands;
+
+import se.softhouse.jargo.Command;
+import se.softhouse.jargo.ParsedArguments;
+import se.softhouse.jargo.commands.Build.BuildTarget;
+
+public class MvnLog extends Command
+{
+	final BuildTarget target;
+
+	public MvnLog(BuildTarget target)
+	{
+		this.target = target;
+	}
+
+	@Override
+	protected String commandName()
+	{
+		return "log";
+	}
+
+	@Override
+	public String description()
+	{
+		return "Log a target";
+	}
+
+	@Override
+	protected void execute(ParsedArguments parsedArguments)
+	{
+		target.log();
+	}
+}
diff --git a/jargo/src/test/java/se/softhouse/jargo/commands/ProfilingExecuteCommand.java b/jargo/src/test/java/se/softhouse/jargo/commands/ProfilingExecuteCommand.java
index 207f3300..f087bad2 100644
--- a/jargo/src/test/java/se/softhouse/jargo/commands/ProfilingExecuteCommand.java
+++ b/jargo/src/test/java/se/softhouse/jargo/commands/ProfilingExecuteCommand.java
@@ -12,6 +12,9 @@
  */
 package se.softhouse.jargo.commands;
 
+import java.util.List;
+
+import se.softhouse.jargo.Argument;
 import se.softhouse.jargo.Command;
 import se.softhouse.jargo.ParsedArguments;
 
@@ -19,6 +22,16 @@ public final class ProfilingExecuteCommand extends Command
 {
 	public int numberOfCallsToExecute;
 
+	public ProfilingExecuteCommand(Argument ... args)
+	{
+		super(args);
+	}
+
+	public ProfilingExecuteCommand(List> args)
+	{
+		super(args);
+	}
+
 	@Override
 	public String commandName()
 	{
diff --git a/jargo/src/test/java/se/softhouse/jargo/commands/SubcommandAccessingParentArgument.java b/jargo/src/test/java/se/softhouse/jargo/commands/SubcommandAccessingParentArgument.java
new file mode 100644
index 00000000..1ebd8ed8
--- /dev/null
+++ b/jargo/src/test/java/se/softhouse/jargo/commands/SubcommandAccessingParentArgument.java
@@ -0,0 +1,35 @@
+/* Copyright 2018 jonatan
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ */
+package se.softhouse.jargo.commands;
+
+import se.softhouse.jargo.Command;
+import se.softhouse.jargo.ParsedArguments;
+
+public class SubcommandAccessingParentArgument extends Command
+{
+	String mainArg;
+
+	@Override
+	protected void execute(ParsedArguments parsedArguments)
+	{
+		mainArg = parsedArguments.get(CommandTest.MAIN_ARG);
+	}
+
+	@Override
+	protected String commandName()
+	{
+		return "sub";
+	}
+}
diff --git a/jargo/src/test/java/se/softhouse/jargo/limiters/LimiterTest.java b/jargo/src/test/java/se/softhouse/jargo/limiters/LimiterTest.java
index d4584c20..4ee4dc47 100644
--- a/jargo/src/test/java/se/softhouse/jargo/limiters/LimiterTest.java
+++ b/jargo/src/test/java/se/softhouse/jargo/limiters/LimiterTest.java
@@ -12,28 +12,36 @@
  */
 package se.softhouse.jargo.limiters;
 
+import static org.fest.assertions.Assertions.assertThat;
+import static org.fest.assertions.Fail.fail;
+import static se.softhouse.jargo.Arguments.integerArgument;
+import static se.softhouse.jargo.Arguments.stringArgument;
+import static se.softhouse.jargo.Arguments.withParser;
+import static se.softhouse.jargo.limiters.FooLimiter.foos;
+import static se.softhouse.jargo.utils.Assertions2.assertThat;
+import static se.softhouse.jargo.utils.ExpectedTexts.expected;
+
+import java.util.Arrays;
+import java.util.SortedSet;
+import java.util.function.Predicate;
+
+import org.junit.Test;
+
 import com.google.common.collect.Range;
+
 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
-import org.junit.Test;
+import se.softhouse.common.strings.Describables;
+import se.softhouse.common.strings.StringsUtil;
 import se.softhouse.common.testlib.Explanation;
 import se.softhouse.jargo.Argument;
 import se.softhouse.jargo.ArgumentBuilder;
 import se.softhouse.jargo.ArgumentException;
+import se.softhouse.jargo.CommandLineParser;
+import se.softhouse.jargo.FakeCompleter;
 import se.softhouse.jargo.Usage;
 import se.softhouse.jargo.stringparsers.custom.Port;
 import se.softhouse.jargo.stringparsers.custom.PortParser;
 
-import java.util.function.Predicate;
-
-import static org.fest.assertions.Assertions.assertThat;
-import static org.fest.assertions.Fail.fail;
-import static se.softhouse.jargo.Arguments.integerArgument;
-import static se.softhouse.jargo.Arguments.stringArgument;
-import static se.softhouse.jargo.Arguments.withParser;
-import static se.softhouse.jargo.limiters.FooLimiter.foos;
-import static se.softhouse.jargo.utils.Assertions2.assertThat;
-import static se.softhouse.jargo.utils.ExpectedTexts.expected;
-
 /**
  * Test for {@link ArgumentBuilder#limitTo(Predicate)}
  */
@@ -71,6 +79,23 @@ public void testThatSeveralLimiterAreCombinedIntoAnAndLimiter()
 		}
 	}
 
+	@Test
+	public void testThatValidValuesForSeveralLimitersCanBeSpecified()
+	{
+		try
+		{
+			integerArgument("-n") //
+					.limitTo(java(Range.closed(1, 2)), "between 1 and 2") //
+					.limitTo(java(Range.closed(0, 4)), Describables.withString("between 0 and 4")) //
+					.parse("-n", "3");
+			fail("3 should not be allowed, limiter override lost previously set limiter");
+		}
+		catch(ArgumentException expected)
+		{
+			assertThat(expected).hasMessage("'3' is not AND(between 1 and 2, between 0 and 4)");
+		}
+	}
+
 	@Test
 	public void testRepeatedWithLimiter()
 	{
@@ -101,6 +126,22 @@ public void testArityOfWithLimiter()
 		}
 	}
 
+	// Because it would require parsing of all suggestions which could be prohibitively expensive
+	@Test
+	public void testThatLimiterDoesNotAffectCompleter()
+	{
+		Argument onlyFoos = stringArgument("-i", "--index") //
+				.limitTo(foos()) //
+				.completer((str) -> StringsUtil.prefixes(str, Arrays.asList("foo", "bar"))) //
+				.build();
+		CommandLineParser parser = CommandLineParser.withArguments(onlyFoos);
+		SortedSet suggestions = FakeCompleter.complete(parser, "-i", "fo");
+		assertThat(suggestions).containsOnly("foo");
+
+		suggestions = FakeCompleter.complete(parser, "-i", "ba");
+		assertThat(suggestions).containsOnly("bar");
+	}
+
 	@Test(expected = ArgumentException.class)
 	public void testSplittingAndLimiting() throws ArgumentException
 	{
diff --git a/jargo/src/test/java/se/softhouse/jargo/nonfunctional/ConcurrencyTest.java b/jargo/src/test/java/se/softhouse/jargo/nonfunctional/ConcurrencyTest.java
index 11deeb59..67efa244 100644
--- a/jargo/src/test/java/se/softhouse/jargo/nonfunctional/ConcurrencyTest.java
+++ b/jargo/src/test/java/se/softhouse/jargo/nonfunctional/ConcurrencyTest.java
@@ -12,97 +12,29 @@
  */
 package se.softhouse.jargo.nonfunctional;
 
-import static java.util.Arrays.asList;
 import static org.fest.assertions.Assertions.assertThat;
-import static se.softhouse.jargo.Arguments.bigDecimalArgument;
-import static se.softhouse.jargo.Arguments.bigIntegerArgument;
-import static se.softhouse.jargo.Arguments.booleanArgument;
-import static se.softhouse.jargo.Arguments.byteArgument;
-import static se.softhouse.jargo.Arguments.charArgument;
-import static se.softhouse.jargo.Arguments.enumArgument;
-import static se.softhouse.jargo.Arguments.fileArgument;
-import static se.softhouse.jargo.Arguments.integerArgument;
-import static se.softhouse.jargo.Arguments.longArgument;
-import static se.softhouse.jargo.Arguments.optionArgument;
-import static se.softhouse.jargo.Arguments.shortArgument;
 import static se.softhouse.jargo.Arguments.stringArgument;
 import static se.softhouse.jargo.utils.Assertions2.assertThat;
 
-import java.io.File;
-import java.math.BigDecimal;
-import java.math.BigInteger;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Locale;
-import java.util.Map;
 import java.util.concurrent.TimeUnit;
 
-import org.fest.assertions.Description;
 import org.junit.Test;
 
+import net.jcip.annotations.NotThreadSafe;
 import se.softhouse.common.testlib.ConcurrencyTester;
 import se.softhouse.common.testlib.ConcurrencyTester.RunnableFactory;
 import se.softhouse.jargo.Argument;
 import se.softhouse.jargo.CommandLineParser;
 import se.softhouse.jargo.ParsedArguments;
-import se.softhouse.jargo.stringparsers.EnumArgumentTest.Action;
 import se.softhouse.jargo.utils.ExpectedTexts;
 
-import com.google.common.base.Strings;
-
 /**
  * Stress tests that verifies that a {@link CommandLineParser} can be used from several
  * {@link Thread}s concurrently
  */
-public class ConcurrencyTest
+@NotThreadSafe // Each test is parallel enough (signal to surefire to not run each test in parallel)
+public class ConcurrencyTest extends ExhaustiveProgram
 {
-	final Argument enableLoggingArgument = optionArgument("-l", "--enable-logging").description("Output debug information to standard out")
-			.build();
-
-	final Argument port = integerArgument("-p", "--listen-port").required().description("The port to start the server on.").build();
-
-	final Argument greetingPhraseArgument = stringArgument().required().description("A greeting phrase to greet new connections with")
-			.build();
-
-	final Argument longArgument = longArgument("--long").build();
-
-	final Argument shortArgument = shortArgument("--short").build();
-
-	final Argument byteArgument = byteArgument("--byte").build();
-
-	final Argument fileArgument = fileArgument("--file").defaultValueDescription("The current directory").build();
-
-	final Argument string = stringArgument("--string").build();
-
-	final Argument charArgument = charArgument("--char").build();
-
-	final Argument boolArgument = booleanArgument("--bool").build();
-
-	final Argument> propertyArgument = booleanArgument("-B").asPropertyMap().build();
-
-	final Argument> arityArgument = booleanArgument("--arity").arity(6).build();
-
-	final Argument> repeatedArgument = integerArgument("--repeated").repeated().build();
-
-	final Argument> splittedArgument = integerArgument("--split").separator("=").splitWith(",").build();
-
-	final Argument enumArgument = enumArgument(Action.class, "--enum").build();
-
-	final Argument> variableArityArgument = integerArgument("--variableArity").variableArity().build();
-
-	final Argument bigIntegerArgument = bigIntegerArgument("--big-integer").build();
-
-	final Argument bigDecimalArgument = bigDecimalArgument("--big-decimal").build();
-
-	/**
-	 * The shared instance that the different threads will use
-	 */
-	final CommandLineParser parser = CommandLineParser
-			.withArguments(	greetingPhraseArgument, enableLoggingArgument, port, longArgument, shortArgument, byteArgument, fileArgument, string,
-							charArgument, boolArgument, propertyArgument, arityArgument, repeatedArgument, splittedArgument, enumArgument,
-							variableArityArgument, bigIntegerArgument, bigDecimalArgument)
-			.programDescription("Example of most argument types that jargo can handle by default").locale(Locale.US);
-
 	final String expectedUsageText = ExpectedTexts.expected("allFeaturesInUsage");
 
 	private static final int timeoutInSeconds = 60;
@@ -151,134 +83,6 @@ public Runnable create(int uniqueNumber)
 		}, timeoutInSeconds, TimeUnit.SECONDS);
 	}
 
-	private final class ArgumentParseRunner implements Runnable
-	{
-		/**
-		 * The unique number each thread is assigned, used to differentiate results
-		 */
-		private final int offset;
-		private ParsedArguments parsedArguments;
-		private final String[] inputArgs;
-		private final int portNumber;
-		private final String greetingPhrase;
-		private final char c;
-		private final boolean bool;
-		private final String enableLogging;
-		private final short shortNumber;
-		private final byte byteNumber;
-		private final long longNumber;
-		private final BigInteger bigInteger;
-		private final String str;
-		private final String action;
-		private final Map propertyMap = new HashMap();
-		private final int amountOfVariableArity;
-		private final String variableArityIntegers;
-		private final List arityBooleans;
-		private final String arityString;
-		private final String filename;
-		private final File file;
-		private final BigDecimal bigDecimal;
-
-		private ArgumentParseRunner(int offset)
-		{
-			this.offset = offset;
-			portNumber = 8090 + offset;
-
-			greetingPhrase = "Hello" + offset;
-			c = charFor(offset);
-			bool = offset % 2 == 0;
-			enableLogging = bool ? "-l " : "";
-			shortNumber = (short) (1232 + offset);
-			byteNumber = (byte) (123 + offset);
-			longNumber = 1234567890L + offset;
-			bigInteger = BigInteger.valueOf(12312313212323L + offset);
-			str = "FooBar" + offset;
-			action = Action.values()[offset % Action.values().length].toString();
-
-			propertyMap.put("foo" + offset, true);
-			propertyMap.put("bar", false);
-
-			amountOfVariableArity = offset % 10;
-			variableArityIntegers = Strings.repeat(" " + portNumber, amountOfVariableArity);
-			arityBooleans = asList(bool, bool, bool, bool, bool, bool);
-			arityString = Strings.repeat(" " + bool, 6);
-
-			filename = "user_" + offset;
-			file = new File(filename);
-			bigDecimal = BigDecimal.valueOf(Long.MAX_VALUE);
-			String inputArguments = enableLogging + "-p " + portNumber + " " + greetingPhrase + " --long " + longNumber + " --big-integer "
-					+ bigInteger + " --short " + shortNumber + " --byte " + byteNumber + " --file " + filename + " --string " + str + " --char " + c
-					+ " --bool " + bool + " -Bfoo" + offset + "=true -Bbar=false" + " --arity" + arityString + " --repeated 1 --repeated " + offset
-					+ " --split=1," + (2 + offset) + ",3" + " --enum " + action + " --big-decimal " + bigDecimal + " --variableArity"
-					+ variableArityIntegers;
-			inputArgs = inputArguments.split(" ");
-		}
-
-		@Override
-		public void run()
-		{
-			parsedArguments = parser.parse(inputArgs);
-
-			checkThat(enableLoggingArgument).received(bool);
-			checkThat(port).received(portNumber);
-			checkThat(greetingPhraseArgument).received(greetingPhrase);
-			checkThat(longArgument).received(longNumber);
-			checkThat(bigIntegerArgument).received(bigInteger);
-			checkThat(shortArgument).received(shortNumber);
-			checkThat(byteArgument).received(byteNumber);
-			checkThat(fileArgument).received(file);
-			checkThat(string).received(str);
-			checkThat(charArgument).received(c);
-			checkThat(boolArgument).received(bool);
-			checkThat(arityArgument).received(arityBooleans);
-			checkThat(repeatedArgument).received(asList(1, offset));
-			checkThat(splittedArgument).received(asList(1, 2 + offset, 3));
-			checkThat(propertyArgument).received(propertyMap);
-			checkThat(enumArgument).received(Action.valueOf(action));
-			checkThat(bigDecimalArgument).received(bigDecimal);
-			assertThat(parsedArguments.get(variableArityArgument)).hasSize(amountOfVariableArity);
-		}
-
-		private char charFor(int o)
-		{
-			char result = (char) (o % Character.MAX_VALUE);
-			return result == ' ' ? '.' : result; // A space would be trimmed to nothing
-		}
-
-		/**
-		 * Verifies that an argument received an expected value
-		 */
-		public  Checker checkThat(Argument argument)
-		{
-			return new Checker(argument);
-		}
-
-		private class Checker
-		{
-			Argument arg;
-
-			public Checker(Argument argument)
-			{
-				arg = argument;
-			}
-
-			public void received(final T expectation)
-			{
-				final T parsedValue = parsedArguments.get(arg);
-				Description description = new Description(){
-					// In a concurrency test it makes a big performance difference
-					// with lazily created descriptions
-					@Override
-					public String value()
-					{
-						return "Failed to match: " + arg + ", actual: " + parsedValue + ", expected: " + expectation;
-					}
-				};
-				assertThat(parsedValue).as(description).isEqualTo(expectation);
-			}
-		}
-	}
-
 	@Test(timeout = (timeoutInSeconds + cleanupTime) * 1000)
 	public void testThatEndOfOptionsIsNotSharedBetweenParsers() throws Throwable
 	{
diff --git a/jargo/src/test/java/se/softhouse/jargo/nonfunctional/ExhaustiveProgram.java b/jargo/src/test/java/se/softhouse/jargo/nonfunctional/ExhaustiveProgram.java
new file mode 100644
index 00000000..b8c8035b
--- /dev/null
+++ b/jargo/src/test/java/se/softhouse/jargo/nonfunctional/ExhaustiveProgram.java
@@ -0,0 +1,233 @@
+/* Copyright 2018 jonatanjonsson
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ */
+package se.softhouse.jargo.nonfunctional;
+
+import static java.util.Arrays.asList;
+import static org.fest.assertions.Assertions.assertThat;
+import static se.softhouse.jargo.Arguments.bigDecimalArgument;
+import static se.softhouse.jargo.Arguments.bigIntegerArgument;
+import static se.softhouse.jargo.Arguments.booleanArgument;
+import static se.softhouse.jargo.Arguments.byteArgument;
+import static se.softhouse.jargo.Arguments.charArgument;
+import static se.softhouse.jargo.Arguments.enumArgument;
+import static se.softhouse.jargo.Arguments.fileArgument;
+import static se.softhouse.jargo.Arguments.integerArgument;
+import static se.softhouse.jargo.Arguments.longArgument;
+import static se.softhouse.jargo.Arguments.optionArgument;
+import static se.softhouse.jargo.Arguments.shortArgument;
+import static se.softhouse.jargo.Arguments.stringArgument;
+
+import java.io.File;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+import org.fest.assertions.Description;
+
+import com.google.common.base.Strings;
+
+import se.softhouse.jargo.Argument;
+import se.softhouse.jargo.CommandLineParser;
+import se.softhouse.jargo.ParsedArguments;
+import se.softhouse.jargo.stringparsers.EnumArgumentTest.Action;
+
+/**
+ * Gathers most of jargos features to be able to run orthogonal tests around every feature
+ */
+public class ExhaustiveProgram
+{
+	final Argument enableLoggingArgument = optionArgument("-l", "--enable-logging").description("Output debug information to standard out")
+			.build();
+
+	final Argument port = integerArgument("-p", "--listen-port").required().description("The port to start the server on.").build();
+
+	final Argument greetingPhraseArgument = stringArgument().required().description("A greeting phrase to greet new connections with")
+			.build();
+
+	final Argument longArgument = longArgument("--long").build();
+
+	final Argument shortArgument = shortArgument("--short").build();
+
+	final Argument byteArgument = byteArgument("--byte").build();
+
+	final Argument fileArgument = fileArgument("--file").defaultValueDescription("The current directory").build();
+
+	final Argument string = stringArgument("--string").build();
+
+	final Argument charArgument = charArgument("--char").build();
+
+	final Argument boolArgument = booleanArgument("--bool").build();
+
+	final Argument> propertyArgument = booleanArgument("-B").asPropertyMap().build();
+
+	final Argument> arityArgument = booleanArgument("--arity").arity(6).build();
+
+	final Argument> repeatedArgument = integerArgument("--repeated").repeated().build();
+
+	final Argument> splittedArgument = integerArgument("--split").separator("=").splitWith(",").build();
+
+	final Argument transformedArgument = stringArgument("--transformed").transform(String::length).build();
+
+	final Argument enumArgument = enumArgument(Action.class, "--enum").build();
+
+	final Argument> variableArityArgument = integerArgument("--variableArity").variableArity().build();
+
+	final Argument bigIntegerArgument = bigIntegerArgument("--big-integer").build();
+
+	final Argument bigDecimalArgument = bigDecimalArgument("--big-decimal").build();
+
+	/**
+	 * The shared instance that the different threads will use
+	 */
+	final CommandLineParser parser = CommandLineParser
+			.withArguments(	greetingPhraseArgument, enableLoggingArgument, port, longArgument, shortArgument, byteArgument, fileArgument, string,
+							charArgument, boolArgument, propertyArgument, arityArgument, repeatedArgument, splittedArgument, transformedArgument,
+							enumArgument, variableArityArgument, bigIntegerArgument, bigDecimalArgument)
+			.programDescription("Example of most argument types that jargo can handle by default").locale(Locale.US);
+
+	final class ArgumentParseRunner implements Runnable
+	{
+		/**
+		 * The unique number each thread is assigned, used to differentiate results
+		 */
+		private final int offset;
+		private ParsedArguments parsedArguments;
+		private final String[] inputArgs;
+		private final int portNumber;
+		private final String greetingPhrase;
+		private final char c;
+		private final boolean bool;
+		private final String enableLogging;
+		private final short shortNumber;
+		private final byte byteNumber;
+		private final long longNumber;
+		private final BigInteger bigInteger;
+		private final String str;
+		private final String action;
+		private final Map propertyMap = new HashMap();
+		private final int amountOfVariableArity;
+		private final String variableArityIntegers;
+		private final List arityBooleans;
+		private final String arityString;
+		private final String filename;
+		private final File file;
+		private final BigDecimal bigDecimal;
+		private final String transformString;
+
+		ArgumentParseRunner(int offset)
+		{
+			this.offset = offset;
+			portNumber = 8090 + offset;
+
+			greetingPhrase = "Hello" + offset;
+			c = charFor(offset);
+			bool = offset % 2 == 0;
+			enableLogging = bool ? "-l " : "";
+			shortNumber = (short) (1232 + offset);
+			byteNumber = (byte) (123 + offset);
+			longNumber = 1234567890L + offset;
+			bigInteger = BigInteger.valueOf(12312313212323L + offset);
+			str = "FooBar" + offset;
+			action = Action.values()[offset % Action.values().length].toString();
+
+			propertyMap.put("foo" + offset, true);
+			propertyMap.put("bar", false);
+
+			amountOfVariableArity = offset % 10;
+			variableArityIntegers = Strings.repeat(" " + portNumber, amountOfVariableArity);
+			arityBooleans = asList(bool, bool, bool, bool, bool, bool);
+			arityString = Strings.repeat(" " + bool, 6);
+
+			filename = "user_" + offset;
+			file = new File(filename);
+			bigDecimal = BigDecimal.valueOf(Long.MAX_VALUE);
+			transformString = Strings.repeat("a", offset % 50);
+			String inputArguments = enableLogging + "-p " + portNumber + " " + greetingPhrase + " --long " + longNumber + " --big-integer "
+					+ bigInteger + " --short " + shortNumber + " --byte " + byteNumber + " --file " + filename + " --string " + str + " --char " + c
+					+ " --bool " + bool + " -Bfoo" + offset + "=true -Bbar=false" + " --arity" + arityString + " --repeated 1 --repeated " + offset
+					+ " --split=1," + (2 + offset) + ",3" + " --transformed " + transformString + " --enum " + action + " --big-decimal " + bigDecimal
+					+ " --variableArity" + variableArityIntegers;
+			inputArgs = inputArguments.split(" ");
+		}
+
+		@Override
+		public void run()
+		{
+			parsedArguments = parser.parse(inputArgs);
+
+			checkThat(enableLoggingArgument).received(bool);
+			checkThat(port).received(portNumber);
+			checkThat(greetingPhraseArgument).received(greetingPhrase);
+			checkThat(longArgument).received(longNumber);
+			checkThat(bigIntegerArgument).received(bigInteger);
+			checkThat(shortArgument).received(shortNumber);
+			checkThat(byteArgument).received(byteNumber);
+			checkThat(fileArgument).received(file);
+			checkThat(string).received(str);
+			checkThat(charArgument).received(c);
+			checkThat(boolArgument).received(bool);
+			checkThat(arityArgument).received(arityBooleans);
+			checkThat(repeatedArgument).received(asList(1, offset));
+			checkThat(splittedArgument).received(asList(1, 2 + offset, 3));
+			checkThat(propertyArgument).received(propertyMap);
+			checkThat(enumArgument).received(Action.valueOf(action));
+			checkThat(bigDecimalArgument).received(bigDecimal);
+			assertThat(parsedArguments.get(variableArityArgument)).hasSize(amountOfVariableArity);
+			checkThat(transformedArgument).received(transformString.length());
+		}
+
+		private char charFor(int o)
+		{
+			char result = (char) (o % Character.MAX_VALUE);
+			return result == ' ' ? '.' : result; // A space would be trimmed to nothing
+		}
+
+		/**
+		 * Verifies that an argument received an expected value
+		 */
+		public  Checker checkThat(Argument argument)
+		{
+			return new Checker(argument);
+		}
+
+		private class Checker
+		{
+			Argument arg;
+
+			public Checker(Argument argument)
+			{
+				arg = argument;
+			}
+
+			public void received(final T expectation)
+			{
+				final T parsedValue = parsedArguments.get(arg);
+				Description description = new Description(){
+					// In a concurrency test it makes a big performance difference
+					// with lazily created descriptions
+					@Override
+					public String value()
+					{
+						return "Failed to match: " + arg + ", actual: " + parsedValue + ", expected: " + expectation;
+					}
+				};
+				assertThat(parsedValue).as(description).isEqualTo(expectation);
+			}
+		}
+	}
+}
diff --git a/jargo/src/test/java/se/softhouse/jargo/nonfunctional/SecurityTest.java b/jargo/src/test/java/se/softhouse/jargo/nonfunctional/SecurityTest.java
index fbc78979..41ece925 100644
--- a/jargo/src/test/java/se/softhouse/jargo/nonfunctional/SecurityTest.java
+++ b/jargo/src/test/java/se/softhouse/jargo/nonfunctional/SecurityTest.java
@@ -12,61 +12,40 @@
  */
 package se.softhouse.jargo.nonfunctional;
 
-import java.io.FilePermission;
-import java.net.NetPermission;
-import java.security.Permission;
-import java.util.PropertyPermission;
-import java.util.Set;
+import static java.util.Collections.emptyMap;
+import static org.fest.assertions.Assertions.assertThat;
 
-import com.google.common.collect.Sets;
+import org.junit.Test;
+
+import com.google.common.collect.ImmutableList;
+
+import se.softhouse.common.testlib.Launcher;
+import se.softhouse.common.testlib.Launcher.LaunchedProgram;
 
 /**
- * Tests that argument parser works with an extremely restrictive {@link SecurityManager} installed.
+ * Tests that argument parser works with the default {@link SecurityManager} activated.
  */
-public final class SecurityTest
+public final class SecurityTest extends ExhaustiveProgram
 {
-	private SecurityTest()
+	public static void main(String[] args) throws Throwable
 	{
+		new SecurityTest().run();
 	}
 
-	public static void main(String[] args) throws Throwable
+	private void run()
 	{
-		// TODO(jontejj): introduce this as a security test
-		System.setSecurityManager(new SecurityManager(){
-			@Override
-			public void checkPermission(Permission perm)
-			{
-				if(perm instanceof FilePermission)
-				{
-					// To load the java class
-					if(perm.getActions().equals("read"))
-						return;
-				}
-				else if(perm instanceof NetPermission)
-				{
-					// To load the java class
-					if(perm.getName().equals("specifyStreamHandler"))
-						return;
-				}
-				else if(perm instanceof RuntimePermission)
-				{
-					// To shutdown the executor
-					if(perm.getName().equals("modifyThread"))
-						return;
-				}
-				else if(perm instanceof PropertyPermission)
-				{
-					if(READABLE_PROPERTIES.contains(perm.getName()) && perm.getActions().equals("read"))
-						return;
-				}
-				throw new SecurityException("Permission: " + perm + " not granted");
-			}
-		});
-		ConcurrencyTest test = new ConcurrencyTest();
-		test.testThatDifferentArgumentsCanBeParsedConcurrently();
+		parser.noCompleter(); // Otherwise environment variables will be read
+		ArgumentParseRunner runner = new ArgumentParseRunner(1);
+		runner.run();
 	}
 
-	static final Set READABLE_PROPERTIES = Sets.newHashSet(	"user.timezone", "user.country", "java.home",
-																	"org.joda.time.DateTimeZone.Provider", "org.joda.time.DateTimeZone.NameProvider",
-																	"sun.timezone.ids.oldmapping", "os.name");
+	@Test
+	public void testThatProgramCanBeRunWithSecuritManagerActivated() throws Exception
+	{
+		ImmutableList secureVmArgs = ImmutableList.of(	"-Djava.security.manager",
+																"-Djava.security.policy=src/test/resources/jargo/security.policy");
+		LaunchedProgram launchedProgram = Launcher.launch(secureVmArgs, emptyMap(), SecurityTest.class, "");
+		assertThat(launchedProgram.errors()).isEmpty();
+		assertThat(launchedProgram.output()).isEmpty();
+	}
 }
diff --git a/jargo/src/test/java/se/softhouse/jargo/stringparsers/BigIntegerArgumentTest.java b/jargo/src/test/java/se/softhouse/jargo/stringparsers/BigIntegerArgumentTest.java
index 327f22a6..c59c8a48 100644
--- a/jargo/src/test/java/se/softhouse/jargo/stringparsers/BigIntegerArgumentTest.java
+++ b/jargo/src/test/java/se/softhouse/jargo/stringparsers/BigIntegerArgumentTest.java
@@ -12,8 +12,8 @@
  */
 package se.softhouse.jargo.stringparsers;
 
+import static java.lang.System.lineSeparator;
 import static org.fest.assertions.Assertions.assertThat;
-import static se.softhouse.common.strings.StringsUtil.NEWLINE;
 import static se.softhouse.jargo.Arguments.bigIntegerArgument;
 import static se.softhouse.jargo.utils.Assertions2.assertThat;
 
@@ -52,7 +52,7 @@ public void testInvalidInteger()
 			/**
 			 * @formatter.off
 			 */
-			assertThat(e).hasMessage("'1a' is not a valid big-integer (Localization: English (United States))" + NEWLINE +
+			assertThat(e).hasMessage("'1a' is not a valid big-integer (Localization: English (United States))" + lineSeparator() +
 			                         "  ^");
 			/**
 			 * @formatter.on
diff --git a/jargo/src/test/java/se/softhouse/jargo/stringparsers/BooleanArgumentTest.java b/jargo/src/test/java/se/softhouse/jargo/stringparsers/BooleanArgumentTest.java
index 86cee878..7a0f019c 100644
--- a/jargo/src/test/java/se/softhouse/jargo/stringparsers/BooleanArgumentTest.java
+++ b/jargo/src/test/java/se/softhouse/jargo/stringparsers/BooleanArgumentTest.java
@@ -16,10 +16,14 @@
 import static se.softhouse.jargo.Arguments.booleanArgument;
 import static se.softhouse.jargo.utils.Assertions2.assertThat;
 
+import java.util.SortedSet;
+
 import org.junit.Test;
 
 import se.softhouse.jargo.ArgumentException;
 import se.softhouse.jargo.Arguments;
+import se.softhouse.jargo.CommandLineParser;
+import se.softhouse.jargo.FakeCompleter;
 import se.softhouse.jargo.StringParsers;
 import se.softhouse.jargo.Usage;
 
@@ -55,4 +59,21 @@ public void testThatBooleanDefaultsToFalse() throws ArgumentException
 		boolean result = booleanArgument("-b").parse();
 		assertThat(result).isFalse();
 	}
+
+	@Test
+	public void testThatBooleansAreCompletedCorrectly() throws Exception
+	{
+		CommandLineParser parser = CommandLineParser.withArguments(booleanArgument().build());
+		SortedSet suggestions = FakeCompleter.complete(parser, "");
+		assertThat(suggestions).containsOnly("true", "false");
+
+		suggestions = FakeCompleter.complete(parser, "f");
+		assertThat(suggestions).containsOnly("false");
+
+		suggestions = FakeCompleter.complete(parser, "t");
+		assertThat(suggestions).containsOnly("true");
+
+		suggestions = FakeCompleter.complete(parser, "junk");
+		assertThat(suggestions).isEmpty();
+	}
 }
diff --git a/jargo/src/test/java/se/softhouse/jargo/stringparsers/EnumArgumentTest.java b/jargo/src/test/java/se/softhouse/jargo/stringparsers/EnumArgumentTest.java
index e749012f..190915b7 100644
--- a/jargo/src/test/java/se/softhouse/jargo/stringparsers/EnumArgumentTest.java
+++ b/jargo/src/test/java/se/softhouse/jargo/stringparsers/EnumArgumentTest.java
@@ -19,15 +19,20 @@
 import static se.softhouse.jargo.utils.Assertions2.assertThat;
 
 import java.util.List;
+import java.util.SortedSet;
+import java.util.concurrent.TimeUnit;
 
 import org.junit.Test;
 
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+import se.softhouse.jargo.Argument;
 import se.softhouse.jargo.ArgumentException;
 import se.softhouse.jargo.Arguments;
+import se.softhouse.jargo.CommandLineParser;
+import se.softhouse.jargo.FakeCompleter;
 import se.softhouse.jargo.StringParsers;
 import se.softhouse.jargo.Usage;
 import se.softhouse.jargo.internal.Texts.UserErrors;
-import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
 
 /**
  * Tests for {@link Arguments#enumArgument(Class, String...)} and
@@ -119,6 +124,31 @@ public void testInvalidEnumArgument()
 		}
 	}
 
+	@Test
+	public void testThatEnumValuesAreCompletedCorrectly() throws Exception
+	{
+		Argument action = enumArgument(Action.class, "--action").build();
+		CommandLineParser parser = CommandLineParser.withArguments(action);
+		SortedSet suggestions = FakeCompleter.complete(parser, "--action", "");
+		assertThat(suggestions).containsOnly("start", "stop", "restart");
+
+		Argument time = enumArgument(TimeUnit.class, "--time").build();
+		parser = CommandLineParser.withArguments(action, time);
+		suggestions = FakeCompleter.complete(parser, "--time", "");
+		// --action should not be included as the next argument should be a --time enum
+		assertThat(suggestions).containsOnly("NANOSECONDS", "MICROSECONDS", "MILLISECONDS", "SECONDS", "MINUTES", "HOURS", "DAYS");
+	}
+
+	@Test
+	public void testThatEnumValuesAreCompletedWithLowerCaseIfNeeded() throws Exception
+	{
+		Argument time = enumArgument(TimeUnit.class, "--time").build();
+		CommandLineParser parser = CommandLineParser.withArguments(time);
+
+		SortedSet suggestions = FakeCompleter.complete(parser, "--time", "sec");
+		assertThat(suggestions).containsOnly("seconds");
+	}
+
 	@Test
 	public void testThatValidEnumOptionsAreNotConstructedIfNotNeeded()
 	{
@@ -145,10 +175,9 @@ enum NefariousToString
 	{
 		ONE;
 
-		@Override
-		public String toString()
-		{
-			throw new IllegalStateException("Nefarious behavior not avoided");
-		}
+	@Override
+	public String toString()
+	{
+		throw new IllegalStateException("Nefarious behavior not avoided");
 	}
-}
+}}
diff --git a/jargo/src/test/java/se/softhouse/jargo/stringparsers/FileArgumentTest.java b/jargo/src/test/java/se/softhouse/jargo/stringparsers/FileArgumentTest.java
index e0c0dc7e..9f64347c 100644
--- a/jargo/src/test/java/se/softhouse/jargo/stringparsers/FileArgumentTest.java
+++ b/jargo/src/test/java/se/softhouse/jargo/stringparsers/FileArgumentTest.java
@@ -17,11 +17,14 @@
 import static se.softhouse.jargo.utils.Assertions2.assertThat;
 
 import java.io.File;
+import java.util.SortedSet;
 
 import org.junit.Test;
 
 import se.softhouse.jargo.ArgumentException;
 import se.softhouse.jargo.Arguments;
+import se.softhouse.jargo.CommandLineParser;
+import se.softhouse.jargo.FakeCompleter;
 import se.softhouse.jargo.StringParsers;
 import se.softhouse.jargo.Usage;
 
@@ -49,6 +52,16 @@ public void testThatFileArgumentsDefaultsToCurrentDirectory() throws ArgumentExc
 	{
 		File defaultFile = fileArgument("-f").parse();
 		assertThat(defaultFile).exists().isDirectory().isEqualTo(new File("."));
+	}
+
+	@Test
+	public void testThatFilesAreCompletedCorrectly() throws Exception
+	{
+		CommandLineParser parser = CommandLineParser.withArguments(fileArgument("-f").build());
+		SortedSet suggestions = FakeCompleter.complete(parser, "-f", "sr");
+		assertThat(suggestions).containsOnly("src");
 
+		suggestions = FakeCompleter.complete(parser, "-f", "non-existing");
+		assertThat(suggestions).isEmpty();
 	}
 }
diff --git a/jargo/src/test/java/se/softhouse/jargo/stringparsers/HelpArgumentTest.java b/jargo/src/test/java/se/softhouse/jargo/stringparsers/HelpArgumentTest.java
index 39c00fde..14a5b26b 100644
--- a/jargo/src/test/java/se/softhouse/jargo/stringparsers/HelpArgumentTest.java
+++ b/jargo/src/test/java/se/softhouse/jargo/stringparsers/HelpArgumentTest.java
@@ -15,6 +15,7 @@
 import static java.lang.String.format;
 import static org.fest.assertions.Assertions.assertThat;
 import static org.junit.Assert.fail;
+import static se.softhouse.jargo.Arguments.enumArgument;
 import static se.softhouse.jargo.Arguments.helpArgument;
 import static se.softhouse.jargo.Arguments.integerArgument;
 import static se.softhouse.jargo.Arguments.stringArgument;
@@ -22,17 +23,26 @@
 import static se.softhouse.jargo.utils.ExpectedTexts.expected;
 
 import java.util.Arrays;
+import java.util.SortedSet;
 
 import org.junit.Test;
 
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+import se.softhouse.common.testlib.Explanation;
 import se.softhouse.jargo.Argument;
 import se.softhouse.jargo.ArgumentException;
 import se.softhouse.jargo.Arguments;
 import se.softhouse.jargo.CommandLineParser;
+import se.softhouse.jargo.FakeCompleter;
+import se.softhouse.jargo.ParsedArguments;
 import se.softhouse.jargo.commands.Build;
+import se.softhouse.jargo.commands.CommandWithArgument;
 import se.softhouse.jargo.commands.CommandWithOneIndexedArgument;
+import se.softhouse.jargo.commands.Commit.Repository;
+import se.softhouse.jargo.commands.Git;
 import se.softhouse.jargo.internal.Texts.UsageTexts;
 import se.softhouse.jargo.internal.Texts.UserErrors;
+import se.softhouse.jargo.stringparsers.EnumArgumentTest.Action;
 
 /**
  * Tests for {@link Arguments#helpArgument(String, String...)}
@@ -187,6 +197,7 @@ public void testThatOneCommandParserCanProvideHelpWhileAnotherDoesNot()
 	}
 
 	@Test
+	@SuppressFBWarnings(value = "RV_RETURN_VALUE_IGNORED", justification = Explanation.FAIL_FAST)
 	public void testThatNameCollisionsForHelpArgumentAndOtherArgumentsAreDetected()
 	{
 		try
@@ -199,4 +210,46 @@ public void testThatNameCollisionsForHelpArgumentAndOtherArgumentsAreDetected()
 			assertThat(expected.getMessage()).isEqualTo("-h is handled by several arguments");
 		}
 	}
+
+	@Test
+	public void testThatHelpArgumentsCanBeCompleted() throws Exception
+	{
+		CommandLineParser parser = CommandLineParser.withArguments(HELP, enumArgument(Action.class, "-a").variableArity().build());
+		SortedSet suggestions = FakeCompleter.complete(parser, "-h", "-");
+
+		assertThat(suggestions).containsOnly("-a ");
+	}
+
+	@Test
+	public void testThatHelpForCommandsCanBeCompleted() throws Exception
+	{
+		CommandWithArgument command = new CommandWithArgument<>("cmd", stringArgument("--google").build());
+		CommandLineParser parser = CommandLineParser.withArguments(HELP).andCommands(command);
+		SortedSet suggestions = FakeCompleter.complete(parser, "cmd", "-h", "--g");
+
+		assertThat(suggestions).containsOnly("--google ");
+	}
+
+	@Test
+	public void testThatHelpForMainArgsWorkAfterCommandHasBeenSpecified() throws Exception
+	{
+		CommandLineParser parser = CommandLineParser.withArguments(HELP, Git.MESSAGE).andCommands(new Git(new Repository()));
+		try
+		{
+			parser.parse("git", "log", "-h", "--message");
+			fail("help argument should trigger an argument exception");
+		}
+		catch(ArgumentException expected)
+		{
+			assertThat(expected.getMessageAndUsage()).isEqualTo(expected("helpForSpecificArg"));
+		}
+	}
+
+	@Test
+	public void testThatEndOfOptionsOverridesHelp() throws Exception
+	{
+		CommandLineParser parser = CommandLineParser.withArguments(HELP, STRING);
+		ParsedArguments parsedArguments = parser.parse("--", "-h");
+		assertThat(parsedArguments.get(STRING)).isEqualTo("-h");
+	}
 }
diff --git a/jargo/src/test/java/se/softhouse/jargo/stringparsers/NumberArgumentTest.java b/jargo/src/test/java/se/softhouse/jargo/stringparsers/NumberArgumentTest.java
index 912d76b0..45904970 100644
--- a/jargo/src/test/java/se/softhouse/jargo/stringparsers/NumberArgumentTest.java
+++ b/jargo/src/test/java/se/softhouse/jargo/stringparsers/NumberArgumentTest.java
@@ -12,8 +12,8 @@
  */
 package se.softhouse.jargo.stringparsers;
 
+import static java.lang.System.lineSeparator;
 import static org.fest.assertions.Fail.fail;
-import static se.softhouse.common.strings.StringsUtil.NEWLINE;
 import static se.softhouse.common.testlib.Locales.TURKISH;
 import static se.softhouse.jargo.Arguments.bigDecimalArgument;
 import static se.softhouse.jargo.Arguments.bigIntegerArgument;
@@ -56,10 +56,10 @@ public class NumberArgumentTest
 	@Test
 	public void testUsage()
 	{
-		String validIntegers = ": -2,147,483,648 to 2,147,483,647" + NEWLINE;
+		String validIntegers = ": -2,147,483,648 to 2,147,483,647" + lineSeparator();
 		Usage usage = integerArgument().usage();
 		assertThat(usage).contains(validIntegers);
-		assertThat(usage).contains("Default: 0" + NEWLINE);
+		assertThat(usage).contains("Default: 0" + lineSeparator());
 	}
 
 	@Test
@@ -95,7 +95,7 @@ public void testThatDefaultValueForShortIsFormattedInTheChosenLocale()
 	{
 		Argument localeDependentArgument = shortArgument().defaultValue(Short.MAX_VALUE).build();
 		Usage usage = CommandLineParser.withArguments(localeDependentArgument).locale(TURKISH).usage();
-		assertThat(usage).contains("Default: 32.767" + NEWLINE);
+		assertThat(usage).contains("Default: 32.767" + lineSeparator());
 	}
 
 	@Test
@@ -103,7 +103,7 @@ public void testThatDefaultValueForIntegerIsFormattedInTheChosenLocale()
 	{
 		Argument localeDependentArgument = integerArgument().defaultValue(Integer.MAX_VALUE).build();
 		Usage usage = CommandLineParser.withArguments(localeDependentArgument).locale(TURKISH).usage();
-		assertThat(usage).contains("Default: 2.147.483.647" + NEWLINE);
+		assertThat(usage).contains("Default: 2.147.483.647" + lineSeparator());
 	}
 
 	@Test
@@ -111,7 +111,7 @@ public void testThatDefaultValueForLongIsFormattedInTheChosenLocale()
 	{
 		Argument localeDependentArgument = longArgument().defaultValue(Long.MAX_VALUE).build();
 		Usage usage = CommandLineParser.withArguments(localeDependentArgument).locale(TURKISH).usage();
-		assertThat(usage).contains("Default: 9.223.372.036.854.775.807" + NEWLINE);
+		assertThat(usage).contains("Default: 9.223.372.036.854.775.807" + lineSeparator());
 	}
 
 	@Test
@@ -119,7 +119,7 @@ public void testThatDefaultValueForBigDecimalIsFormattedInTheChosenLocale()
 	{
 		Argument localeDependentArgument = bigDecimalArgument().defaultValue(BigDecimal.valueOf(Long.MAX_VALUE)).build();
 		Usage usage = CommandLineParser.withArguments(localeDependentArgument).locale(TURKISH).usage();
-		assertThat(usage).contains("Default: 9.223.372.036.854.775.807" + NEWLINE);
+		assertThat(usage).contains("Default: 9.223.372.036.854.775.807" + lineSeparator());
 	}
 
 	@Test
@@ -127,6 +127,6 @@ public void testThatDefaultValueForBigIntegerIsFormattedInTheChosenLocale()
 	{
 		Argument localeDependentArgument = bigIntegerArgument().defaultValue(BigInteger.valueOf(Long.MAX_VALUE)).build();
 		Usage usage = CommandLineParser.withArguments(localeDependentArgument).locale(TURKISH).usage();
-		assertThat(usage).contains("Default: 9.223.372.036.854.775.807" + NEWLINE);
+		assertThat(usage).contains("Default: 9.223.372.036.854.775.807" + lineSeparator());
 	}
 }
diff --git a/jargo/src/test/java/se/softhouse/jargo/stringparsers/OptionalArgumentTest.java b/jargo/src/test/java/se/softhouse/jargo/stringparsers/OptionalArgumentTest.java
index 4b840894..664c5a82 100644
--- a/jargo/src/test/java/se/softhouse/jargo/stringparsers/OptionalArgumentTest.java
+++ b/jargo/src/test/java/se/softhouse/jargo/stringparsers/OptionalArgumentTest.java
@@ -15,17 +15,22 @@
 import static org.fest.assertions.Assertions.assertThat;
 import static org.fest.assertions.Fail.fail;
 import static se.softhouse.jargo.Arguments.optionArgument;
+import static se.softhouse.jargo.Arguments.stringArgument;
 import static se.softhouse.jargo.utils.Assertions2.assertThat;
 
 import java.util.Collections;
+import java.util.SortedSet;
 
 import org.junit.Test;
 
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+import se.softhouse.jargo.Argument;
 import se.softhouse.jargo.ArgumentException;
 import se.softhouse.jargo.Arguments;
+import se.softhouse.jargo.CommandLineParser;
+import se.softhouse.jargo.FakeCompleter;
 import se.softhouse.jargo.Usage;
 import se.softhouse.jargo.internal.Texts.ProgrammaticErrors;
-import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
 
 /**
  * Tests for {@link Arguments#optionArgument(String, String...)}
@@ -66,6 +71,27 @@ public void testForDefaultTrue() throws ArgumentException
 		assertThat(optionArgument("--disable-logging").defaultValue(true).parse()).isTrue();
 	}
 
+	@Test
+	public void testThatOptionalValueIsCompleted() throws Exception
+	{
+		Argument logging = optionArgument("--disable-logging").build();
+		CommandLineParser parser = CommandLineParser.withArguments(logging);
+
+		SortedSet suggestions = FakeCompleter.complete(parser, "--disable");
+		assertThat(suggestions).containsOnly("--disable-logging ");
+	}
+
+	@Test
+	public void testThatArgsAfterOptionalValueCanBeCompleted() throws Exception
+	{
+		Argument logging = optionArgument("--disable-logging").build();
+		Argument logDir = stringArgument("--log-dir").build();
+		CommandLineParser parser = CommandLineParser.withArguments(logging, logDir);
+
+		SortedSet suggestions = FakeCompleter.complete(parser, "--disable-logging", "--log");
+		assertThat(suggestions).containsOnly("--log-dir ");
+	}
+
 	@Test
 	@SuppressFBWarnings(value = "NP_NONNULL_PARAM_VIOLATION", justification = "Checks enforcement of the annotation")
 	public void testThatNullIsNotAllowed()
diff --git a/jargo/src/test/java/se/softhouse/jargo/stringparsers/custom/LimitedKeyParser.java b/jargo/src/test/java/se/softhouse/jargo/stringparsers/custom/LimitedKeyParser.java
index 22c4aa9a..22c02a40 100644
--- a/jargo/src/test/java/se/softhouse/jargo/stringparsers/custom/LimitedKeyParser.java
+++ b/jargo/src/test/java/se/softhouse/jargo/stringparsers/custom/LimitedKeyParser.java
@@ -14,15 +14,17 @@
 
 import static se.softhouse.jargo.StringParsers.stringParser;
 
+import java.util.ArrayList;
+import java.util.List;
 import java.util.Locale;
 import java.util.Set;
 
+import com.google.common.collect.ImmutableSet;
+
 import se.softhouse.jargo.ArgumentException;
 import se.softhouse.jargo.ArgumentExceptions;
 import se.softhouse.jargo.ForwardingStringParser.SimpleForwardingStringParser;
 
-import com.google.common.collect.ImmutableSet;
-
 public class LimitedKeyParser extends SimpleForwardingStringParser
 {
 	private final Set validKeys;
@@ -47,4 +49,18 @@ public String descriptionOfValidValues(Locale locale)
 	{
 		return "any of " + validKeys;
 	}
+
+	@Override
+	public Iterable complete(String partOfWord)
+	{
+		List suggestedKeys = new ArrayList<>();
+		for(String validKey : validKeys)
+		{
+			if(validKey.startsWith(partOfWord))
+			{
+				suggestedKeys.add(validKey);
+			}
+		}
+		return suggestedKeys;
+	}
 }
diff --git a/jargo/src/test/java/se/softhouse/jargo/stringparsers/custom/NullReturningParser.java b/jargo/src/test/java/se/softhouse/jargo/stringparsers/custom/NullReturningParser.java
index fceea9cc..0275a1be 100644
--- a/jargo/src/test/java/se/softhouse/jargo/stringparsers/custom/NullReturningParser.java
+++ b/jargo/src/test/java/se/softhouse/jargo/stringparsers/custom/NullReturningParser.java
@@ -14,12 +14,15 @@
 
 import java.util.Locale;
 
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+import se.softhouse.common.testlib.Explanation;
 import se.softhouse.jargo.ArgumentException;
 import se.softhouse.jargo.StringParser;
 
 public class NullReturningParser implements StringParser
 {
 
+	@SuppressFBWarnings(value = "NP_NONNULL_RETURN_VIOLATION", justification = Explanation.TESTING_INVALID_CODE)
 	@Override
 	public Object parse(String argument, Locale locale) throws ArgumentException
 	{
diff --git a/jargo/src/test/java/se/softhouse/jargo/utils/ExpectedTexts.java b/jargo/src/test/java/se/softhouse/jargo/utils/ExpectedTexts.java
index 29bfdf2b..7502b37f 100644
--- a/jargo/src/test/java/se/softhouse/jargo/utils/ExpectedTexts.java
+++ b/jargo/src/test/java/se/softhouse/jargo/utils/ExpectedTexts.java
@@ -32,6 +32,7 @@ private ExpectedTexts()
 	 */
 	public static String expected(String testName)
 	{
+
 		String expectedUsage = ResourceLoader.get("/jargo/usage_texts/" + testName + ".txt");
 
 		// Avoids having RemoteTestRunner or ForkedBooter in the .txt files. As the main class is
diff --git a/jargo/src/test/resources/jargo/security.policy b/jargo/src/test/resources/jargo/security.policy
new file mode 100644
index 00000000..8ba48068
--- /dev/null
+++ b/jargo/src/test/resources/jargo/security.policy
@@ -0,0 +1,6 @@
+grant {
+    permission java.io.FilePermission "${user.home}${/}jargo", "read";
+    permission java.io.FilePermission "${user.home}${/}jargo${/}jacoco.exec", "write";
+    permission java.lang.RuntimePermission "shutdownHooks";
+};
+
diff --git a/jargo/src/test/resources/jargo/usage_texts/allFeaturesInUsage.txt b/jargo/src/test/resources/jargo/usage_texts/allFeaturesInUsage.txt
index aeb0773f..6ceb3b1c 100644
--- a/jargo/src/test/resources/jargo/usage_texts/allFeaturesInUsage.txt
+++ b/jargo/src/test/resources/jargo/usage_texts/allFeaturesInUsage.txt
@@ -40,5 +40,7 @@ Arguments:
                                                         Default: Empty list
 --string                                        : any string
                                                         Default: 
+--transformed                                   : any string
+                                                        Default: 0
 --variableArity  ...                           : -2,147,483,648 to 2,147,483,647
                                                         Default: Empty list
diff --git a/jargo/src/test/resources/jargo/usage_texts/exceptionFromCommand.txt b/jargo/src/test/resources/jargo/usage_texts/exceptionFromCommand.txt
new file mode 100644
index 00000000..b7d2f349
--- /dev/null
+++ b/jargo/src/test/resources/jargo/usage_texts/exceptionFromCommand.txt
@@ -0,0 +1,6 @@
+Catch me
+
+Usage: exceptionFromCommand [Arguments]
+
+Arguments:
+catcher 
diff --git a/jargo/src/test/resources/jargo/usage_texts/helpForSpecificArg.txt b/jargo/src/test/resources/jargo/usage_texts/helpForSpecificArg.txt
new file mode 100644
index 00000000..06545e3f
--- /dev/null
+++ b/jargo/src/test/resources/jargo/usage_texts/helpForSpecificArg.txt
@@ -0,0 +1,7 @@
+Help requested with -h. See usage for --message for proper values.
+
+Usage: helpForSpecificArg [Arguments]
+
+Arguments:
+--message, -m     : any string
+                          Default: 
diff --git a/jargo/src/test/resources/jargo/usage_texts/metaDescriptionsForArityArgument.txt b/jargo/src/test/resources/jargo/usage_texts/metaDescriptionsForArityArgument.txt
index 7f7bccb1..bee1a300 100644
--- a/jargo/src/test/resources/jargo/usage_texts/metaDescriptionsForArityArgument.txt
+++ b/jargo/src/test/resources/jargo/usage_texts/metaDescriptionsForArityArgument.txt
@@ -7,6 +7,12 @@ Arguments:
 --foo       MetaDescShouldBeDisplayedThreeTimes
                                     : any string
                                     Default: , , 
+--trans  ...               MetaDescShouldIndicateVariableAmount
+                                    : -2,147,483,648 to 2,147,483,647
+                                    Default: Empty list
+--trans-two       MetaDescShouldBeDisplayedTwoTimes
+                                    : -2,147,483,648 to 2,147,483,647
+                                    Default: 0, 0
 --zoo  ...                 MetaDescShouldIndicateVariableAmount
                                     : -2,147,483,648 to 2,147,483,647
                                     Default: Empty list
diff --git a/pom.xml b/pom.xml
index 60bb1908..a880b362 100644
--- a/pom.xml
+++ b/pom.xml
@@ -8,12 +8,13 @@
 	se.softhouse
 	jargo-parent
 	Jargo maven parent
-	0.4.5-SNAPSHOT
+	0.4.15-SNAPSHOT
 	pom
 	https://github.com/Softhouse/jargo
 	
 		24.1-jre
 		3.0.0
+		0.8.1
 	
 	
 		https://github.com/jontejj/jargo
@@ -56,9 +57,6 @@
 			+1
 		
 	
-	
-		3.0.3
-	
 	
 		common-test
 		jargo
@@ -127,44 +125,23 @@
 				
 					
 						
-							validate
+							
 							
-							
+							format
 						
 					
 				
 			
-			
-				org.apache.maven.plugins
-				maven-surefire-plugin
-				2.9
-				
-					
-					false
-					
-					1
-					
-					1
-					true
-					
-					${coverageAgent}
-				
-			
 			
 				org.jacoco
 				jacoco-maven-plugin
-				0.7.4.201502262128
+				${jacoco.version}
 				
-					
-					coverageAgent
+					${user.home}/jargo/jacoco.exec
 				
 				
 					
@@ -249,9 +226,33 @@
 					Release of ${project.version}
 				
 			
+			
+				org.apache.maven.plugins
+				maven-enforcer-plugin
+				
+				1.4
+			
 		
 		
 			
+				
+					org.apache.maven.plugins
+					maven-surefire-plugin
+					2.21.0
+					
+						
+						false
+						all
+						true
+						
+						
+						1
+						true
+					
+				
 				
 					org.apache.maven.plugins
 					maven-site-plugin
@@ -326,7 +327,7 @@
 			org.jacoco
 			org.jacoco.agent
 			runtime
-			0.6.2.201302030002
+			${jacoco.version}
 			test
 		
 		
diff --git a/readme.md b/readme.md
index 757194fd..9a6428de 100644
--- a/readme.md
+++ b/readme.md
@@ -38,6 +38,14 @@ catch(ArgumentException exception)
 	System.exit(1);
 }
 ```
+## Tab-completions
+No program is complete without tab-completions! So after building your project's jar (.jar), you (and your users) need to specify:
+```bash
+alias my-app="java -jar .jar"
+complete -o default -o bashdefault -o nospace -C my-app "my-app"
+```
+in their ~/.bash_profile file to enjoy the automatic tab-completions Jargo provides.
+
 For more examples see the [Javadoc](http://jontejj.github.io/jargo/javadoc/jargo/)
 
 # Dependency
@@ -45,14 +53,14 @@ For more examples see the [Javadoc](http://jontejj.github.io/jargo/javadoc/jargo
      
        se.softhouse
        jargo
-       0.4.4
+       0.4.13
      
   
 #### Common-test (optional) [Javadoc](http://jontejj.github.io/jargo/javadoc/common-test/)
      
       se.softhouse
       common-test
-      0.4.4
+      0.4.13
   
   
 # JDK compatiblity
@@ -99,3 +107,8 @@ From version 0.4.2 and onwards this library requires jdk 8 and Guava was removed
 
 5. Reflection makes it hard to analyze references to classes/methods and it
     often requires a granted suppressAccessChecks from the SecurityManager, this may not be wanted. No reflection is used in jargo.
+
+# For contributors
+
+## Possible candidates for refactoring
+[![](https://codescene.io/projects/3636/status.svg) Check code-smells at *codescene.io*.](https://codescene.io/projects/3636/jobs/latest-successful/results)