package picocli; import org.junit.Rule; import org.junit.Test; import org.junit.contrib.java.lang.system.ProvideSystemProperty; import org.junit.contrib.java.lang.system.RestoreSystemProperties; import org.junit.rules.TestRule; import picocli.CommandLine.Command; import picocli.CommandLine.Help.Ansi; import picocli.CommandLine.IDefaultValueProvider; import picocli.CommandLine.Model.ArgSpec; import picocli.CommandLine.Model.CommandSpec; import picocli.CommandLine.Model.OptionSpec; import picocli.CommandLine.Option; import java.util.Map; import java.util.regex.Pattern; import static org.junit.Assert.*; public class NegatableOptionTest { // 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 ProvideSystemProperty ansiOFF = new ProvideSystemProperty("picocli.ansi", "false"); @Test public void testRegexTransformDefault() { CommandLine.INegatableOptionTransformer transformer = CommandLine.RegexTransformer.createDefault(); CommandSpec dummy = CommandSpec.create(); assertEquals("-X:-option", transformer.makeNegative("-X:+option", dummy)); assertEquals("-X:(+|-)option", transformer.makeSynopsis("-X:+option", dummy)); assertEquals("-X:+option", transformer.makeNegative("-X:-option", dummy)); assertEquals("-X:(+|-)option", transformer.makeSynopsis("-X:-option", dummy)); assertEquals("--no-verbose", transformer.makeNegative("--verbose", dummy)); assertEquals("--[no-]verbose", transformer.makeSynopsis("--verbose", dummy)); assertEquals("--verbose", transformer.makeNegative("--no-verbose", dummy)); assertEquals("--[no-]verbose", transformer.makeSynopsis("--no-verbose", dummy)); } @Test public void testRegexTransformDefaultDisabledForShortOptions() { CommandLine.RegexTransformer transformer = CommandLine.RegexTransformer.createDefault(); CommandSpec dummy = CommandSpec.create(); assertEquals("+x", transformer.makeNegative("+x", dummy)); assertEquals("+x", transformer.makeSynopsis("+x", dummy)); assertEquals("-x", transformer.makeNegative("-x", dummy)); assertEquals("-x", transformer.makeSynopsis("-x", dummy)); } @Test public void testRegexTransformCustomForShortOptions() { CommandLine.RegexTransformer transformer = createNegatableShortOptionsTransformer(); CommandSpec dummy = CommandSpec.create(); assertEquals("-x", transformer.makeNegative("+x", dummy)); assertEquals("\u00b1x", transformer.makeSynopsis("+x", dummy)); assertEquals("+x", transformer.makeNegative("-x", dummy)); assertEquals("\u00b1x", transformer.makeSynopsis("-x", dummy)); } static CommandLine.RegexTransformer createNegatableShortOptionsTransformer() { CommandLine.RegexTransformer transformer = new CommandLine.RegexTransformer.Builder(CommandLine.RegexTransformer.createDefault()) .addPattern("^-(\\w)$", "+$1", "\u00b1$1") // TBD include short option transforms by default? .addPattern("^\\+(\\w)$", "-$1", "\u00b1$1") // (same: transform +x to -x) .build(); return transformer; } @Test public void testModelFromReflection() { class App { @Option(names = "-a", negatable = true) boolean a; @Option(names = "-b", negatable = false) boolean b; } CommandLine cmd = new CommandLine(new App()); assertTrue(cmd.getCommandSpec().findOption("-a").negatable()); assertFalse(cmd.getCommandSpec().findOption("-b").negatable()); } @Test public void testUsage() { @Command(usageHelpWidth = 90) class App { @Option(names = "-a", negatable = true, description = "...") boolean a; @Option(names = "--long", negatable = true, description = "...") boolean longWithoutNo; @Option(names = {"-b", "--long-b"}, negatable = true, description = "...") boolean shortAndLong; @Option(names = "--no-verbose", negatable = true, description = "...") boolean longInitiallyNo; @Option(names = {"-n", "--no-verbose-b"}, negatable = true, description = "...") boolean shortAndLongInitiallyNo; @Option(names = {"-c", "--X:-java1"}, negatable = true, description = "...", defaultValue = "true") boolean javaStyle1; @Option(names = { "-X:-java2"}, negatable = true, description = "...", defaultValue = "true") boolean javaStyle2; @Option(names = { "-XX:-java3"}, negatable = true, description = "...", defaultValue = "true") boolean javaStyle3; @Option(names = {"-f", "--X:+java4"}, negatable = true, description = "...", defaultValue = "true") boolean javaStyle4; @Option(names = {"-g", "-X:+java5"}, negatable = true, description = "...", defaultValue = "true") boolean javaStyle5; @Option(names = {"-h", "-XX:+java6"}, negatable = true, description = "...", defaultValue = "true") boolean javaStyle6; } String expected = String.format("" + "Usage:
[-abcfghn] [--[no-]long] [--[no-]verbose] [-X:(+|-)java2] [-XX:(+|-)%n" + " java3]%n" + " -a ...%n" + " -b, --[no-]long-b ...%n" + " -c, --X:(+|-)java1 ...%n" + " -f, --X:(+|-)java4 ...%n" + " -g, -X:(+|-)java5 ...%n" + " -h, -XX:(+|-)java6 ...%n" + " --[no-]long ...%n" + " -n, --[no-]verbose-b ...%n" + " --[no-]verbose ...%n" + " -X:(+|-)java2 ...%n" + " -XX:(+|-)java3 ...%n"); String actual = new CommandLine(new App()).getUsageMessage(Ansi.OFF); assertEquals(expected, actual); } @Test public void testPlusMinusWidth() { @Command(name = "negatable-options-demo", mixinStandardHelpOptions = true) class Demo { @Option(names = "--verbose", negatable = true, description = "Show verbose output") boolean verbose; @Option(names = "-XX:+PrintGCDetails", negatable = true, description = "Prints GC details") boolean printGCDetails; @Option(names = "-XX:-UseG1GC", negatable = true, description = "Use G1 algorithm for GC") boolean useG1GC = true; } String expected = String.format("" + "Usage: negatable-options-demo [-hV] [--[no-]verbose] [-XX:(+|-)PrintGCDetails]%n" + " [-XX:(+|-)UseG1GC]%n" + " -h, --help Show this help message and exit.%n" + " -V, --version Print version information and exit.%n" + " --[no-]verbose Show verbose output%n" + " -XX:(+|-)PrintGCDetails%n" + " Prints GC details%n" + " -XX:(+|-)UseG1GC Use G1 algorithm for GC%n"); String actual = new CommandLine(new Demo()).getUsageMessage(); assertEquals(expected, actual); } @Test public void testParser() { class App { @Option(names = "-a", negatable = true, description = "...") boolean a; @Option(names = "--long", negatable = true, description = "...") boolean longWithoutNo; @Option(names = {"-b", "--long-b"}, negatable = true, description = "...") boolean longB; @Option(names = "--no-verbose", negatable = true, description = "...") boolean noVerbose; @Option(names = {"-n", "--no-verbose-b"}, negatable = true, description = "...") boolean noVerboseB; @Option(names = {"-c", "--X:-java1"}, negatable = true, description = "...", defaultValue = "true") boolean javaStyle1; @Option(names = { "-X:-java2"}, negatable = true, description = "...", defaultValue = "true") boolean javaStyle2; @Option(names = { "-XX:-java3"}, negatable = true, description = "...", defaultValue = "true") boolean javaStyle3; @Option(names = {"-f", "--X:+java4"}, negatable = true, description = "...", defaultValue = "false") boolean javaStyle4; @Option(names = {"-g", "-X:+java5"}, negatable = true, description = "...", defaultValue = "false") boolean javaStyle5; @Option(names = {"-h", "-XX:+java6"}, negatable = true, description = "...", defaultValue = "false") boolean javaStyle6; } String[] args = { "-a", "--no-long", "--no-long-b", "--verbose", "--verbose-b", "--X:+java1", "-X:+java2", "-XX:+java3", "--X:-java4", "-X:-java5", "-XX:-java6", }; App app = new App(); assertFalse(app.a); new CommandLine(app).parseArgs(args); assertTrue(app.a); assertFalse(app.longWithoutNo); assertFalse(app.longB); assertFalse(app.noVerbose); assertFalse(app.noVerboseB); assertTrue(app.javaStyle1); assertTrue(app.javaStyle2); assertTrue(app.javaStyle3); assertFalse(app.javaStyle4); assertFalse(app.javaStyle5); assertFalse(app.javaStyle6); } @Test public void testNegatableDisallowedForNonBooleanOptions() { class App { @Option(names = "--a", negatable = true, description = "...") int a; } App app = new App(); try { //TestUtil.setTraceLevel("DEBUG"); new CommandLine(app); fail("Expected exception"); } catch (CommandLine.InitializationException ex) { String cls = app.getClass().getName(); assertEquals("Only boolean options can be negatable, but field int " + cls + ".a is of type int", ex.getMessage()); } } @Test public void testNegatableMultivalueOptions() { class App { @Option(names = "--a", negatable = true, description = "...") boolean[] a; } App app = new App(); new CommandLine(app).parseArgs("--no-a", "--no-a", "--no-a"); assertArrayEquals(new boolean[]{false, false, false}, app.a); } @Test public void testCustomizeRegexTransformer() { class App { @Option(names = "-a", negatable = true, description = "...") boolean a; @Option(names = {"-b", "--long-b"}, negatable = true, description = "...") boolean b; @Option(names = "+c", negatable = true, description = "...") boolean c; @Option(names = {"+n", "--no-verbose"}, negatable = true, description = "...") boolean noVerbose; } App app = new App(); assertFalse(app.a); assertFalse(app.b); assertFalse(app.c); assertFalse(app.noVerbose); //TestUtil.setTraceLevel("DEBUG"); CommandLine cmd = new CommandLine(app); cmd.setNegatableOptionTransformer(createNegatableShortOptionsTransformer()); String[] args = { "-a", "+b", "-c", "+n" }; cmd.parseArgs(args); assertTrue(app.a); assertFalse(app.b); assertFalse(app.c); assertTrue(app.noVerbose); String expected = String.format("" + "Usage:
[\u00b1a] [\u00b1b] [\u00b1c] [\u00b1n]%n" + " \u00b1a ...%n" + " \u00b1b, --[no-]long-b ...%n" + " \u00b1c ...%n" + " \u00b1n, --[no-]verbose ...%n"); String actual = cmd.getUsageMessage(Ansi.OFF); //cmd.usage(System.out); assertEquals(expected, actual); } @Test public void testCustomizeRegexTransformerShortOptionsBecomeLong() { class App { @Option(names = "-a", negatable = true, description = "...") boolean a; @Option(names = {"-b", "--long-b"}, negatable = true, description = "...") boolean b; @Option(names = "+c", negatable = true, description = "...") boolean c; @Option(names = {"+n", "--no-verbose"}, negatable = true, description = "...") boolean noVerbose; } //TestUtil.setTraceLevel("DEBUG"); App app = new App(); CommandLine cmd = new CommandLine(app); CommandLine.RegexTransformer transformer = new CommandLine.RegexTransformer.Builder(CommandLine.RegexTransformer.createDefault()) .addPattern("^-(\\w)$", "+$1", "(+|-)$1") // TBD include short option transforms by default? .addPattern("^\\+(\\w)$", "-$1", "(+|-)$1") // (same: transform +x to -x) .build(); cmd.setNegatableOptionTransformer(transformer); String[] args = { "-a", "+b", "-c", "+n" }; cmd.parseArgs(args); String expected = String.format("" + "Usage:
[(+|-)a] [(+|-)b] [(+|-)c] [(+|-)n]%n" + " (+|-)a%n" + " ...%n" + " (+|-)b%n" + " , --[no-]long-b ...%n" + " (+|-)c%n" + " ...%n" + " (+|-)n%n" + " , --[no-]verbose ...%n"); String actual = cmd.getUsageMessage(Ansi.OFF); //cmd.usage(System.out); assertEquals(expected, actual); } static class Issue754DefaultProvider implements IDefaultValueProvider { public String defaultValue(ArgSpec argSpec) throws Exception { return ((OptionSpec) argSpec).longestName().equals("-z") ? "true" : null; } } @Test public void testAllowBooleanOptionsToGetValueFromFallback() { @Command(defaultValueProvider = Issue754DefaultProvider.class) class App { @Option(names = "-x", fallbackValue = "false", defaultValue = "true") boolean x; @Option(names = "-y", fallbackValue = "true", defaultValue = "false") boolean y; @Option(names = "-z", fallbackValue = "false") boolean z; } App app = new App(); assertFalse(app.x); assertFalse(app.y); assertFalse(app.z); app.x = true; app.z = true; new CommandLine(app).parseArgs("-x", "-y", "-z"); assertFalse(app.x); assertTrue(app.y); assertFalse(app.z); } static class ConfigDefaultProvider implements IDefaultValueProvider { static boolean status = false; public String defaultValue(ArgSpec argSpec) { return String.valueOf(status); } } @Test public void test754DefaultProvider2() { @Command(name = "commit", defaultValueProvider = ConfigDefaultProvider.class) class GitCommit { // this option has fallbackValue = "true", so when specified on the command line // it is assigned `true`, regardless of the default value @Option(names = "--status", fallbackValue = "true", negatable = true) boolean status; } GitCommit app = new GitCommit(); assertFalse(app.status); new CommandLine(app).parseArgs(); assertFalse(app.status); assertEquals(ConfigDefaultProvider.status, app.status); new CommandLine(app).parseArgs("--status"); assertTrue(app.status); ConfigDefaultProvider.status = true; new CommandLine(app).parseArgs(); assertTrue(app.status); assertEquals(ConfigDefaultProvider.status, app.status); new CommandLine(app).parseArgs("--status"); assertTrue(app.status); } @Test public void testIssue813() { class App { @Option(names = "--no-xxx", defaultValue = "true", negatable = true) boolean xxx; @Option(names = "--yyy", defaultValue = "false", negatable = true) boolean yyy; } App app = new App(); new CommandLine(app).parseArgs("--xxx", "--yyy"); assertTrue(app.xxx); assertTrue(app.yyy); new CommandLine(app).parseArgs("--no-xxx", "--no-yyy"); assertFalse(app.xxx); assertFalse(app.yyy); } @Test public void testDefaultTransformerToString() { CommandLine.INegatableOptionTransformer transformer = CommandLine.RegexTransformer.createDefault(); String actual = transformer.toString().substring(0, transformer.toString().lastIndexOf('@')); assertEquals("picocli.CommandLine$RegexTransformer[replacements={" + "^--no-(\\w(-|\\w)*)$=--$1, " + "^--(\\w(-|\\w)*)$=--no-$1, " + "^(-|--)(\\w*:)\\+(\\w(-|\\w)*)$=$1$2-$3, " + "^(-|--)(\\w*:)\\-(\\w(-|\\w)*)$=$1$2+$3}, " + "synopsis={" + "^--no-(\\w(-|\\w)*)$=--[no-]$1, " + "^--(\\w(-|\\w)*)$=--[no-]$1, " + "^(-|--)(\\w*:)\\+(\\w(-|\\w)*)$=$1$2(+|-)$3, " + "^(-|--)(\\w*:)\\-(\\w(-|\\w)*)$=$1$2(+|-)$3}]", actual); } @Test public void testDefaultTransformerBuilderRemovePattern() { CommandLine.RegexTransformer.Builder builder = new CommandLine.RegexTransformer.Builder(); builder.addPattern("aaa", "-aaa", "!aaa"); builder.addPattern("bbb", "-bbb", "!bbb"); builder.addPattern("ccc", "-ccc", "!ccc"); assertEquals("-aaa", findForKey(builder.replacements, "aaa")); assertEquals("-bbb", findForKey(builder.replacements, "bbb")); assertEquals("-ccc", findForKey(builder.replacements, "ccc")); CommandLine.RegexTransformer.Builder builder1 = builder.removePattern("bbb"); assertSame(builder1, builder); assertEquals(2, builder.replacements.size()); assertEquals(2, builder.synopsis.size()); assertEquals("-aaa", findForKey(builder.replacements, "aaa")); assertEquals("-ccc", findForKey(builder.replacements, "ccc")); assertNull(builder.replacements.get(Pattern.compile("bbb"))); builder.removePattern("aaa"); assertEquals(1, builder.replacements.size()); assertEquals(1, builder.synopsis.size()); assertEquals("-ccc", findForKey(builder.replacements, "ccc")); assertNull(builder.replacements.get(Pattern.compile("bbb"))); assertNull(builder.replacements.get(Pattern.compile("aaa"))); } private String findForKey(Map map, String pattern) { for (Pattern p : map.keySet()) { if (p.pattern().equals(pattern)) { return map.get(p); } } return null; } @Test public void testDuplicateNegatableOption() { class NegOptDupli { @Option(names = "--verbose") boolean verbose; @Option(names = "--no-verbose", negatable = true) boolean noVerbose; } try { new CommandLine(new NegOptDupli()); fail("Expected Exception"); } catch (CommandLine.DuplicateOptionAnnotationsException ex) { String cls = NegOptDupli.class.getName(); assertEquals("Option name '--verbose' is used by both field boolean " + cls + ".noVerbose and field boolean " + cls + ".verbose", ex.getMessage()); } } @Test public void testDuplicateCaseInsensitiveNegatableOption() { class NegOptDupli { @Option(names = "--VERBOSE") boolean verbose; @Option(names = "--no-verbose", negatable = true) boolean noVerbose; } try { new CommandLine(new NegOptDupli()).setOptionsCaseInsensitive(true); fail("Expected Exception"); } catch (CommandLine.DuplicateOptionAnnotationsException ex) { String cls = NegOptDupli.class.getName(); assertEquals("Option name '--VERBOSE' is used by both field boolean " + cls + ".noVerbose and field boolean " + cls + ".verbose", ex.getMessage()); } } @Test public void testDuplicateNegatableOption2() { class NegOptDupli { @Option(names = "--verbose", negatable = true) boolean verbose; @Option(names = "--no-verbose") boolean noVerbose; } try { new CommandLine(new NegOptDupli()); fail("Expected Exception"); } catch (CommandLine.DuplicateOptionAnnotationsException ex) { String cls = NegOptDupli.class.getName(); assertEquals("Option name '--no-verbose' is used by both field boolean " + cls + ".noVerbose and field boolean " + cls + ".verbose", ex.getMessage()); } } @Test public void testDuplicateCaseInsensitiveNegatableOption2() { class NegOptDupli { @Option(names = "--VERBOSE", negatable = true) boolean verbose; @Option(names = "--no-verbose") boolean noVerbose; } try { new CommandLine(new NegOptDupli()).setOptionsCaseInsensitive(true); fail("Expected Exception"); } catch (CommandLine.DuplicateOptionAnnotationsException ex) { String cls = NegOptDupli.class.getName(); assertEquals("Option name '--no-verbose' is used by both field boolean " + cls + ".verbose and field boolean " + cls + ".noVerbose", ex.getMessage()); } } @Test public void testDuplicateCaseInsensitiveNegatableOption3() { class NegOptDupli { @Option(names = "--VERBOSE", negatable = true) boolean verbose; @Option(names = "--NO-verbose") boolean noVerbose; } try { new CommandLine(new NegOptDupli()).setOptionsCaseInsensitive(true); fail("Expected Exception"); } catch (CommandLine.DuplicateOptionAnnotationsException ex) { String cls = NegOptDupli.class.getName(); assertEquals("Option name '--NO-verbose' is used by both field boolean " + cls + ".verbose and field boolean " + cls + ".noVerbose", ex.getMessage()); } } @Test public void testDuplicateCaseInsensitiveNegatableOption4() { class NegOptDupli { @Option(names = "--VERBOSE") boolean verbose; @Option(names = "--NO-verbose", negatable = true) boolean noVerbose; } try { new CommandLine(new NegOptDupli()).setOptionsCaseInsensitive(true); fail("Expected Exception"); } catch (CommandLine.DuplicateOptionAnnotationsException ex) { String cls = NegOptDupli.class.getName(); assertEquals("Option name '--VERBOSE' is used by both field boolean " + cls + ".noVerbose and field boolean " + cls + ".verbose", ex.getMessage()); } } @Test public void testDuplicateNegatableOption5() { class NegOptDupli { @Option(names = "--no-verbose", negatable = true) boolean noVerbose; @Option(names = "--verbose") boolean verbose; } try { new CommandLine(new NegOptDupli()); fail("Expected Exception"); } catch (CommandLine.DuplicateOptionAnnotationsException ex) { String cls = NegOptDupli.class.getName(); assertEquals("Option name '--verbose' is used by both field boolean " + cls + ".verbose and field boolean " + cls + ".noVerbose", ex.getMessage()); } } @Test public void testDuplicateCaseInsensitiveNegatableOption5() { class NegOptDupli { @Option(names = "--NO-verbose", negatable = true) boolean noVerbose; @Option(names = "--VERBOSE") boolean verbose; } try { new CommandLine(new NegOptDupli()).setOptionsCaseInsensitive(true); fail("Expected Exception"); } catch (CommandLine.DuplicateOptionAnnotationsException ex) { String cls = NegOptDupli.class.getName(); assertEquals("Option name '--VERBOSE' is used by both field boolean " + cls + ".noVerbose and field boolean " + cls + ".verbose", ex.getMessage()); } } @Test public void testDuplicateNegatableOption6() { class NegOptDupli { @Option(names = "--no-verbose") boolean noVerbose; @Option(names = "--verbose", negatable = true) boolean verbose; } try { new CommandLine(new NegOptDupli()); fail("Expected Exception"); } catch (CommandLine.DuplicateOptionAnnotationsException ex) { String cls = NegOptDupli.class.getName(); assertEquals("Option name '--no-verbose' is used by both field boolean " + cls + ".verbose and field boolean " + cls + ".noVerbose", ex.getMessage()); } } @Test public void testDuplicateCaseInsensitiveNegatableOption6() { class NegOptDupli { @Option(names = "--NO-verbose") boolean noVerbose; @Option(names = "--VERBOSE", negatable = true) boolean verbose; } try { new CommandLine(new NegOptDupli()).setOptionsCaseInsensitive(true); fail("Expected Exception"); } catch (CommandLine.DuplicateOptionAnnotationsException ex) { String cls = NegOptDupli.class.getName(); assertEquals("Option name '--NO-verbose' is used by both field boolean " + cls + ".verbose and field boolean " + cls + ".noVerbose", ex.getMessage()); } } }