diff --git a/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/TaskHelper.java b/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/TaskHelper.java index 23b3dfb7079f5..689c8b24743e9 100644 --- a/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/TaskHelper.java +++ b/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/TaskHelper.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -535,18 +535,21 @@ public List<String> handleOptions(T task, String[] args) throws BadArgs { } Option<?> opt = pluginOption == null ? option : pluginOption; String param = null; + boolean potentiallyGnuOption = false; if (opt.hasArg) { if (name.startsWith("--") && name.indexOf('=') > 0) { param = name.substring(name.indexOf('=') + 1, name.length()); } else if (i + 1 < args.length) { + potentiallyGnuOption = true; param = args[++i]; } - if (param == null || param.isEmpty() - || (param.length() >= 2 && param.charAt(0) == '-' - && param.charAt(1) == '-')) { - throw new BadArgs("err.missing.arg", name). - showUsage(true); + if (param == null || param.isEmpty()) { + throw new BadArgs("err.missing.arg", name).showUsage(true); + } + if (potentiallyGnuOption && param.length() >= 2 && + param.charAt(0) == '-' && param.charAt(1) == '-') { + throw new BadArgs("err.ambiguous.arg", name).showUsage(false); } } if (pluginOption != null) { diff --git a/src/jdk.jlink/share/classes/jdk/tools/jlink/resources/jlink.properties b/src/jdk.jlink/share/classes/jdk/tools/jlink/resources/jlink.properties index a491b758ea0ee..080d51506a646 100644 --- a/src/jdk.jlink/share/classes/jdk/tools/jlink/resources/jlink.properties +++ b/src/jdk.jlink/share/classes/jdk/tools/jlink/resources/jlink.properties @@ -1,5 +1,5 @@ # -# Copyright (c) 2015, 2024, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2015, 2025, Oracle and/or its affiliates. All rights reserved. # DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. # # This code is free software; you can redistribute it and/or modify it @@ -147,6 +147,7 @@ err.dir.exists={0} already exists err.badpattern=bad pattern {0} err.unknown.option=unknown option: {0} err.missing.arg=no value given for {0} +err.ambiguous.arg=value for option {0} starts with \"--\" should use {0}=<value> format err.internal.error=internal error: {0} {1} {2} err.invalid.arg.for.option={0} does not accept \"{1}\" argument err.option.after.class=option must be specified before classes: {0} diff --git a/test/jdk/tools/jlink/TaskHelperTest.java b/test/jdk/tools/jlink/TaskHelperTest.java new file mode 100644 index 0000000000000..51dea8de24a9a --- /dev/null +++ b/test/jdk/tools/jlink/TaskHelperTest.java @@ -0,0 +1,220 @@ +/* + * Copyright (c) 2024, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import java.io.IOException; +import java.util.*; +import java.util.stream.Stream; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import jdk.tools.jlink.internal.PluginRepository; +import jdk.tools.jlink.internal.TaskHelper; +import jdk.tools.jlink.internal.TaskHelper.Option; +import jdk.tools.jlink.internal.TaskHelper.OptionsHelper; +import jdk.tools.jlink.plugin.Plugin; +import jdk.tools.jlink.plugin.ResourcePool; +import jdk.tools.jlink.plugin.ResourcePoolBuilder; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import jdk.tools.jlink.internal.TaskHelper.BadArgs; + +/* + * @test + * @summary Test TaskHelper option parsing + * @bug 8303884 + * @modules jdk.jlink/jdk.tools.jlink.internal + * jdk.jlink/jdk.tools.jlink.plugin + * @run junit TaskHelperTest + */ +public class TaskHelperTest { + private static TaskHelper taskHelper; + private static OptionsHelper<TaskHelperTest> optionsHelper; + + private static final List<Option<TaskHelperTest>> OPTIONS = List.of( + new Option<>(true, (task, opt, arg) -> { + System.out.println(arg); + mainArgValue = arg; + }, true, "--main-expecting"), + new Option<>(false, (task, opt, arg) -> { + mainFlag = true; + }, true, "--main-no-arg") + ); + + private static String argValue; + private static String mainArgValue; + private static boolean mainFlag = false; + + public record ArgTestCase(String cmdLine, String[] tokens, String pluginArgValue, String mainArgValue, boolean mainFlagSet) {}; + + public static class TestPluginWithRawOption implements Plugin { + @Override + public ResourcePool transform(ResourcePool in, ResourcePoolBuilder out) { + return out.build(); + } + + @Override + public boolean hasArguments() { + return true; + } + + @Override + public boolean hasRawArgument() { + return true; + } + + @Override + public String getName() { + return "raw-arg-plugin"; + } + + @Override + public void configure(Map<String, String> config) { + config.forEach((k, v) -> { + System.out.println(k + " -> " + v); + }); + var v = config.get(getName()); + if (v == null) + throw new AssertionError(); + argValue = v; + } + } + + @BeforeAll + public static void setup() { + taskHelper = new TaskHelper(TaskHelper.JLINK_BUNDLE); + optionsHelper = taskHelper.newOptionsHelper(TaskHelperTest.class, OPTIONS.toArray(Option[]::new)); + PluginRepository.registerPlugin(new TestPluginWithRawOption()); + } + + @BeforeEach + public void reset() { + argValue = null; + mainArgValue = null; + mainFlag = false; + } + + public static Stream<ArgTestCase> gnuStyleUsages() { + return Stream.of( + new ArgTestCase( + "--main-expecting=--main-no-arg --main-no-arg", + new String[] { "--main-expecting=--main-no-arg", "--main-no-arg" }, + null, + "--main-no-arg", + true + ), + new ArgTestCase( + "--main-expecting ' --main-no-arg' --main-no-arg", + new String[] { "--main-expecting", " --main-no-arg", "--main-no-arg" }, + null, + " --main-no-arg", + true + ), + new ArgTestCase( + "--raw-arg-plugin=--main-no-arg --main-no-arg", + new String[] { "--raw-arg-plugin=--main-no-arg", "--main-no-arg" }, + "--main-no-arg", + null, + true + ), + new ArgTestCase( + "--raw-arg-plugin ' --main-no-arg' --main-no-arg", + new String[] { "--raw-arg-plugin", " --main-no-arg", "--main-no-arg" }, + " --main-no-arg", + null, + true + ), + new ArgTestCase( + "--raw-arg-plugin=--main-expecting=value --main-no-arg", + new String[] { "--raw-arg-plugin=--main-expecting=value", "--main-no-arg" }, + "--main-expecting=value", + null, + true + ), + new ArgTestCase( + "--raw-arg-plugin='--main-expecting value' --main-no-arg", + new String[] { "--raw-arg-plugin=--main-expecting value", "--main-no-arg" }, + "--main-expecting value", + null, + true + ), + new ArgTestCase( + "--raw-arg-plugin='--main-expecting value' --main-expecting realValue", + new String[] { "--raw-arg-plugin=--main-expecting value", "--main-expecting", "realValue" }, + "--main-expecting value", + "realValue", + false + )); + } + + @ParameterizedTest + @MethodSource("gnuStyleUsages") + public void testGnuStyleOptionAsArgValue(ArgTestCase testCase) throws TaskHelper.BadArgs { + System.out.println("Test cmdline: " + testCase.cmdLine()); + var args = testCase.tokens(); + var remaining = optionsHelper.handleOptions(this, args); + try { + // trigger Plugin::configure + taskHelper.getPluginsConfig(null, null, null); + } catch (IOException ex) { + fail("Unexpected IOException"); + } + assertTrue(remaining.isEmpty()); + assertEquals(testCase.mainFlagSet(), mainFlag); + assertEquals(testCase.pluginArgValue(), argValue); + assertEquals(testCase.mainArgValue(), mainArgValue); + } + + @Test + public void testGnuStyleOptionAsArgValueMissing() { + var invalidFormat = new String[][] { + { "--main-expecting", "--main-no-arg --list", "--main-no-arg" }, + { "--main-expecting", "--main-no-arg", "--main-no-arg" }, + { "--raw-arg-plugin", "--main-no-arg --list", "--main-no-arg" }, + { "--raw-arg-plugin", "--main-no-arg", "--main-no-arg" }, + { "--raw-arg-plugin", "--main-expecting", "value", "--main-no-arg" } + }; + + for (var args: invalidFormat) { + try { + optionsHelper.handleOptions(this, args); + fail("Should get an ambiguous error"); + } catch (BadArgs ex) { + // expected + } + } + } + + @Test + public void testRemaining() throws BadArgs { + String[] args = { "--raw-arg-plugin=--main-expecting", "value", "--main-no-arg" }; + var remaining = optionsHelper.handleOptions(this, args); + assertEquals(2, remaining.size()); + } +} \ No newline at end of file