/*
   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.FileNotFoundException;
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.ListResourceBundle;
import java.util.PropertyResourceBundle;
import java.util.Scanner;
import java.util.concurrent.TimeUnit;

import static java.lang.String.format;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.not;
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<CommandLine> 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<String> {
        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<InetAddress> 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<InetAddress> 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<InetAddress> 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<InetAddress> 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=<factoryClass>] [-n=<commandName>]%n" +
            "                            [-o=<autoCompleteScript>] [@<filename>...]%n" +
            "                            <commandLineFQCN>%n" +
            "Generates a bash completion script for the specified command class.%n" +
            "      [@<filename>...]       One or more argument files containing options.%n" +
            "      <commandLineFQCN>      Fully qualified class name of the annotated%n" +
            "                               `@Command` class to generate a completion script%n" +
            "                               for.%n" +
            "  -c, --factory=<factoryClass>%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=<commandName>   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, '<CLASS-SIMPLE-NAME>' (in%n" +
            "                               lower-case) is used.%n" +
            "  -o, --completionScript=<autoCompleteScript>%n" +
            "                             Optionally specify the path of the completion%n" +
            "                               script file to generate. When omitted, a file%n" +
            "                               named '<commandName>_completion' is generated in%n" +
            "                               the current directory.%n" +
            "  -w, --writeCommandScript   Write a '<commandName>' 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.2.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: '<commandLineFQCN>'%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: '<commandLineFQCN>'%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.<init>()"));
                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.<init>()"));
        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> K create(Class<K> 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<CharSequence> expected) {
        List<CharSequence> actual = new ArrayList<CharSequence>();
        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<CharSequence> l(CharSequence... args) {
        return Arrays.asList(args);
    }

    private static List<CharSequence> timeUnitValues() {
        List<CharSequence> result = new ArrayList<CharSequence>();
        for (TimeUnit tu : TimeUnit.values()) { result.add(tu.toString()); }
        return result;
    }

    static class CharSequenceSort implements Comparator<CharSequence> {
        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<CharSequence>());
    }

    @Test(expected = NullPointerException.class)
    public void testCompleteDisallowsNullArgs() {
        AutoComplete.complete(CommandSpec.create().addOption(OptionSpec.builder("-x").build()), null, 0, 0, 0, new ArrayList<CharSequence>());
    }

    @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<CharSequence>());
    }

    @Test(expected = IllegalArgumentException.class)
    public void testCompleteDisallowsTooLargeArgIndex() {
        AutoComplete.complete(CommandSpec.create().addOption(OptionSpec.builder("-x").build()), new String[] {"-x"}, 2, 0, 0, new ArrayList<CharSequence>());
    }

    @Test(expected = IllegalArgumentException.class)
    public void testCompleteDisallowsNegativePositionInArg() {
        AutoComplete.complete(CommandSpec.create().addOption(OptionSpec.builder("-x").build()), new String[] {"-x"}, 0, -1, 0, new ArrayList<CharSequence>());
    }

    @Test(expected = IllegalArgumentException.class)
    public void testCompleteDisallowsTooLargePositionInArg() {
        AutoComplete.complete(CommandSpec.create().addOption(OptionSpec.builder("-x").build()), new String[] {"-x"}, 0, 3, 0, new ArrayList<CharSequence>());
    }

    @Test
    public void testCompleteAllowsNormalValues() {
        List<CharSequence> candidates = new ArrayList<CharSequence>();
        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<Possibilities> 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<Possibilities> 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, 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<String> candidates = new ArrayList<String>();
        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<String> 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<String> candidates = new ArrayList<String>();
        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<String> candidates = new ArrayList<String>();
        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<String> candidates = new ArrayList<String>();
        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] <value> [COMMAND]%n" +
                "      <value>     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] <value> [COMMAND]%n" +
                "      <value>     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=<apples>] [--bbb=<b>] [COMMAND]%n" +
                "      --apples=<apples>%n" +
                "      --bbb=<b>%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() {
        }
    }

    @Command(name = "${project.cli.command}", resourceBundle = "picocli.Issue1352ResourceBundle",
            mixinStandardHelpOptions = true)
    static class Issue1352CommandWithResourceBundle {}

    @Test
    public void testIssue1352_CommandNameResourceBundle() throws FileNotFoundException {
        // AutoComplete.bash("scriptname", new CommandLine(new Issue1352CommandWithResourceBundle()));
        File existingScript = new File("mycommandfromresourcebundle_completion");
        if (existingScript.exists()) {
            assertTrue(existingScript.delete());
        }
        try {
            AutoComplete.main(Issue1352CommandWithResourceBundle.class.getName());

            assertEquals("", systemErrRule.getLog());
            assertEquals("", systemOutRule.getLog());

            assertTrue("Expected file '" + existingScript.getAbsolutePath() + "' to exist",
                    existingScript.exists());

            Scanner scanner = new Scanner(existingScript);
            scanner.useDelimiter("\\Z"); // end of file
            String script = scanner.next();
            scanner.close();
            assertThat(script, containsString("mycommandfromresourcebundle"));
            assertThat(script, not(containsString("${project.cli.command}")));
        } finally {
            existingScript.delete();
        }
    }

    @Command(name = "parent", resourceBundle = "picocli.Issue1352ResourceBundle",
            subcommands = Issue1352CommandWithResourceBundle.class,
            mixinStandardHelpOptions = true)
    static class Issue1352ParentCommand {}

    @Test
    public void testIssue1352_SubcommandNameResourceBundle() throws FileNotFoundException {
        File existingScript = new File("parent_completion");
        if (existingScript.exists()) {
            assertTrue(existingScript.delete());
        }
        try {
            AutoComplete.main(Issue1352ParentCommand.class.getName());

            assertEquals("", systemErrRule.getLog());
            assertEquals("", systemOutRule.getLog());

            assertTrue("Expected file '" + existingScript.getAbsolutePath() + "' to exist",
                    existingScript.exists());

            Scanner scanner = new Scanner(existingScript);
            scanner.useDelimiter("\\Z"); // end of file
            String script = scanner.next();
            scanner.close();
            assertThat(script, containsString("_picocli_parent"));
            assertThat(script, containsString("mycommandfromresourcebundle"));

            // FIXME: the below assertion fails with picocli 4.6.1...
            //assertThat(script, not(containsString("${project.cli.command}")));

        } finally {
            existingScript.delete();
        }
    }

    @CommandLine.Command(name = "aliases", aliases = {"a"})
    static class Issue1388AliasesCommand {}

    @CommandLine.Command(name = "aliases-parent", subcommands = {Issue1388AliasesCommand.class})
    static class Issue1388AliasesParentCommand {}

    @Test
    public void testIssue1388_AliasesCommand() throws FileNotFoundException {
        File existingScript = new File("aliases-parent_completion");
        if (existingScript.exists()) {
            assertTrue(existingScript.delete());
        }
        try {
            AutoComplete.main(Issue1388AliasesParentCommand.class.getName());

            assertEquals("", systemErrRule.getLog());
            assertEquals("", systemOutRule.getLog());

            assertTrue("Expected file '" + existingScript.getAbsolutePath() + "' to exist",
                    existingScript.exists());

            Scanner scanner = new Scanner(existingScript);
            scanner.useDelimiter("\\Z"); // end of file
            String script = scanner.next();
            scanner.close();
            assertThat(script, containsString("local cmds0=(aliases)"));
            assertThat(script, containsString("local cmds1=(a)"));
        } finally {
            existingScript.delete();
        }
    }
}
