/* Copyright 2017 Remko Popma 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 picocli; import org.hamcrest.CoreMatchers; import org.junit.Rule; import org.junit.Test; import org.junit.contrib.java.lang.system.Assertion; import org.junit.contrib.java.lang.system.ExpectedSystemExit; import org.junit.contrib.java.lang.system.ProvideSystemProperty; import org.junit.contrib.java.lang.system.RestoreSystemProperties; import org.junit.contrib.java.lang.system.SystemErrRule; import org.junit.contrib.java.lang.system.SystemOutRule; import org.junit.rules.TestRule; import picocli.CommandLine.Command; import picocli.CommandLine.Model.CommandSpec; import picocli.CommandLine.Model.OptionSpec; import picocli.CommandLine.Model.PositionalParamSpec; import picocli.CommandLine.Option; import picocli.CommandLine.Parameters; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStreamReader; import java.io.PrintWriter; import java.io.StringWriter; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.net.InetAddress; import java.net.URL; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.concurrent.TimeUnit; import static java.lang.String.format; import static org.junit.Assert.*; /** * Tests the scripts generated by AutoComplete. */ // http://hayne.net/MacDev/Notes/unixFAQ.html#shellStartup // https://apple.stackexchange.com/a/13019 public class AutoCompleteTest { @Rule public final ProvideSystemProperty ansiOFF = new ProvideSystemProperty("picocli.ansi", "false"); // allows tests to set any kind of properties they like, without having to individually roll them back @Rule public final TestRule restoreSystemProperties = new RestoreSystemProperties(); @Rule public final SystemErrRule systemErrRule = new SystemErrRule().enableLog().muteForSuccessfulTests(); @Rule public final SystemOutRule systemOutRule = new SystemOutRule().enableLog().muteForSuccessfulTests(); @Rule public final ExpectedSystemExit exit = ExpectedSystemExit.none(); public static class BasicExample implements Runnable { @Option(names = {"-u", "--timeUnit"}) private TimeUnit timeUnit; @Option(names = {"-t", "--timeout"}) private long timeout; public void run() { System.out.printf("BasicExample was invoked with %d %s.%n", timeout, timeUnit); } public static void main(String[] args) { new CommandLine(new BasicExample()).execute(args); } } @Test public void basic() throws Exception { String script = AutoComplete.bash("basicExample", new CommandLine(new BasicExample())); String expected = format(loadTextFromClasspath("/basic.bash"), CommandLine.VERSION, spaced(TimeUnit.values())); assertEquals(expected, script); } public static class TopLevel { @Option(names = {"-V", "--version"}, help = true) boolean versionRequested; @Option(names = {"-h", "--help"}, help = true) boolean helpRequested; @SuppressWarnings("deprecation") public static void main(String[] args) { CommandLine hierarchy = new CommandLine(new TopLevel()) .addSubcommand("sub1", new Sub1()) .addSubcommand("sub2", new CommandLine(new Sub2()) .addSubcommand("subsub1", new Sub2Child1()) .addSubcommand("subsub2", new Sub2Child2()) ); List commandLines = hierarchy.parse(args); //Collections.reverse(commandLines); for (CommandLine cmdLine : commandLines) { Object command = cmdLine.getCommand(); System.out.printf("Parsed command %s%n", AutoCompleteTest.toString(command)); } } } static class Candidates extends ArrayList { Candidates() {super(Arrays.asList("aaa", "bbb", "ccc"));} } @Command(description = "First level subcommand 1") public static class Sub1 { @Option(names = "--num", description = "a number") double number; @Option(names = "--str", description = "a String") String str; @Option(names = "--candidates", completionCandidates = Candidates.class, description = "with candidates") String[] str2; } @Command(description = "First level subcommand 2") public static class Sub2 { @Option(names = "--num2", description = "another number") int number2; @Option(names = {"--directory", "-d"}, description = "a directory") File[] directory; @Parameters(arity = "0..1") Possibilities possibilities; } @Command(description = "Second level sub-subcommand 1") public static class Sub2Child1 { @Option(names = {"-h", "--host"}, description = "a host") List host; } @Command(description = "Second level sub-subcommand 2") public static class Sub2Child2 { @Option(names = {"-u", "--timeUnit"}) private TimeUnit timeUnit; @Option(names = {"-t", "--timeout"}) private long timeout; @Parameters(completionCandidates = Candidates.class, description = "with candidates") String str2; } @Command(description = "Second level sub-subcommand 3") public static class Sub2Child3 { @Parameters(index = "1..2") File[] files; @Parameters(index = "3..*") List other; @Parameters(completionCandidates = Candidates.class, index = "0") String[] cands; } // TopLevel // @Option(names = {"-V", "--version"}, help = true) boolean versionRequested; // @Option(names = {"-h", "--help"}, help = true) boolean helpRequested; // Sub1 { // @Option(names = "--num") double number; // @Option(names = "--str") String str; // @Option(names = "--candidates", completionCandidates = Candidates.class) String[] str2;//"aaa", "bbb", "ccc" // } // Sub2 { // @Option(names = "--num2") int number2; // @Option(names = {"--directory", "-d"}) File[] directory; // @Parameters(arity = "0..1") Possibilities possibilities; // Aaa, Bbb, Ccc // ----- // Sub2Child1 { // @Option(names = {"-h", "--host"}) List host; // } // Sub2Child2 { // @Option(names = {"-u", "--timeUnit"}) private TimeUnit timeUnit; // @Option(names = {"-t", "--timeout"}) private long timeout; // @Parameters(completionCandidates = Candidates.class) String str2;//"aaa", "bbb", "ccc" // } // Sub2Child3 { // @Parameters(completionCandidates = Candidates.class, index = "0") String[] cands;//"aaa", "bbb", "ccc" // @Parameters(index = "1..2") File[] files; // @Parameters(index = "3..*") List other; // } // } @Test public void nestedSubcommands() throws Exception { CommandLine hierarchy = new CommandLine(new TopLevel()) .addSubcommand("sub1", new Sub1()) .addSubcommand("sub2", new CommandLine(new Sub2()) .addSubcommand("subsub1", new Sub2Child1()) .addSubcommand("subsub2", new Sub2Child2()) .addSubcommand("subsub3", new Sub2Child3()) ); String script = AutoComplete.bash("picocompletion-demo", hierarchy); String expected = format(loadTextFromClasspath("/picocompletion-demo_completion.bash"), CommandLine.VERSION, spaced(TimeUnit.values())); assertEquals(expected, script); } @Test public void helpCommand() { CommandLine hierarchy = new CommandLine(new AutoCompleteTest.TopLevel()) .addSubcommand("sub1", new AutoCompleteTest.Sub1()) .addSubcommand("sub2", new CommandLine(new AutoCompleteTest.Sub2()) .addSubcommand("subsub1", new AutoCompleteTest.Sub2Child1()) .addSubcommand("subsub2", new AutoCompleteTest.Sub2Child2()) .addSubcommand("subsub3", new AutoCompleteTest.Sub2Child3()) ) .addSubcommand(new CommandLine.HelpCommand()); String script = AutoComplete.bash("picocompletion-demo-help", hierarchy); String expected = format(loadTextFromClasspath("/picocompletion-demo-help_completion.bash"), CommandLine.VERSION, spaced(TimeUnit.values())); assertEquals(expected, script); } private static String spaced(Object[] values) { StringBuilder result = new StringBuilder(); for (Object value : values) { result.append(value).append(' '); } return result.toString().substring(0, result.length() - 1); } static String loadTextFromClasspath(String path) { URL url = AutoCompleteTest.class.getResource(path); if (url == null) { throw new IllegalArgumentException("Could not find '" + path + "' in classpath."); } BufferedReader reader = null; try { reader = new BufferedReader(new InputStreamReader(url.openStream())); StringBuilder result = new StringBuilder(512); char[] buff = new char[4096]; int read = 0; do { result.append(buff, 0, read); read = reader.read(buff); } while (read >= 0); return result.toString(); } catch (IOException ex) { throw new IllegalStateException("Could not read " + url + " for '" + path + "':", ex); } finally { if (reader != null) { try { reader.close(); } catch (IOException e) { /* ignore */ } } } } private static String toString(Object obj) { StringBuilder sb = new StringBuilder(256); Class cls = obj.getClass(); sb.append(cls.getSimpleName()).append("["); String sep = ""; for (Field f : cls.getDeclaredFields()) { f.setAccessible(true); sb.append(sep).append(f.getName()).append("="); try { sb.append(f.get(obj)); } catch (Exception ex) { sb.append(ex); } sep = ", "; } return sb.append("]").toString(); } private static final String AUTO_COMPLETE_APP_USAGE = String.format("" + "Usage: picocli.AutoComplete [-fhVw] [-c=] [-n=]%n" + " [-o=] [@...]%n" + " %n" + "Generates a bash completion script for the specified command class.%n" + " [@...] One or more argument files containing options.%n" + " Fully qualified class name of the annotated%n" + " `@Command` class to generate a completion script%n" + " for.%n" + " -c, --factory=%n" + " Optionally specify the fully qualified class name%n" + " of the custom factory to use to instantiate the%n" + " command class. When omitted, the default picocli%n" + " factory is used.%n" + " -n, --name= Optionally specify the name of the command to%n" + " create a completion script for. When omitted,%n" + " the annotated class `@Command(name = \"...\")`%n" + " attribute is used. If no `@Command(name = ...)`%n" + " attribute exists, '' (in%n" + " lower-case) is used.%n" + " -o, --completionScript=%n" + " Optionally specify the path of the completion%n" + " script file to generate. When omitted, a file%n" + " named '_completion' is generated in%n" + " the current directory.%n" + " -w, --writeCommandScript Write a '' sample command script to%n" + " the same directory as the completion script.%n" + " -f, --force Overwrite existing script files.%n" + " -h, --help Show this help message and exit.%n" + " -V, --version Print version information and exit.%n" + "%n" + "Exit Codes:%n" + " 0 Successful program execution%n" + " 1 Usage error: user input for the command was incorrect, e.g., the wrong%n" + " number of arguments, a bad flag, a bad syntax in a parameter, etc.%n" + " 2 The specified command script exists (Specify `--force` to overwrite).%n" + " 3 The specified completion script exists (Specify `--force` to overwrite).%n" + " 4 An exception occurred while generating the completion script.%n" + "%n" + "System Properties:%n" + "Set the following system properties to control the exit code of this program:%n" + "%n" + "* `\"picocli.autocomplete.systemExitOnSuccess\"`%n" + " call `System.exit(0)` when execution completes normally.%n" + "* `\"picocli.autocomplete.systemExitOnError\"`%n" + " call `System.exit(ERROR_CODE)` when an error occurs.%n" + "%n" + "If these system properties are not defined or have value \"false\", this program%n" + "completes without terminating the JVM.%n" + "%n" + "Example%n" + "-------%n" + " java -cp \"myapp.jar;picocli-4.6.1.jar\" \\%n" + " picocli.AutoComplete my.pkg.MyClass%n"); @Test public void testAutoCompleteAppHelp() { String[][] argsList = new String[][] { {"-h"}, {"--help"}, }; for (final String[] args : argsList) { exit.expectSystemExitWithStatus(AutoComplete.EXIT_CODE_SUCCESS); exit.checkAssertionAfterwards(new Assertion() { public void checkAssertion() { assertEquals(args[0], AUTO_COMPLETE_APP_USAGE, systemOutRule.getLog()); systemOutRule.clearLog(); } }); System.setProperty("picocli.autocomplete.systemExitOnSuccess", "YES"); AutoComplete.main(args); } } @Test public void testAutoCompleteAppHelp_NoSystemExit() { String[][] argsList = new String[][] { {"-h"}, {"--help"}, }; System.setProperty("picocli.autocomplete.systemExitOnSuccess", "false"); for (final String[] args : argsList) { AutoComplete.main(args); assertEquals(args[0], AUTO_COMPLETE_APP_USAGE, systemOutRule.getLog()); systemOutRule.clearLog(); } } @Test public void testAutoCompleteRequiresCommandLineFQCN() { exit.expectSystemExitWithStatus(AutoComplete.EXIT_CODE_INVALID_INPUT); exit.checkAssertionAfterwards(new Assertion() { public void checkAssertion() { String expected = String.format("Missing required parameter: ''%n") + AUTO_COMPLETE_APP_USAGE; assertEquals(expected, systemErrRule.getLog()); } }); System.setProperty("picocli.autocomplete.systemExitOnError", "true"); AutoComplete.main(); } @Test public void testAutoCompleteRequiresCommandLineFQCN_NoSystemExit() { AutoComplete.main(); String expected = String.format("Missing required parameter: ''%n") + AUTO_COMPLETE_APP_USAGE; assertEquals(expected, systemErrRule.getLog()); } @Test public void testAutoCompleteAppCannotInstantiate() { @Command(name = "test") class TestApp { public TestApp(String noDefaultConstructor) { throw new RuntimeException();} } exit.expectSystemExitWithStatus(AutoComplete.EXIT_CODE_EXECUTION_ERROR); exit.checkAssertionAfterwards(new Assertion() { public void checkAssertion() { String actual = systemErrRule.getLog(); assertTrue(actual.startsWith("java.lang.NoSuchMethodException: picocli.AutoCompleteTest$1TestApp.()")); assertTrue(actual.contains(AUTO_COMPLETE_APP_USAGE)); } }); System.setProperty("picocli.autocomplete.systemExitOnSuccess", "false"); System.setProperty("picocli.autocomplete.systemExitOnError", "YES"); AutoComplete.main(TestApp.class.getName()); } @Test public void testAutoCompleteAppCannotInstantiate_NoSystemExit() { @Command(name = "test") class TestApp { public TestApp(String noDefaultConstructor) { throw new RuntimeException();} } AutoComplete.main(TestApp.class.getName()); String actual = systemErrRule.getLog(); assertTrue(actual.startsWith("java.lang.NoSuchMethodException: picocli.AutoCompleteTest$2TestApp.()")); assertTrue(actual.contains(AUTO_COMPLETE_APP_USAGE)); } @Test public void testAutoCompleteAppCompletionScriptFileWillNotOverwrite() throws Exception { File dir = new File(System.getProperty("java.io.tmpdir")); final File completionScript = new File(dir, "App_completion"); if (completionScript.exists()) {assertTrue(completionScript.delete());} completionScript.deleteOnExit(); // create the file FileOutputStream fous = new FileOutputStream(completionScript, false); fous.close(); exit.expectSystemExitWithStatus(AutoComplete.EXIT_CODE_COMPLETION_SCRIPT_EXISTS); exit.checkAssertionAfterwards(new Assertion() { public void checkAssertion() { String expected = String.format("" + "ERROR: picocli.AutoComplete: %s exists. Specify --force to overwrite.%n" + "%s", completionScript.getAbsolutePath(), AUTO_COMPLETE_APP_USAGE); assertTrue(systemErrRule.getLog().startsWith(expected)); } }); System.setProperty("picocli.autocomplete.systemExitOnError", ""); AutoComplete.main(String.format("-o=%s", completionScript.getAbsolutePath()), "picocli.AutoComplete$App"); } @Test public void testAutoCompleteAppCompletionScriptFileWillNotOverwrite_NoSystemExit() throws Exception { File dir = new File(System.getProperty("java.io.tmpdir")); final File completionScript = new File(dir, "App_completion"); if (completionScript.exists()) {assertTrue(completionScript.delete());} completionScript.deleteOnExit(); // create the file FileOutputStream fous = new FileOutputStream(completionScript, false); fous.close(); AutoComplete.main(String.format("-o=%s", completionScript.getAbsolutePath()), "picocli.AutoComplete$App"); String expected = String.format("" + "ERROR: picocli.AutoComplete: %s exists. Specify --force to overwrite.%n" + "%s", completionScript.getAbsolutePath(), AUTO_COMPLETE_APP_USAGE); assertTrue(systemErrRule.getLog().startsWith(expected)); } @Test public void testAutoCompleteAppCommandScriptFileWillNotOverwrite() throws Exception { File dir = new File(System.getProperty("java.io.tmpdir")); final File commandScript = new File(dir, "picocli.AutoComplete"); if (commandScript.exists()) {assertTrue(commandScript.delete());} commandScript.deleteOnExit(); // create the file FileOutputStream fous = new FileOutputStream(commandScript, false); fous.close(); File completionScript = new File(dir, commandScript.getName() + "_completion"); exit.expectSystemExitWithStatus(AutoComplete.EXIT_CODE_COMMAND_SCRIPT_EXISTS); exit.checkAssertionAfterwards(new Assertion() { public void checkAssertion() { String expected = String.format("" + "ERROR: picocli.AutoComplete: %s exists. Specify --force to overwrite.%n" + "%s", commandScript.getAbsolutePath(), AUTO_COMPLETE_APP_USAGE); assertTrue(systemErrRule.getLog().startsWith(expected)); } }); System.setProperty("picocli.autocomplete.systemExitOnError", "true"); AutoComplete.main("--writeCommandScript", String.format("-o=%s", completionScript.getAbsolutePath()), "picocli.AutoComplete$App"); } @Test public void testAutoCompleteAppCommandScriptFileWillNotOverwrite_NoSystemExit() throws Exception { File dir = new File(System.getProperty("java.io.tmpdir")); final File commandScript = new File(dir, "picocli.AutoComplete"); if (commandScript.exists()) {assertTrue(commandScript.delete());} commandScript.deleteOnExit(); // create the file FileOutputStream fous = new FileOutputStream(commandScript, false); fous.close(); File completionScript = new File(dir, commandScript.getName() + "_completion"); AutoComplete.main("--writeCommandScript", String.format("-o=%s", completionScript.getAbsolutePath()), "picocli.AutoComplete$App"); String expected = String.format("" + "ERROR: picocli.AutoComplete: %s exists. Specify --force to overwrite.%n" + "%s", commandScript.getAbsolutePath(), AUTO_COMPLETE_APP_USAGE); assertTrue(systemErrRule.getLog().startsWith(expected)); } @Test public void testAutoCompleteAppCommandScriptFileWillOverwriteIfRequested() throws Exception { File dir = new File(System.getProperty("java.io.tmpdir")); final File commandScript = new File(dir, "picocli.AutoComplete"); if (commandScript.exists()) {assertTrue(commandScript.delete());} commandScript.deleteOnExit(); // create the file FileOutputStream fous = new FileOutputStream(commandScript, false); fous.close(); assertEquals(0, commandScript.length()); File completionScript = new File(dir, commandScript.getName() + "_completion"); exit.expectSystemExitWithStatus(AutoComplete.EXIT_CODE_SUCCESS); exit.checkAssertionAfterwards(new Assertion() { public void checkAssertion() { assertEquals("", systemErrRule.getLog()); assertNotEquals(0, commandScript.length()); assertTrue(commandScript.delete()); } }); System.setProperty("picocli.autocomplete.systemExitOnSuccess", "true"); AutoComplete.main("--writeCommandScript", "--force", String.format("-o=%s", completionScript.getAbsolutePath()), "picocli.AutoComplete$App"); } @Test public void testAutoCompleteAppBothScriptFilesForceOverwrite() throws Exception { File dir = new File(System.getProperty("java.io.tmpdir")); final File commandScript = new File(dir, "picocli.AutoComplete"); if (commandScript.exists()) {assertTrue(commandScript.delete());} commandScript.deleteOnExit(); // create the file FileOutputStream fous1 = new FileOutputStream(commandScript, false); fous1.close(); final File completionScript = new File(dir, commandScript.getName() + "_completion"); if (completionScript.exists()) {assertTrue(completionScript.delete());} completionScript.deleteOnExit(); // create the file FileOutputStream fous2 = new FileOutputStream(completionScript, false); fous2.close(); exit.expectSystemExitWithStatus(AutoComplete.EXIT_CODE_SUCCESS); exit.checkAssertionAfterwards(new Assertion() { public void checkAssertion() throws Exception { byte[] command = readBytes(commandScript); assertEquals(("" + "#!/usr/bin/env bash\n" + "\n" + "LIBS=path/to/libs\n" + "CP=\"${LIBS}/myApp.jar\"\n" + "java -cp \"${CP}\" 'picocli.AutoComplete$App' $@"), new String(command, "UTF8")); byte[] completion = readBytes(completionScript); String expected = expectedCompletionScriptForAutoCompleteApp(); assertEquals(expected, new String(completion, "UTF8")); } }); System.setProperty("picocli.autocomplete.systemExitOnSuccess", "true"); AutoComplete.main("--force", "--writeCommandScript", String.format("-o=%s", completionScript.getAbsolutePath()), "picocli.AutoComplete$App"); } @Test public void testAutoCompleteAppBothScriptFilesForceOverwrite_NoSystemExit() throws Exception { File dir = new File(System.getProperty("java.io.tmpdir")); final File commandScript = new File(dir, "picocli.AutoComplete"); if (commandScript.exists()) {assertTrue(commandScript.delete());} commandScript.deleteOnExit(); // create the file FileOutputStream fous1 = new FileOutputStream(commandScript, false); fous1.close(); final File completionScript = new File(dir, commandScript.getName() + "_completion"); if (completionScript.exists()) {assertTrue(completionScript.delete());} completionScript.deleteOnExit(); // create the file FileOutputStream fous2 = new FileOutputStream(completionScript, false); fous2.close(); AutoComplete.main("--force", "--writeCommandScript", String.format("-o=%s", completionScript.getAbsolutePath()), "picocli.AutoComplete$App"); byte[] command = readBytes(commandScript); assertEquals(("" + "#!/usr/bin/env bash\n" + "\n" + "LIBS=path/to/libs\n" + "CP=\"${LIBS}/myApp.jar\"\n" + "java -cp \"${CP}\" 'picocli.AutoComplete$App' $@"), new String(command, "UTF8")); byte[] completion = readBytes(completionScript); String expected = expectedCompletionScriptForAutoCompleteApp(); assertEquals(expected, new String(completion, "UTF8")); } @Test public void testAutoCompleteAppGeneratesScriptNameBasedOnCommandName() throws Exception { final String commandName = "bestCommandEver"; final File completionScript = new File(commandName + "_completion"); if (completionScript.exists()) {assertTrue(completionScript.delete());} completionScript.deleteOnExit(); exit.expectSystemExitWithStatus(AutoComplete.EXIT_CODE_SUCCESS); exit.checkAssertionAfterwards(new Assertion() { public void checkAssertion() throws Exception { byte[] completion = readBytes(completionScript); assertTrue(completionScript.delete()); String expected = expectedCompletionScriptForAutoCompleteApp().replaceAll("picocli\\.AutoComplete", commandName); assertEquals(expected, new String(completion, "UTF8")); } }); System.setProperty("picocli.autocomplete.systemExitOnSuccess", "YES"); AutoComplete.main(String.format("--name=%s", commandName), "picocli.AutoComplete$App"); } @Test public void testAutoCompleteAppGeneratesScriptNameBasedOnCommandName_NoSystemExit() throws Exception { final String commandName = "bestCommandEver"; final File completionScript = new File(commandName + "_completion"); if (completionScript.exists()) {assertTrue(completionScript.delete());} completionScript.deleteOnExit(); AutoComplete.main(String.format("--name=%s", commandName), "picocli.AutoComplete$App"); byte[] completion = readBytes(completionScript); assertTrue(completionScript.delete()); String expected = expectedCompletionScriptForAutoCompleteApp().replaceAll("picocli\\.AutoComplete", commandName); assertEquals(expected, new String(completion, "UTF8")); } public static class NonDefaultCommand { @Option(names = {"-t", "--timeout"}) private long timeout; public NonDefaultCommand(int i) {} } public static class MyFactory implements CommandLine.IFactory { @SuppressWarnings("unchecked") public K create(Class cls) { return (K) new NonDefaultCommand(123); } } private String expectedCompletionScriptForAutoCompleteApp() { return String.format("" + "#!/usr/bin/env bash\n" + "#\n" + "# picocli.AutoComplete Bash Completion\n" + "# =======================\n" + "#\n" + "# Bash completion support for the `picocli.AutoComplete` command,\n" + "# generated by [picocli](http://picocli.info/) version %s.\n" + "#\n" + "# Installation\n" + "# ------------\n" + "#\n" + "# 1. Source all completion scripts in your .bash_profile\n" + "#\n" + "# cd $YOUR_APP_HOME/bin\n" + "# for f in $(find . -name \"*_completion\"); do line=\". $(pwd)/$f\"; grep \"$line\" ~/.bash_profile || echo \"$line\" >> ~/.bash_profile; done\n" + "#\n" + "# 2. Open a new bash console, and type `picocli.AutoComplete [TAB][TAB]`\n" + "#\n" + "# 1a. Alternatively, if you have [bash-completion](https://github.com/scop/bash-completion) installed:\n" + "# Place this file in a `bash-completion.d` folder:\n" + "#\n" + "# * /etc/bash-completion.d\n" + "# * /usr/local/etc/bash-completion.d\n" + "# * ~/bash-completion.d\n" + "#\n" + "# Documentation\n" + "# -------------\n" + "# The script is called by bash whenever [TAB] or [TAB][TAB] is pressed after\n" + "# 'picocli.AutoComplete (..)'. By reading entered command line parameters,\n" + "# it determines possible bash completions and writes them to the COMPREPLY variable.\n" + "# Bash then completes the user input if only one entry is listed in the variable or\n" + "# shows the options if more than one is listed in COMPREPLY.\n" + "#\n" + "# References\n" + "# ----------\n" + "# [1] http://stackoverflow.com/a/12495480/1440785\n" + "# [2] http://tiswww.case.edu/php/chet/bash/FAQ\n" + "# [3] https://www.gnu.org/software/bash/manual/html_node/The-Shopt-Builtin.html\n" + "# [4] http://zsh.sourceforge.net/Doc/Release/Options.html#index-COMPLETE_005fALIASES\n" + "# [5] https://stackoverflow.com/questions/17042057/bash-check-element-in-array-for-elements-in-another-array/17042655#17042655\n" + "# [6] https://www.gnu.org/software/bash/manual/html_node/Programmable-Completion.html#Programmable-Completion\n" + "# [7] https://stackoverflow.com/questions/3249432/can-a-bash-tab-completion-script-be-used-in-zsh/27853970#27853970\n" + "#\n" + "\n" + "if [ -n \"$BASH_VERSION\" ]; then\n" + " # Enable programmable completion facilities when using bash (see [3])\n" + " shopt -s progcomp\n" + "elif [ -n \"$ZSH_VERSION\" ]; then\n" + " # Make alias a distinct command for completion purposes when using zsh (see [4])\n" + " setopt COMPLETE_ALIASES\n" + " alias compopt=complete\n" + "\n" + " # Enable bash completion in zsh (see [7])\n" + " autoload -U +X compinit && compinit\n" + " autoload -U +X bashcompinit && bashcompinit\n" + "fi\n" + "\n" + "# CompWordsContainsArray takes an array and then checks\n" + "# if all elements of this array are in the global COMP_WORDS array.\n" + "#\n" + "# Returns zero (no error) if all elements of the array are in the COMP_WORDS array,\n" + "# otherwise returns 1 (error).\n" + "function CompWordsContainsArray() {\n" + " declare -a localArray\n" + " localArray=(\"$@\")\n" + " local findme\n" + " for findme in \"${localArray[@]}\"; do\n" + " if ElementNotInCompWords \"$findme\"; then return 1; fi\n" + " done\n" + " return 0\n" + "}\n" + "function ElementNotInCompWords() {\n" + " local findme=\"$1\"\n" + " local element\n" + " for element in \"${COMP_WORDS[@]}\"; do\n" + " if [[ \"$findme\" = \"$element\" ]]; then return 1; fi\n" + " done\n" + " return 0\n" + "}\n" + "\n" + "# The `currentPositionalIndex` function calculates the index of the current positional parameter.\n" + "#\n" + "# currentPositionalIndex takes three parameters:\n" + "# the command name,\n" + "# a space-separated string with the names of options that take a parameter, and\n" + "# a space-separated string with the names of boolean options (that don't take any params).\n" + "# When done, this function echos the current positional index to std_out.\n" + "#\n" + "# Example usage:\n" + "# local currIndex=$(currentPositionalIndex \"mysubcommand\" \"$ARG_OPTS\" \"$FLAG_OPTS\")\n" + "function currentPositionalIndex() {\n" + " local commandName=\"$1\"\n" + " local optionsWithArgs=\"$2\"\n" + " local booleanOptions=\"$3\"\n" + " local previousWord\n" + " local result=0\n" + "\n" + " for i in $(seq $((COMP_CWORD - 1)) -1 0); do\n" + " previousWord=${COMP_WORDS[i]}\n" + " if [ \"${previousWord}\" = \"$commandName\" ]; then\n" + " break\n" + " fi\n" + " if [[ \"${optionsWithArgs}\" =~ ${previousWord} ]]; then\n" + " ((result-=2)) # Arg option and its value not counted as positional param\n" + " elif [[ \"${booleanOptions}\" =~ ${previousWord} ]]; then\n" + " ((result-=1)) # Flag option itself not counted as positional param\n" + " fi\n" + " ((result++))\n" + " done\n" + " echo \"$result\"\n" + "}\n" + "\n" + "# Bash completion entry point function.\n" + "# _complete_picocli.AutoComplete finds which commands and subcommands have been specified\n" + "# on the command line and delegates to the appropriate function\n" + "# to generate possible options and subcommands for the last specified subcommand.\n" + "function _complete_picocli.AutoComplete() {\n" + "\n" + "\n" + " # No subcommands were specified; generate completions for the top-level command.\n" + " _picocli_picocli.AutoComplete; return $?;\n" + "}\n" + "\n" + "# Generates completions for the options and subcommands of the `picocli.AutoComplete` command.\n" + "function _picocli_picocli.AutoComplete() {\n" + " # Get completion data\n" + " local curr_word=${COMP_WORDS[COMP_CWORD]}\n" + " local prev_word=${COMP_WORDS[COMP_CWORD-1]}\n" + "\n" + " local commands=\"\"\n" + " local flag_opts=\"-w --writeCommandScript -f --force -h --help -V --version\"\n" + " local arg_opts=\"-c --factory -n --name -o --completionScript\"\n" + "\n" + " compopt +o default\n" + "\n" + " case ${prev_word} in\n" + " -c|--factory)\n" + " return\n" + " ;;\n" + " -n|--name)\n" + " return\n" + " ;;\n" + " -o|--completionScript)\n" + " compopt -o filenames\n" + " COMPREPLY=( $( compgen -f -- \"${curr_word}\" ) ) # files\n" + " return $?\n" + " ;;\n" + " esac\n" + "\n" + " if [[ \"${curr_word}\" == -* ]]; then\n" + " COMPREPLY=( $(compgen -W \"${flag_opts} ${arg_opts}\" -- \"${curr_word}\") )\n" + " else\n" + " local positionals=\"\"\n" + " COMPREPLY=( $(compgen -W \"${commands} ${positionals}\" -- \"${curr_word}\") )\n" + " fi\n" + "}\n" + "\n" + "# Define a completion specification (a compspec) for the\n" + "# `picocli.AutoComplete`, `picocli.AutoComplete.sh`, and `picocli.AutoComplete.bash` commands.\n" + "# Uses the bash `complete` builtin (see [6]) to specify that shell function\n" + "# `_complete_picocli.AutoComplete` is responsible for generating possible completions for the\n" + "# current word on the command line.\n" + "# The `-o default` option means that if the function generated no matches, the\n" + "# default Bash completions and the Readline default filename completions are performed.\n" + "complete -F _complete_picocli.AutoComplete -o default picocli.AutoComplete picocli.AutoComplete.sh picocli.AutoComplete.bash\n", CommandLine.VERSION); } public static byte[] readBytes(File f) throws IOException { int pos = 0; int len = 0; byte[] buffer = new byte[(int) f.length()]; FileInputStream fis = null; try { fis = new FileInputStream(f); while ((len = fis.read(buffer, pos, buffer.length - pos)) > 0) { pos += len; } return buffer; } finally { fis.close(); } } @Test public void testAutoCompleteAppUsesCustomFactory() throws Exception { final String commandName = "nondefault"; final File completionScript = new File(commandName + "_completion"); if (completionScript.exists()) {assertTrue(completionScript.delete());} completionScript.deleteOnExit(); exit.expectSystemExitWithStatus(AutoComplete.EXIT_CODE_SUCCESS); exit.checkAssertionAfterwards(new Assertion() { public void checkAssertion() throws Exception { byte[] completion = readBytes(completionScript); assertTrue(completionScript.delete()); String expected = expectedCompletionScriptForNonDefault().replaceAll("picocli\\.AutoComplete", commandName); assertEquals(expected, new String(completion, "UTF8")); } }); System.setProperty("picocli.autocomplete.systemExitOnSuccess", "true"); AutoComplete.main(String.format("--factory=%s", MyFactory.class.getName()), String.format("--name=%s", commandName), NonDefaultCommand.class.getName()); } private String expectedCompletionScriptForNonDefault() { return String.format("" + "#!/usr/bin/env bash\n" + "#\n" + "# nondefault Bash Completion\n" + "# =======================\n" + "#\n" + "# Bash completion support for the `nondefault` command,\n" + "# generated by [picocli](http://picocli.info/) version %s.\n" + "#\n" + "# Installation\n" + "# ------------\n" + "#\n" + "# 1. Source all completion scripts in your .bash_profile\n" + "#\n" + "# cd $YOUR_APP_HOME/bin\n" + "# for f in $(find . -name \"*_completion\"); do line=\". $(pwd)/$f\"; grep \"$line\" ~/.bash_profile || echo \"$line\" >> ~/.bash_profile; done\n" + "#\n" + "# 2. Open a new bash console, and type `nondefault [TAB][TAB]`\n" + "#\n" + "# 1a. Alternatively, if you have [bash-completion](https://github.com/scop/bash-completion) installed:\n" + "# Place this file in a `bash-completion.d` folder:\n" + "#\n" + "# * /etc/bash-completion.d\n" + "# * /usr/local/etc/bash-completion.d\n" + "# * ~/bash-completion.d\n" + "#\n" + "# Documentation\n" + "# -------------\n" + "# The script is called by bash whenever [TAB] or [TAB][TAB] is pressed after\n" + "# 'nondefault (..)'. By reading entered command line parameters,\n" + "# it determines possible bash completions and writes them to the COMPREPLY variable.\n" + "# Bash then completes the user input if only one entry is listed in the variable or\n" + "# shows the options if more than one is listed in COMPREPLY.\n" + "#\n" + "# References\n" + "# ----------\n" + "# [1] http://stackoverflow.com/a/12495480/1440785\n" + "# [2] http://tiswww.case.edu/php/chet/bash/FAQ\n" + "# [3] https://www.gnu.org/software/bash/manual/html_node/The-Shopt-Builtin.html\n" + "# [4] http://zsh.sourceforge.net/Doc/Release/Options.html#index-COMPLETE_005fALIASES\n" + "# [5] https://stackoverflow.com/questions/17042057/bash-check-element-in-array-for-elements-in-another-array/17042655#17042655\n" + "# [6] https://www.gnu.org/software/bash/manual/html_node/Programmable-Completion.html#Programmable-Completion\n" + "# [7] https://stackoverflow.com/questions/3249432/can-a-bash-tab-completion-script-be-used-in-zsh/27853970#27853970\n" + "#\n" + "\n" + "if [ -n \"$BASH_VERSION\" ]; then\n" + " # Enable programmable completion facilities when using bash (see [3])\n" + " shopt -s progcomp\n" + "elif [ -n \"$ZSH_VERSION\" ]; then\n" + " # Make alias a distinct command for completion purposes when using zsh (see [4])\n" + " setopt COMPLETE_ALIASES\n" + " alias compopt=complete\n" + "\n" + " # Enable bash completion in zsh (see [7])\n" + " autoload -U +X compinit && compinit\n" + " autoload -U +X bashcompinit && bashcompinit\n" + "fi\n" + "\n" + "# CompWordsContainsArray takes an array and then checks\n" + "# if all elements of this array are in the global COMP_WORDS array.\n" + "#\n" + "# Returns zero (no error) if all elements of the array are in the COMP_WORDS array,\n" + "# otherwise returns 1 (error).\n" + "function CompWordsContainsArray() {\n" + " declare -a localArray\n" + " localArray=(\"$@\")\n" + " local findme\n" + " for findme in \"${localArray[@]}\"; do\n" + " if ElementNotInCompWords \"$findme\"; then return 1; fi\n" + " done\n" + " return 0\n" + "}\n" + "function ElementNotInCompWords() {\n" + " local findme=\"$1\"\n" + " local element\n" + " for element in \"${COMP_WORDS[@]}\"; do\n" + " if [[ \"$findme\" = \"$element\" ]]; then return 1; fi\n" + " done\n" + " return 0\n" + "}\n" + "\n" + "# The `currentPositionalIndex` function calculates the index of the current positional parameter.\n" + "#\n" + "# currentPositionalIndex takes three parameters:\n" + "# the command name,\n" + "# a space-separated string with the names of options that take a parameter, and\n" + "# a space-separated string with the names of boolean options (that don't take any params).\n" + "# When done, this function echos the current positional index to std_out.\n" + "#\n" + "# Example usage:\n" + "# local currIndex=$(currentPositionalIndex \"mysubcommand\" \"$ARG_OPTS\" \"$FLAG_OPTS\")\n" + "function currentPositionalIndex() {\n" + " local commandName=\"$1\"\n" + " local optionsWithArgs=\"$2\"\n" + " local booleanOptions=\"$3\"\n" + " local previousWord\n" + " local result=0\n" + "\n" + " for i in $(seq $((COMP_CWORD - 1)) -1 0); do\n" + " previousWord=${COMP_WORDS[i]}\n" + " if [ \"${previousWord}\" = \"$commandName\" ]; then\n" + " break\n" + " fi\n" + " if [[ \"${optionsWithArgs}\" =~ ${previousWord} ]]; then\n" + " ((result-=2)) # Arg option and its value not counted as positional param\n" + " elif [[ \"${booleanOptions}\" =~ ${previousWord} ]]; then\n" + " ((result-=1)) # Flag option itself not counted as positional param\n" + " fi\n" + " ((result++))\n" + " done\n" + " echo \"$result\"\n" + "}\n" + "\n" + "# Bash completion entry point function.\n" + "# _complete_nondefault finds which commands and subcommands have been specified\n" + "# on the command line and delegates to the appropriate function\n" + "# to generate possible options and subcommands for the last specified subcommand.\n" + "function _complete_nondefault() {\n" + "\n" + "\n" + " # No subcommands were specified; generate completions for the top-level command.\n" + " _picocli_nondefault; return $?;\n" + "}\n" + "\n" + "# Generates completions for the options and subcommands of the `nondefault` command.\n" + "function _picocli_nondefault() {\n" + " # Get completion data\n" + " local curr_word=${COMP_WORDS[COMP_CWORD]}\n" + " local prev_word=${COMP_WORDS[COMP_CWORD-1]}\n" + "\n" + " local commands=\"\"\n" + " local flag_opts=\"\"\n" + " local arg_opts=\"-t --timeout\"\n" + "\n" + " compopt +o default\n" + "\n" + " case ${prev_word} in\n" + " -t|--timeout)\n" + " return\n" + " ;;\n" + " esac\n" + "\n" + " if [[ \"${curr_word}\" == -* ]]; then\n" + " COMPREPLY=( $(compgen -W \"${flag_opts} ${arg_opts}\" -- \"${curr_word}\") )\n" + " else\n" + " local positionals=\"\"\n" + " COMPREPLY=( $(compgen -W \"${commands} ${positionals}\" -- \"${curr_word}\") )\n" + " fi\n" + "}\n" + "\n" + "# Define a completion specification (a compspec) for the\n" + "# `nondefault`, `nondefault.sh`, and `nondefault.bash` commands.\n" + "# Uses the bash `complete` builtin (see [6]) to specify that shell function\n" + "# `_complete_nondefault` is responsible for generating possible completions for the\n" + "# current word on the command line.\n" + "# The `-o default` option means that if the function generated no matches, the\n" + "# default Bash completions and the Readline default filename completions are performed.\n" + "complete -F _complete_nondefault -o default nondefault nondefault.sh nondefault.bash\n", CommandLine.VERSION); } @Test public void testBashRejectsNullScript() { try { AutoComplete.bash(null, new CommandLine(new TopLevel())); fail("Expected NPE"); } catch (NullPointerException ok) { assertEquals("scriptName", ok.getMessage()); } } @Test public void testBashRejectsNullCommandLine() { try { AutoComplete.bash("script", null); fail("Expected NPE"); } catch (NullPointerException ok) { assertEquals("commandLine", ok.getMessage()); } } @Test public void testBashAcceptsNullCommand() throws Exception { File temp = File.createTempFile("abc", "b"); temp.deleteOnExit(); AutoComplete.bash("script", temp, null, new CommandLine(new TopLevel())); assertTrue(temp.length() > 0); } @Test public void testBashRejectsNullOut() throws Exception { File commandFile = File.createTempFile("abc", "b"); commandFile.deleteOnExit(); try { AutoComplete.bash("script", null, commandFile, new CommandLine(new TopLevel())); fail("Expected NPE"); } catch (NullPointerException ok) { assertEquals(null, ok.getMessage()); } } @Command private static class PrivateCommandClass { } //Support generating autocompletion scripts for non-public @Command classes #306 @Test public void test306_SupportGeneratingAutocompletionScriptForNonPublicCommandClasses() { File dir = new File(System.getProperty("java.io.tmpdir")); final File completionScript = new File(dir, "App_completion"); if (completionScript.exists()) {assertTrue(completionScript.delete());} completionScript.deleteOnExit(); exit.expectSystemExitWithStatus(AutoComplete.EXIT_CODE_SUCCESS); exit.checkAssertionAfterwards(new Assertion() { public void checkAssertion() { assertEquals("", systemErrRule.getLog()); assertEquals("", systemOutRule.getLog()); completionScript.delete(); } }); System.setProperty("picocli.autocomplete.systemExitOnSuccess", ""); AutoComplete.main(String.format("-o=%s", completionScript.getAbsolutePath()), PrivateCommandClass.class.getName()); } @Test public void testComplete() { CommandLine hierarchy = new CommandLine(new TopLevel()) .addSubcommand("sub1", new Sub1()) .addSubcommand("sub2", new CommandLine(new Sub2()) .addSubcommand("subsub1", new Sub2Child1()) .addSubcommand("subsub2", new Sub2Child2()) ); CommandSpec spec = hierarchy.getCommandSpec(); spec.parser().collectErrors(true); int cur = 500; test(spec, a(), 0, 0, cur, l("--help", "--version", "-V", "-h", "sub1", "sub2")); test(spec, a("-"), 0, 0, cur, l("--help", "--version", "-V", "-h", "sub1", "sub2")); test(spec, a("-"), 0, 1, cur, l("-help", "-version", "V", "h")); test(spec, a("-h"), 0, 1, cur, l("-help", "-version", "V", "h")); test(spec, a("-h"), 0, 2, cur, l("")); test(spec, a("s"), 0, 1, cur, l("ub1", "ub2")); test(spec, a("sub1"), 0, 0, cur, l("--help", "--version", "-V", "-h", "sub1", "sub2")); test(spec, a("sub1"), 1, 0, cur, l("--candidates", "--num", "--str")); test(spec, a("sub1", "-"), 1, 0, cur, l("--candidates", "--num", "--str")); test(spec, a("sub1", "-"), 1, 1, cur, l("-candidates", "-num", "-str")); test(spec, a("sub1", "--"), 1, 1, cur, l("-candidates", "-num", "-str")); test(spec, a("sub1", "--"), 1, 2, cur, l("candidates", "num", "str")); test(spec, a("sub1", "--c"), 1, 2, cur, l("candidates", "num", "str")); test(spec, a("sub1", "--c"), 1, 3, cur, l("andidates")); test(spec, a("sub1", "--candidates"), 2, 0, cur, l("aaa", "bbb", "ccc")); test(spec, a("sub1", "--candidates"), 1, 12, cur, l("")); test(spec, a("sub1", "--candidates="), 1, 11, cur, l("s")); // cursor before 's' test(spec, a("sub1", "--candidates="), 1, 12, cur, l("=aaa", "=bbb", "=ccc")); test(spec, a("sub1", "--candidates="), 1, 13, cur, l("aaa", "bbb", "ccc")); test(spec, a("sub1", "--candidates=a"), 1, 13, cur, l("aaa", "bbb", "ccc")); test(spec, a("sub1", "--candidates=a"), 1, 14, cur, l("aa")); test(spec, a("sub1", "--candidates", "a"), 2, 1, cur, l("aa")); test(spec, a("sub1", "--candidates", "a"), 3, 0, cur, l("--candidates", "--num", "--str")); test(spec, a("sub1", "--candidates", "a", "-"), 3, 1, cur, l("-candidates", "-num", "-str")); test(spec, a("sub1", "--candidates", "a", "--"), 3, 2, cur, l("candidates", "num", "str")); test(spec, a("sub1", "--num"), 2, 0, cur, l()); test(spec, a("sub1", "--str"), 2, 0, cur, l()); test(spec, a("sub2"), 1, 0, cur, l("--directory", "--num2", "-d", "Aaa", "Bbb", "Ccc", "subsub1", "subsub2")); test(spec, a("sub2", "-"), 1, 1, cur, l("-directory", "-num2", "d")); test(spec, a("sub2", "-d"), 2, 0, cur, l()); test(spec, a("sub2", "-d", "/"), 3, 0, cur, l("--directory", "--num2", "-d", "Aaa", "Bbb", "Ccc", "subsub1", "subsub2")); test(spec, a("sub2", "-d", "/", "-"), 3, 1, cur, l("-directory", "-num2", "d")); test(spec, a("sub2", "-d", "/", "--"), 3, 2, cur, l("directory", "num2")); test(spec, a("sub2", "-d", "/", "--n"), 3, 3, cur, l("um2")); test(spec, a("sub2", "-d", "/", "--num2"), 3, 6, cur, l("")); test(spec, a("sub2", "-d", "/", "--num2"), 4, 0, cur, l()); test(spec, a("sub2", "-d", "/", "--num2", "0"), 4, 1, cur, l()); test(spec, a("sub2", "-d", "/", "--num2", "0"), 5, 0, cur, l("--directory", "--num2", "-d", "Aaa", "Bbb", "Ccc", "subsub1", "subsub2")); test(spec, a("sub2", "-d", "/", "--num2", "0", "s"), 5, 1, cur, l("ubsub1", "ubsub2")); test(spec, a("sub2", "A"), 1, 1, cur, l("aa")); test(spec, a("sub2", "Aaa"), 1, 3, cur, l("")); test(spec, a("sub2", "Aaa"), 2, 0, cur, l("--directory", "--num2", "-d", "Aaa", "Bbb", "Ccc", "subsub1", "subsub2")); test(spec, a("sub2", "Aaa", "s"), 2, 1, cur, l("ubsub1", "ubsub2")); test(spec, a("sub2", "Aaa", "subsub1"), 3, 0, cur, l("--host", "-h")); test(spec, a("sub2", "subsub1"), 2, 0, cur, l("--host", "-h")); test(spec, a("sub2", "subsub2"), 2, 0, cur, l("--timeUnit", "--timeout", "-t", "-u", "aaa", "bbb", "ccc")); test(spec, a("sub2", "subsub2", "-"), 2, 1, cur, l("-timeUnit", "-timeout", "t", "u")); test(spec, a("sub2", "subsub2", "-t"), 2, 2, cur, l("")); test(spec, a("sub2", "subsub2", "-t"), 3, 0, cur, l()); test(spec, a("sub2", "subsub2", "-t", "0"), 3, 1, cur, l()); test(spec, a("sub2", "subsub2", "-t", "0"), 4, 0, cur, l("--timeUnit", "--timeout", "-t", "-u", "aaa", "bbb", "ccc")); test(spec, a("sub2", "subsub2", "-t", "0", "-"), 4, 1, cur, l("-timeUnit", "-timeout", "t", "u")); test(spec, a("sub2", "subsub2", "-t", "0", "--"), 4, 2, cur, l("timeUnit", "timeout")); test(spec, a("sub2", "subsub2", "-t", "0", "--t"), 4, 3, cur, l("imeUnit", "imeout")); test(spec, a("sub2", "subsub2", "-t", "0", "-u"), 4, 2, cur, l("")); test(spec, a("sub2", "subsub2", "-t", "0", "-u"), 5, 0, cur, timeUnitValues()); test(spec, a("sub2", "subsub2", "-t", "0", "-u", "S"),5, 1, cur, l("ECONDS")); test(spec, a("sub2", "subsub2", "a"), 2, 1, cur, l("aa")); test(spec, a("sub2", "subsub2", "a"), 3, 0, cur, l("--timeUnit", "--timeout", "-t", "-u", "aaa", "bbb", "ccc")); } private static void test(CommandSpec spec, String[] args, int argIndex, int positionInArg, int cursor, List expected) { List actual = new ArrayList(); AutoComplete.complete(spec, args, argIndex, positionInArg, cursor, actual); Collections.sort(actual, new CharSequenceSort()); Collections.sort(expected, new CharSequenceSort()); assertEquals(expected, actual); } private static String[] a(String... args) { return args; } private static List l(CharSequence... args) { return Arrays.asList(args); } private static List timeUnitValues() { List result = new ArrayList(); for (TimeUnit tu : TimeUnit.values()) { result.add(tu.toString()); } return result; } static class CharSequenceSort implements Comparator { public int compare(CharSequence left, CharSequence right) { return left.toString().compareTo(right.toString()); } } @Test(expected = NullPointerException.class) public void testCompleteDisallowsNullSpec() { AutoComplete.complete(null, new String[] {"-x"}, 0, 0, 0, new ArrayList()); } @Test(expected = NullPointerException.class) public void testCompleteDisallowsNullArgs() { AutoComplete.complete(CommandSpec.create().addOption(OptionSpec.builder("-x").build()), null, 0, 0, 0, new ArrayList()); } @Test(expected = NullPointerException.class) public void testCompleteDisallowsNullCandidates() { AutoComplete.complete(CommandSpec.create().addOption(OptionSpec.builder("-x").build()), new String[] {"-x"}, 0, 0, 0, null); } @Test(expected = IllegalArgumentException.class) public void testCompleteDisallowsNegativeArgIndex() { AutoComplete.complete(CommandSpec.create().addOption(OptionSpec.builder("-x").build()), new String[] {"-x"}, -1, 0, 0, new ArrayList()); } @Test(expected = IllegalArgumentException.class) public void testCompleteDisallowsTooLargeArgIndex() { AutoComplete.complete(CommandSpec.create().addOption(OptionSpec.builder("-x").build()), new String[] {"-x"}, 2, 0, 0, new ArrayList()); } @Test(expected = IllegalArgumentException.class) public void testCompleteDisallowsNegativePositionInArg() { AutoComplete.complete(CommandSpec.create().addOption(OptionSpec.builder("-x").build()), new String[] {"-x"}, 0, -1, 0, new ArrayList()); } @Test(expected = IllegalArgumentException.class) public void testCompleteDisallowsTooLargePositionInArg() { AutoComplete.complete(CommandSpec.create().addOption(OptionSpec.builder("-x").build()), new String[] {"-x"}, 0, 3, 0, new ArrayList()); } @Test public void testCompleteAllowsNormalValues() { List candidates = new ArrayList(); AutoComplete.complete(CommandSpec.create().addOption(OptionSpec.builder("-x").build()), new String[] {"-x"}, 0, 0, 0, candidates); assertFalse(candidates.isEmpty()); } enum Possibilities { Aaa, Bbb, Ccc }; @Test public void testCompleteFindCompletionStartPoint() { class App { @Option(names = "-x", arity = "2") List poss; } CommandSpec spec = CommandSpec.forAnnotatedObject(new App()); int cur = 500; test(spec, a("-x"), 1, 0, cur, l("Aaa", "Bbb", "Ccc")); test(spec, a("-x", "A"), 1, 0, cur, l("Aaa", "Bbb", "Ccc")); // suggest 1st arg of same type test(spec, a("-x", "A"), 1, 1, cur, l("aa")); test(spec, a("-x", "Aaa"), 2, 0, cur, l("Aaa", "Bbb", "Ccc")); // suggest 2nd arg of same type test(spec, a("-x", "Aaa", "Bbb"), 3, 0, cur, l("-x")); // we have 2 args for first -x. Suggest -x again. } @Test public void testCompleteFindPositionalForTopLevelCommand() { class App { @Parameters() List poss; } CommandSpec spec = CommandSpec.forAnnotatedObject(new App()); int cur = 500; test(spec, a(), 0, 0, cur, l("Aaa", "Bbb", "Ccc")); test(spec, a("A"), 0, 0, cur, l("Aaa", "Bbb", "Ccc")); test(spec, a("A"), 0, 1, cur, l("aa")); test(spec, a("Aaa"), 1, 0, cur, l("Aaa", "Bbb", "Ccc")); test(spec, a("Aaa", "Bbb"), 2, 0, cur, l("Aaa", "Bbb", "Ccc")); } @Test public void testBashify() { CommandSpec cmd = CommandSpec.create().addOption( OptionSpec.builder("-x") .type(String.class) .paramLabel("_A\tB C") .completionCandidates(Arrays.asList("1")).build()); String actual = AutoComplete.bash("bashify", new CommandLine(cmd)); String expected = format(loadTextFromClasspath("/bashify_completion.bash"), CommandLine.VERSION); assertEquals(expected, actual); } @Test public void testBooleanArgFilter() { @Command(name = "booltest") class App { @Option(names = "-b") boolean primitive; @Option(names = "-B") Boolean object; } String actual = AutoComplete.bash("booltest", new CommandLine(new App())); assertThat(actual, CoreMatchers.containsString("local flag_opts=\"-b -B\"")); } @Test public void testIsPicocliModelObject() throws Exception { Method m = AutoComplete.class.getDeclaredMethod("isPicocliModelObject", Object.class); m.setAccessible(true); assertFalse((Boolean) m.invoke(null, "blah")); assertTrue((Boolean) m.invoke(null, CommandSpec.create())); assertTrue((Boolean) m.invoke(null, OptionSpec.builder("-x").build())); assertTrue((Boolean) m.invoke(null, PositionalParamSpec.builder().build())); } @Test public void testAddCandidatesForArgsFollowingObject() throws Exception { Method m = AutoComplete.class.getDeclaredMethod("addCandidatesForArgsFollowing", Object.class, List.class); m.setAccessible(true); List candidates = new ArrayList(); m.invoke(null, null, candidates); assertTrue("null Object adds no candidates", candidates.isEmpty()); m.invoke(null, new Object(), candidates); assertTrue("non-PicocliModelObject Object adds no candidates", candidates.isEmpty()); List completions = Arrays.asList("x", "y", "z"); PositionalParamSpec positional = PositionalParamSpec.builder().completionCandidates(completions).build(); m.invoke(null, positional, candidates); assertEquals("PositionalParamSpec adds completion candidates", completions, candidates); } @Test public void testAddCandidatesForArgsFollowingNullCommandAddsNoCandidates() throws Exception { Method m = AutoComplete.class.getDeclaredMethod("addCandidatesForArgsFollowing", CommandSpec.class, List.class); m.setAccessible(true); List candidates = new ArrayList(); m.invoke(null, null, candidates); assertTrue("null CommandSpec adds no candidates", candidates.isEmpty()); } @Test public void testAddCandidatesForArgsFollowingNullOptionAddsNoCandidates() throws Exception { Method m = AutoComplete.class.getDeclaredMethod("addCandidatesForArgsFollowing", OptionSpec.class, List.class); m.setAccessible(true); List candidates = new ArrayList(); m.invoke(null, null, candidates); assertTrue("null OptionSpec adds no candidates", candidates.isEmpty()); } @Test public void testAddCandidatesForArgsFollowingNullPositionalParamAddsNoCandidates() throws Exception { Method m = AutoComplete.class.getDeclaredMethod("addCandidatesForArgsFollowing", PositionalParamSpec.class, List.class); m.setAccessible(true); List candidates = new ArrayList(); m.invoke(null, null, candidates); assertTrue("null PositionalParamSpec adds no candidates", candidates.isEmpty()); } @Command(name = "myapp", mixinStandardHelpOptions = true, subcommands = AutoComplete.GenerateCompletion.class) static class MyApp implements Runnable { @Parameters(index = "0", description = "Required positional param") String value; public void run() { } } @Test public void testGenerateCompletionParentUsageMessage() { CommandLine cmd = new CommandLine(new MyApp()); String expected = String.format("" + "Usage: myapp [-hV] [COMMAND]%n" + " Required positional param%n" + " -h, --help Show this help message and exit.%n" + " -V, --version Print version information and exit.%n" + "Commands:%n" + " generate-completion Generate bash/zsh completion script for myapp.%n"); assertEquals(expected, cmd.getUsageMessage(CommandLine.Help.Ansi.OFF)); } @Test public void testGenerateCompletionCanBeHiddenFromParentUsageMessage() { CommandLine cmd = new CommandLine(new MyApp()); CommandLine gen = cmd.getSubcommands().get("generate-completion"); gen.getCommandSpec().usageMessage().hidden(true); String expected = String.format("" + "Usage: myapp [-hV] [COMMAND]%n" + " Required positional param%n" + " -h, --help Show this help message and exit.%n" + " -V, --version Print version information and exit.%n"); assertEquals(expected, cmd.getUsageMessage(CommandLine.Help.Ansi.OFF)); } @Test public void testGenerateCompletionUsageMessage() { CommandLine cmd = new CommandLine(new MyApp()); String expected = String.format("" + "Usage: myapp generate-completion [-hV]%n" + "Generate bash/zsh completion script for myapp.%n" + "Run the following command to give `myapp` TAB completion in the current shell:%n" + "%n" + " source <(myapp generate-completion)%n" + "%n" + "Options:%n" + " -h, --help Show this help message and exit.%n" + " -V, --version Print version information and exit.%n"); CommandLine gen = cmd.getSubcommands().get("generate-completion"); assertEquals(expected, gen.getUsageMessage(CommandLine.Help.Ansi.OFF)); } @Test public void testGenerateCompletionScriptCustomOut() { CommandLine cmd = new CommandLine(new MyApp()); StringWriter sw = new StringWriter(); cmd.setOut(new PrintWriter(sw)); String expected = getCompletionScriptText("myapp"); cmd.execute("generate-completion"); assertEquals(expected, sw.toString()); } @Test public void testGenerateCompletionScriptStandardOut() { int exitCode = new CommandLine(new MyApp()).execute("generate-completion"); assertEquals(CommandLine.ExitCode.OK, exitCode); assertEquals("", systemErrRule.getLog()); assertEquals(getCompletionScriptText("myapp"), systemOutRule.getLog()); } private String getCompletionScriptText(String cmdName) { return String.format("" + "#!/usr/bin/env bash\n" + "#\n" + "# %1$s Bash Completion\n" + "# =======================\n" + "#\n" + "# Bash completion support for the `%1$s` command,\n" + "# generated by [picocli](http://picocli.info/) version %2$s.\n" + "#\n" + "# Installation\n" + "# ------------\n" + "#\n" + "# 1. Source all completion scripts in your .bash_profile\n" + "#\n" + "# cd $YOUR_APP_HOME/bin\n" + "# for f in $(find . -name \"*_completion\"); do line=\". $(pwd)/$f\"; grep \"$line\" ~/.bash_profile || echo \"$line\" >> ~/.bash_profile; done\n" + "#\n" + "# 2. Open a new bash console, and type `%1$s [TAB][TAB]`\n" + "#\n" + "# 1a. Alternatively, if you have [bash-completion](https://github.com/scop/bash-completion) installed:\n" + "# Place this file in a `bash-completion.d` folder:\n" + "#\n" + "# * /etc/bash-completion.d\n" + "# * /usr/local/etc/bash-completion.d\n" + "# * ~/bash-completion.d\n" + "#\n" + "# Documentation\n" + "# -------------\n" + "# The script is called by bash whenever [TAB] or [TAB][TAB] is pressed after\n" + "# '%1$s (..)'. By reading entered command line parameters,\n" + "# it determines possible bash completions and writes them to the COMPREPLY variable.\n" + "# Bash then completes the user input if only one entry is listed in the variable or\n" + "# shows the options if more than one is listed in COMPREPLY.\n" + "#\n" + "# References\n" + "# ----------\n" + "# [1] http://stackoverflow.com/a/12495480/1440785\n" + "# [2] http://tiswww.case.edu/php/chet/bash/FAQ\n" + "# [3] https://www.gnu.org/software/bash/manual/html_node/The-Shopt-Builtin.html\n" + "# [4] http://zsh.sourceforge.net/Doc/Release/Options.html#index-COMPLETE_005fALIASES\n" + "# [5] https://stackoverflow.com/questions/17042057/bash-check-element-in-array-for-elements-in-another-array/17042655#17042655\n" + "# [6] https://www.gnu.org/software/bash/manual/html_node/Programmable-Completion.html#Programmable-Completion\n" + "# [7] https://stackoverflow.com/questions/3249432/can-a-bash-tab-completion-script-be-used-in-zsh/27853970#27853970\n" + "#\n" + "\n" + "if [ -n \"$BASH_VERSION\" ]; then\n" + " # Enable programmable completion facilities when using bash (see [3])\n" + " shopt -s progcomp\n" + "elif [ -n \"$ZSH_VERSION\" ]; then\n" + " # Make alias a distinct command for completion purposes when using zsh (see [4])\n" + " setopt COMPLETE_ALIASES\n" + " alias compopt=complete\n" + "\n" + " # Enable bash completion in zsh (see [7])\n" + " autoload -U +X compinit && compinit\n" + " autoload -U +X bashcompinit && bashcompinit\n" + "fi\n" + "\n" + "# CompWordsContainsArray takes an array and then checks\n" + "# if all elements of this array are in the global COMP_WORDS array.\n" + "#\n" + "# Returns zero (no error) if all elements of the array are in the COMP_WORDS array,\n" + "# otherwise returns 1 (error).\n" + "function CompWordsContainsArray() {\n" + " declare -a localArray\n" + " localArray=(\"$@\")\n" + " local findme\n" + " for findme in \"${localArray[@]}\"; do\n" + " if ElementNotInCompWords \"$findme\"; then return 1; fi\n" + " done\n" + " return 0\n" + "}\n" + "function ElementNotInCompWords() {\n" + " local findme=\"$1\"\n" + " local element\n" + " for element in \"${COMP_WORDS[@]}\"; do\n" + " if [[ \"$findme\" = \"$element\" ]]; then return 1; fi\n" + " done\n" + " return 0\n" + "}\n" + "\n" + "# The `currentPositionalIndex` function calculates the index of the current positional parameter.\n" + "#\n" + "# currentPositionalIndex takes three parameters:\n" + "# the command name,\n" + "# a space-separated string with the names of options that take a parameter, and\n" + "# a space-separated string with the names of boolean options (that don't take any params).\n" + "# When done, this function echos the current positional index to std_out.\n" + "#\n" + "# Example usage:\n" + "# local currIndex=$(currentPositionalIndex \"mysubcommand\" \"$ARG_OPTS\" \"$FLAG_OPTS\")\n" + "function currentPositionalIndex() {\n" + " local commandName=\"$1\"\n" + " local optionsWithArgs=\"$2\"\n" + " local booleanOptions=\"$3\"\n" + " local previousWord\n" + " local result=0\n" + "\n" + " for i in $(seq $((COMP_CWORD - 1)) -1 0); do\n" + " previousWord=${COMP_WORDS[i]}\n" + " if [ \"${previousWord}\" = \"$commandName\" ]; then\n" + " break\n" + " fi\n" + " if [[ \"${optionsWithArgs}\" =~ ${previousWord} ]]; then\n" + " ((result-=2)) # Arg option and its value not counted as positional param\n" + " elif [[ \"${booleanOptions}\" =~ ${previousWord} ]]; then\n" + " ((result-=1)) # Flag option itself not counted as positional param\n" + " fi\n" + " ((result++))\n" + " done\n" + " echo \"$result\"\n" + "}\n" + "\n" + "# Bash completion entry point function.\n" + "# _complete_%1$s finds which commands and subcommands have been specified\n" + "# on the command line and delegates to the appropriate function\n" + "# to generate possible options and subcommands for the last specified subcommand.\n" + "function _complete_%1$s() {\n" + " local cmds0=(generate-completion)\n" + "\n" + " if CompWordsContainsArray \"${cmds0[@]}\"; then _picocli_myapp_generatecompletion; return $?; fi\n" + "\n" + " # No subcommands were specified; generate completions for the top-level command.\n" + " _picocli_%1$s; return $?;\n" + "}\n" + "\n" + "# Generates completions for the options and subcommands of the `%1$s` command.\n" + "function _picocli_%1$s() {\n" + " # Get completion data\n" + " local curr_word=${COMP_WORDS[COMP_CWORD]}\n" + "\n" + " local commands=\"generate-completion\"\n" + " local flag_opts=\"-h --help -V --version\"\n" + " local arg_opts=\"\"\n" + "\n" + " if [[ \"${curr_word}\" == -* ]]; then\n" + " COMPREPLY=( $(compgen -W \"${flag_opts} ${arg_opts}\" -- \"${curr_word}\") )\n" + " else\n" + " local positionals=\"\"\n" + " COMPREPLY=( $(compgen -W \"${commands} ${positionals}\" -- \"${curr_word}\") )\n" + " fi\n" + "}\n" + "\n" + "# Generates completions for the options and subcommands of the `generate-completion` subcommand.\n" + "function _picocli_%1$s_generatecompletion() {\n" + " # Get completion data\n" + " local curr_word=${COMP_WORDS[COMP_CWORD]}\n" + "\n" + " local commands=\"\"\n" + " local flag_opts=\"-h --help -V --version\"\n" + " local arg_opts=\"\"\n" + "\n" + " if [[ \"${curr_word}\" == -* ]]; then\n" + " COMPREPLY=( $(compgen -W \"${flag_opts} ${arg_opts}\" -- \"${curr_word}\") )\n" + " else\n" + " local positionals=\"\"\n" + " COMPREPLY=( $(compgen -W \"${commands} ${positionals}\" -- \"${curr_word}\") )\n" + " fi\n" + "}\n" + "\n" + "# Define a completion specification (a compspec) for the\n" + "# `%1$s`, `%1$s.sh`, and `%1$s.bash` commands.\n" + "# Uses the bash `complete` builtin (see [6]) to specify that shell function\n" + "# `_complete_%1$s` is responsible for generating possible completions for the\n" + "# current word on the command line.\n" + "# The `-o default` option means that if the function generated no matches, the\n" + "# default Bash completions and the Readline default filename completions are performed.\n" + "complete -F _complete_%1$s -o default %1$s %1$s.sh %1$s.bash\n" + "\n", cmdName, CommandLine.VERSION); } //https://github.com/remkop/picocli/issues/887 @Test public void testHiddenOptionsAndSubcommandsNotSuggested() { @Command(name="CompletionDemo", subcommands = { picocli.AutoComplete.GenerateCompletion.class, CommandLine.HelpCommand.class } ) class CompletionSubcommandDemo implements Runnable { @Option(names = "--aaa", hidden = true) int a; @Option(names = "--apples", hidden = false) int apples; @Option(names = "--bbb", hidden = false) int b; public void run() { } } CommandLine cmd = new CommandLine(new CompletionSubcommandDemo()); CommandLine gen = cmd.getSubcommands().get("generate-completion"); gen.getCommandSpec().usageMessage().hidden(true); String expectedUsage = String.format("" + "Usage: CompletionDemo [--apples=] [--bbb=] [COMMAND]%n" + " --apples=%n" + " --bbb=%n" + "Commands:%n" + " help Displays help information about the specified command%n"); assertEquals(expectedUsage, cmd.getUsageMessage(CommandLine.Help.Ansi.OFF)); StringWriter sw = new StringWriter(); cmd.setOut(new PrintWriter(sw)); String expected = getCompletionScriptTextWithHidden("CompletionDemo"); cmd.execute("generate-completion"); assertEquals(expected, sw.toString()); } private String getCompletionScriptTextWithHidden(String commandName) { return String.format("" + "#!/usr/bin/env bash\n" + "#\n" + "# %1$s Bash Completion\n" + "# =======================\n" + "#\n" + "# Bash completion support for the `%1$s` command,\n" + "# generated by [picocli](http://picocli.info/) version %2$s.\n" + "#\n" + "# Installation\n" + "# ------------\n" + "#\n" + "# 1. Source all completion scripts in your .bash_profile\n" + "#\n" + "# cd $YOUR_APP_HOME/bin\n" + "# for f in $(find . -name \"*_completion\"); do line=\". $(pwd)/$f\"; grep \"$line\" ~/.bash_profile || echo \"$line\" >> ~/.bash_profile; done\n" + "#\n" + "# 2. Open a new bash console, and type `%1$s [TAB][TAB]`\n" + "#\n" + "# 1a. Alternatively, if you have [bash-completion](https://github.com/scop/bash-completion) installed:\n" + "# Place this file in a `bash-completion.d` folder:\n" + "#\n" + "# * /etc/bash-completion.d\n" + "# * /usr/local/etc/bash-completion.d\n" + "# * ~/bash-completion.d\n" + "#\n" + "# Documentation\n" + "# -------------\n" + "# The script is called by bash whenever [TAB] or [TAB][TAB] is pressed after\n" + "# '%1$s (..)'. By reading entered command line parameters,\n" + "# it determines possible bash completions and writes them to the COMPREPLY variable.\n" + "# Bash then completes the user input if only one entry is listed in the variable or\n" + "# shows the options if more than one is listed in COMPREPLY.\n" + "#\n" + "# References\n" + "# ----------\n" + "# [1] http://stackoverflow.com/a/12495480/1440785\n" + "# [2] http://tiswww.case.edu/php/chet/bash/FAQ\n" + "# [3] https://www.gnu.org/software/bash/manual/html_node/The-Shopt-Builtin.html\n" + "# [4] http://zsh.sourceforge.net/Doc/Release/Options.html#index-COMPLETE_005fALIASES\n" + "# [5] https://stackoverflow.com/questions/17042057/bash-check-element-in-array-for-elements-in-another-array/17042655#17042655\n" + "# [6] https://www.gnu.org/software/bash/manual/html_node/Programmable-Completion.html#Programmable-Completion\n" + "# [7] https://stackoverflow.com/questions/3249432/can-a-bash-tab-completion-script-be-used-in-zsh/27853970#27853970\n" + "#\n" + "\n" + "if [ -n \"$BASH_VERSION\" ]; then\n" + " # Enable programmable completion facilities when using bash (see [3])\n" + " shopt -s progcomp\n" + "elif [ -n \"$ZSH_VERSION\" ]; then\n" + " # Make alias a distinct command for completion purposes when using zsh (see [4])\n" + " setopt COMPLETE_ALIASES\n" + " alias compopt=complete\n" + "\n" + " # Enable bash completion in zsh (see [7])\n" + " autoload -U +X compinit && compinit\n" + " autoload -U +X bashcompinit && bashcompinit\n" + "fi\n" + "\n" + "# CompWordsContainsArray takes an array and then checks\n" + "# if all elements of this array are in the global COMP_WORDS array.\n" + "#\n" + "# Returns zero (no error) if all elements of the array are in the COMP_WORDS array,\n" + "# otherwise returns 1 (error).\n" + "function CompWordsContainsArray() {\n" + " declare -a localArray\n" + " localArray=(\"$@\")\n" + " local findme\n" + " for findme in \"${localArray[@]}\"; do\n" + " if ElementNotInCompWords \"$findme\"; then return 1; fi\n" + " done\n" + " return 0\n" + "}\n" + "function ElementNotInCompWords() {\n" + " local findme=\"$1\"\n" + " local element\n" + " for element in \"${COMP_WORDS[@]}\"; do\n" + " if [[ \"$findme\" = \"$element\" ]]; then return 1; fi\n" + " done\n" + " return 0\n" + "}\n" + "\n" + "# The `currentPositionalIndex` function calculates the index of the current positional parameter.\n" + "#\n" + "# currentPositionalIndex takes three parameters:\n" + "# the command name,\n" + "# a space-separated string with the names of options that take a parameter, and\n" + "# a space-separated string with the names of boolean options (that don't take any params).\n" + "# When done, this function echos the current positional index to std_out.\n" + "#\n" + "# Example usage:\n" + "# local currIndex=$(currentPositionalIndex \"mysubcommand\" \"$ARG_OPTS\" \"$FLAG_OPTS\")\n" + "function currentPositionalIndex() {\n" + " local commandName=\"$1\"\n" + " local optionsWithArgs=\"$2\"\n" + " local booleanOptions=\"$3\"\n" + " local previousWord\n" + " local result=0\n" + "\n" + " for i in $(seq $((COMP_CWORD - 1)) -1 0); do\n" + " previousWord=${COMP_WORDS[i]}\n" + " if [ \"${previousWord}\" = \"$commandName\" ]; then\n" + " break\n" + " fi\n" + " if [[ \"${optionsWithArgs}\" =~ ${previousWord} ]]; then\n" + " ((result-=2)) # Arg option and its value not counted as positional param\n" + " elif [[ \"${booleanOptions}\" =~ ${previousWord} ]]; then\n" + " ((result-=1)) # Flag option itself not counted as positional param\n" + " fi\n" + " ((result++))\n" + " done\n" + " echo \"$result\"\n" + "}\n" + "\n" + "# Bash completion entry point function.\n" + "# _complete_%1$s finds which commands and subcommands have been specified\n" + "# on the command line and delegates to the appropriate function\n" + "# to generate possible options and subcommands for the last specified subcommand.\n" + "function _complete_%1$s() {\n" + " local cmds0=(help)\n" + "\n" + " if CompWordsContainsArray \"${cmds0[@]}\"; then _picocli_%1$s_help; return $?; fi\n" + "\n" + " # No subcommands were specified; generate completions for the top-level command.\n" + " _picocli_%1$s; return $?;\n" + "}\n" + "\n" + "# Generates completions for the options and subcommands of the `%1$s` command.\n" + "function _picocli_%1$s() {\n" + " # Get completion data\n" + " local curr_word=${COMP_WORDS[COMP_CWORD]}\n" + " local prev_word=${COMP_WORDS[COMP_CWORD-1]}\n" + "\n" + " local commands=\"help\"\n" + // NOTE: no generate-completion: this command is hidden " local flag_opts=\"\"\n" + " local arg_opts=\"--apples --bbb\"\n" + // NOTE: no --aaa: this option is hidden "\n" + " compopt +o default\n" + "\n" + " case ${prev_word} in\n" + " --apples)\n" + " return\n" + " ;;\n" + " --bbb)\n" + " return\n" + " ;;\n" + " esac\n" + "\n" + " if [[ \"${curr_word}\" == -* ]]; then\n" + " COMPREPLY=( $(compgen -W \"${flag_opts} ${arg_opts}\" -- \"${curr_word}\") )\n" + " else\n" + " local positionals=\"\"\n" + " COMPREPLY=( $(compgen -W \"${commands} ${positionals}\" -- \"${curr_word}\") )\n" + " fi\n" + "}\n" + "\n" + "# Generates completions for the options and subcommands of the `help` subcommand.\n" + "function _picocli_%1$s_help() {\n" + " # Get completion data\n" + " local curr_word=${COMP_WORDS[COMP_CWORD]}\n" + "\n" + " local commands=\"\"\n" + " local flag_opts=\"-h --help\"\n" + " local arg_opts=\"\"\n" + "\n" + " if [[ \"${curr_word}\" == -* ]]; then\n" + " COMPREPLY=( $(compgen -W \"${flag_opts} ${arg_opts}\" -- \"${curr_word}\") )\n" + " else\n" + " local positionals=\"\"\n" + " COMPREPLY=( $(compgen -W \"${commands} ${positionals}\" -- \"${curr_word}\") )\n" + " fi\n" + "}\n" + "\n" + "# Define a completion specification (a compspec) for the\n" + "# `%1$s`, `%1$s.sh`, and `%1$s.bash` commands.\n" + "# Uses the bash `complete` builtin (see [6]) to specify that shell function\n" + "# `_complete_%1$s` is responsible for generating possible completions for the\n" + "# current word on the command line.\n" + "# The `-o default` option means that if the function generated no matches, the\n" + "# default Bash completions and the Readline default filename completions are performed.\n" + "complete -F _complete_%1$s -o default %1$s %1$s.sh %1$s.bash\n" + "\n", commandName, CommandLine.VERSION); } @Test public void testNestedCompletion() { @Command(name="Demo", subcommands = { NestedLevel1.class } ) class NestedCompletionDemo implements Runnable { public void run() { } } CommandLine root = new CommandLine(new NestedCompletionDemo()); String expectedRoot = String.format("" + "Usage: Demo [COMMAND]%n" + "Commands:%n" + " Level1%n"); assertEquals(expectedRoot, root.getUsageMessage(CommandLine.Help.Ansi.OFF)); CommandLine level2 = root .getSubcommands().get("Level1") .getSubcommands().get("Level2"); String expectedLevel2 = String.format("" + "Usage: Demo Level1 Level2 [COMMAND]%n" + "Commands:%n" + " generate-completion Generate bash/zsh completion script for Demo.%n"); assertEquals(expectedLevel2, level2.getUsageMessage(CommandLine.Help.Ansi.OFF)); CommandLine gen = level2 .getSubcommands().get("generate-completion"); gen.getCommandSpec().usageMessage().hidden(true); String expectedGen = String.format("" + "Usage: Demo Level1 Level2 generate-completion [-hV]%n" + "Generate bash/zsh completion script for Demo.%n" + "Run the following command to give `Demo` TAB completion in the current shell:%n" + "%n" + " source <(Demo Level1 Level2 generate-completion)%n" + "%n" + "Options:%n" + " -h, --help Show this help message and exit.%n" + " -V, --version Print version information and exit.%n"); assertEquals(expectedGen, gen.getUsageMessage(CommandLine.Help.Ansi.OFF)); } @Command(name = "Level2", subcommands = {picocli.AutoComplete.GenerateCompletion.class}) static class NestedLevel2 implements Runnable { public void run() { } } @Command(name = "Level1", subcommands = {NestedLevel2.class}) static class NestedLevel1 implements Runnable { public void run() { } } }