diff --git a/jetty-core/jetty-start/src/main/java/org/eclipse/jetty/start/CommandLineBuilder.java b/jetty-core/jetty-start/src/main/java/org/eclipse/jetty/start/CommandLineBuilder.java index 7c28dab2935c..fbdc74aa863a 100644 --- a/jetty-core/jetty-start/src/main/java/org/eclipse/jetty/start/CommandLineBuilder.java +++ b/jetty-core/jetty-start/src/main/java/org/eclipse/jetty/start/CommandLineBuilder.java @@ -18,6 +18,7 @@ import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; +import java.util.Objects; public class CommandLineBuilder { @@ -38,37 +39,83 @@ public static String findJavaBin() return "java"; } - /** - * Perform an optional quoting of the argument, being intelligent with spaces and quotes as needed. If a subString is set in quotes it won't the subString - * won't be escaped. - * - * @param arg the argument to quote - * @return the quoted and escaped argument - * @deprecated no replacement, quoting is done by {@link #toQuotedString()} now. - */ - @Deprecated - public static String quote(String arg) + private final StringBuilder commandLine = new StringBuilder(); + private final List args = new ArrayList<>(); + private final String separator; + + public CommandLineBuilder() { - return "'" + arg + "'"; + this(false); } - private List args; - - public CommandLineBuilder() + public CommandLineBuilder(boolean multiline) { - args = new ArrayList(); + separator = multiline ? (" \\" + System.lineSeparator() + " ") : " "; } - public CommandLineBuilder(String bin) + /** + * Quote a string suitable for use with a command line shell using double quotes. + *

This method applies doubles quoting as described for the unix {@code sh} commands: + * Enclosing characters within double quotes preserves the literal meaning of all characters except + * dollarsign ($), backquote (`), and backslash (\). + * The backslash inside double quotes is historically weird, and serves + * to quote only the following characters: {@code $ ` " \ newline}. + * Otherwise, it remains literal. + * + * @param input The string to quote if needed + * @return The quoted string or the original string if quotes are not necessary + */ + public static String shellQuoteIfNeeded(String input) { - this(); - args.add(bin); + if (input == null) + return null; + if (input.length() == 0) + return "\"\""; + + int i = 0; + boolean needsQuoting = false; + while (!needsQuoting && i < input.length()) + { + char c = input.charAt(i++); + + // needs quoting unless a limited set of known good characters + needsQuoting = !( + (c >= 'A' && c <= 'Z') || + (c >= 'a' && c <= 'z') || + (c >= '0' && c <= '9') || + c == '/' || + c == ':' || + c == '.' || + c == ',' || + c == '-' || + c == '_' + ); + } + + if (!needsQuoting) + return input; + + StringBuilder builder = new StringBuilder(input.length() * 2); + builder.append('"'); + builder.append(input, 0, --i); + + while (i < input.length()) + { + char c = input.charAt(i++); + switch (c) + { + case '"', '\\', '`', '$' -> builder.append('\\').append(c); + default -> builder.append(c); + } + } + + builder.append('"'); + + return builder.toString(); } /** - * Add a simple argument to the command line. - *

- * Will quote arguments that have a space in them. + * Add a simple argument to the command line, quoted if necessary. * * @param arg the simple argument to add */ @@ -76,49 +123,66 @@ public void addArg(String arg) { if (arg != null) { + if (commandLine.length() > 0) + commandLine.append(separator); args.add(arg); + commandLine.append(shellQuoteIfNeeded(arg)); } } /** - * Similar to {@link #addArg(String)} but concats both name + value with an "=" sign, quoting were needed, and excluding the "=" portion if the value is - * undefined or empty. - * - *

-     *   addEqualsArg("-Dname", "value") = "-Dname=value"
-     *   addEqualsArg("-Djetty.home", "/opt/company inc/jetty (7)/") = "-Djetty.home=/opt/company\ inc/jetty\ (7)/"
-     *   addEqualsArg("-Djenkins.workspace", "/opt/workspaces/jetty jdk7/") = "-Djenkins.workspace=/opt/workspaces/jetty\ jdk7/"
-     *   addEqualsArg("-Dstress", null) = "-Dstress"
-     *   addEqualsArg("-Dstress", "") = "-Dstress"
-     * 
- * + * Add a "name=value" style argument to the command line with + * name and value quoted if necessary. * @param name the name * @param value the value */ - public void addEqualsArg(String name, String value) + public void addArg(String name, String value) { + Objects.requireNonNull(name); + + if (commandLine.length() > 0) + commandLine.append(separator); + if ((value != null) && (value.length() > 0)) { args.add(name + "=" + value); + commandLine.append(shellQuoteIfNeeded(name)).append('=').append(shellQuoteIfNeeded(value)); } else { args.add(name); + commandLine.append(shellQuoteIfNeeded(name)); } } /** - * Add a simple argument to the command line. - *

- * Will NOT quote/escape arguments that have a space in them. - * - * @param arg the simple argument to add + * Add a "-Oname=value" style argument to the command line with + * name and value quoted if necessary. + * @param option the option + * @param name the name + * @param value the value */ - public void addRawArg(String arg) + public void addArg(String option, String name, String value) { - if (arg != null) + Objects.requireNonNull(option); + + if (commandLine.length() > 0) + commandLine.append(separator); + + if (name == null || name.length() == 0) { - args.add(arg); + commandLine.append(option); + args.add(option); + } + else if ((value != null) && (value.length() > 0)) + { + args.add(option + name + "=" + value); + commandLine.append(option).append(shellQuoteIfNeeded(name)).append('=').append(shellQuoteIfNeeded(value)); + } + else + { + args.add(option + name); + commandLine.append(option).append(shellQuoteIfNeeded(name)); } } @@ -129,20 +193,12 @@ public List getArgs() @Override public String toString() - { - return toString(" "); - } - - public String toString(String delim) { StringBuilder buf = new StringBuilder(); - for (String arg : args) { if (buf.length() > 0) - { - buf.append(delim); - } + buf.append(' '); buf.append(arg); // we assume escaping has occurred during addArg } @@ -154,23 +210,9 @@ public String toString(String delim) * * @return the toString but each arg that has spaces is surrounded by {@code '} (single-quote tick) */ - public String toQuotedString() + public String toCommandLine() { - StringBuilder buf = new StringBuilder(); - - for (String arg : args) - { - if (buf.length() > 0) - buf.append(' '); - boolean needsQuotes = (arg.contains(" ")); - if (needsQuotes) - buf.append("'"); - buf.append(arg); - if (needsQuotes) - buf.append("'"); - } - - return buf.toString(); + return commandLine.toString(); } public void debug() diff --git a/jetty-core/jetty-start/src/main/java/org/eclipse/jetty/start/Main.java b/jetty-core/jetty-start/src/main/java/org/eclipse/jetty/start/Main.java index afe0e875af91..4ccc2325ac32 100644 --- a/jetty-core/jetty-start/src/main/java/org/eclipse/jetty/start/Main.java +++ b/jetty-core/jetty-start/src/main/java/org/eclipse/jetty/start/Main.java @@ -436,7 +436,7 @@ public void start(StartArgs args) throws IOException, InterruptedException { CommandLineBuilder cmd = args.getMainArgs(args.getDryRunParts()); cmd.debug(); - System.out.println(cmd.toQuotedString()); + System.out.println(cmd.toCommandLine()); } if (args.isStopCommand()) diff --git a/jetty-core/jetty-start/src/main/java/org/eclipse/jetty/start/StartArgs.java b/jetty-core/jetty-start/src/main/java/org/eclipse/jetty/start/StartArgs.java index 4b6585bfec40..5fb9a01287b4 100644 --- a/jetty-core/jetty-start/src/main/java/org/eclipse/jetty/start/StartArgs.java +++ b/jetty-core/jetty-start/src/main/java/org/eclipse/jetty/start/StartArgs.java @@ -182,6 +182,7 @@ public class StartArgs private boolean listConfig = false; private boolean version = false; private boolean dryRun = false; + private boolean multiLine = false; private final Set dryRunParts = new HashSet<>(); private boolean jpms = false; private boolean createStartD = false; @@ -519,7 +520,7 @@ public CommandLineBuilder getMainArgs(Set parts) throws IOException if (parts.isEmpty()) parts = ALL_PARTS; - CommandLineBuilder cmd = new CommandLineBuilder(); + CommandLineBuilder cmd = new CommandLineBuilder(multiLine); // Special Stop/Shutdown properties ensureSystemPropertySet("STOP.PORT"); @@ -527,13 +528,13 @@ public CommandLineBuilder getMainArgs(Set parts) throws IOException ensureSystemPropertySet("STOP.WAIT"); if (parts.contains("java")) - cmd.addRawArg(CommandLineBuilder.findJavaBin()); + cmd.addArg(CommandLineBuilder.findJavaBin()); if (parts.contains("opts")) { - cmd.addRawArg("-Djava.io.tmpdir=" + System.getProperty("java.io.tmpdir")); - cmd.addRawArg("-Djetty.home=" + baseHome.getHome()); - cmd.addRawArg("-Djetty.base=" + baseHome.getBase()); + cmd.addArg("-D", "java.io.tmpdir", System.getProperty("java.io.tmpdir")); + cmd.addArg("-D", "jetty.home", baseHome.getHome()); + cmd.addArg("-D", "jetty.base", baseHome.getBase()); Props properties = jettyEnvironment.getProperties(); for (String x : getJvmArgSources().keySet()) @@ -545,11 +546,11 @@ public CommandLineBuilder getMainArgs(Set parts) throws IOException String value = assign.length == 1 ? "" : assign[1]; Prop p = processSystemProperty(key, value, null); - cmd.addRawArg("-D" + p.key + "=" + properties.expand(p.value)); + cmd.addArg("-D", p.key, properties.expand(p.value)); } else { - cmd.addRawArg(properties.expand(x)); + cmd.addArg(properties.expand(x)); } } @@ -557,15 +558,16 @@ public CommandLineBuilder getMainArgs(Set parts) throws IOException for (String propKey : systemPropertySource.keySet()) { String value = System.getProperty(propKey); - cmd.addEqualsArg("-D" + propKey, value); + cmd.addArg("-D", propKey, value); } } if (parts.contains("path")) { + Classpath classpath = jettyEnvironment.getClasspath(); if (isJPMS()) { - Map> dirsAndFiles = StreamSupport.stream(jettyEnvironment.getClasspath().spliterator(), false) + Map> dirsAndFiles = StreamSupport.stream(classpath.spliterator(), false) .collect(Collectors.groupingBy(Files::isDirectory)); List paths = dirsAndFiles.get(false); @@ -594,38 +596,38 @@ public CommandLineBuilder getMainArgs(Set parts) throws IOException if (!files.isEmpty()) { - cmd.addRawArg("--module-path"); + cmd.addArg("--module-path", null, null); String modules = files.stream() .map(Path::toAbsolutePath) .map(Path::toString) .collect(Collectors.joining(FS.pathSeparator())); - cmd.addRawArg(modules); + cmd.addArg(modules); } List dirs = dirsAndFiles.get(true); if (dirs != null && !dirs.isEmpty()) { - cmd.addRawArg("--class-path"); + cmd.addArg("--class-path", null, null); String directories = dirs.stream() .map(Path::toAbsolutePath) .map(Path::toString) .collect(Collectors.joining(FS.pathSeparator())); - cmd.addRawArg(directories); + cmd.addArg(directories); } generateJpmsArgs(cmd); } - else + else if (!classpath.isEmpty()) { - cmd.addRawArg("--class-path"); - cmd.addRawArg(jettyEnvironment.getClasspath().toString()); + cmd.addArg("--class-path"); + cmd.addArg(classpath.toString()); } } if (parts.contains("main")) { if (isJPMS()) - cmd.addRawArg("--module"); - cmd.addRawArg(getMainClassname()); + cmd.addArg("--module"); + cmd.addArg(getMainClassname()); } // do properties and xmls @@ -637,7 +639,8 @@ public CommandLineBuilder getMainArgs(Set parts) throws IOException // pass properties as args for (Prop p : properties) { - cmd.addRawArg(CommandLineBuilder.quote(p.key) + "=" + CommandLineBuilder.quote(properties.expand(p.value))); + if (!p.key.startsWith("java.")) + cmd.addArg(p.key, properties.expand(p.value)); } } else if (properties.size() > 0) @@ -658,17 +661,17 @@ else if (properties.size() > 0) { properties.store(out, "start.jar properties"); } - cmd.addRawArg(propPath.toAbsolutePath().toString()); + cmd.addArg(propPath.toAbsolutePath().toString()); } for (Path xml : jettyEnvironment.getXmlFiles()) { - cmd.addRawArg(xml.toAbsolutePath().toString()); + cmd.addArg(xml.toAbsolutePath().toString()); } for (Path propertyFile : jettyEnvironment.getPropertyFiles()) { - cmd.addRawArg(propertyFile.toAbsolutePath().toString()); + cmd.addArg(propertyFile.toAbsolutePath().toString()); } } @@ -693,7 +696,7 @@ else if (properties.size() > 0) // TODO module path for (Prop property : environment.getProperties()) - cmd.addArg(property.key + "=" + property.value); + cmd.addArg(property.key, property.value); for (Path xmlFile : environment.getXmlFiles()) cmd.addArg(xmlFile.toAbsolutePath().toString()); @@ -773,28 +776,28 @@ private void generateJpmsArgs(CommandLineBuilder cmd) { if (!_jmodAdds.isEmpty()) { - cmd.addRawArg("--add-modules"); - cmd.addRawArg(String.join(",", _jmodAdds)); + cmd.addArg("--add-modules"); + cmd.addArg(String.join(",", _jmodAdds)); } for (Map.Entry> entry : _jmodPatch.entrySet()) { - cmd.addRawArg("--patch-module"); - cmd.addRawArg(entry.getKey() + "=" + String.join(FS.pathSeparator(), entry.getValue())); + cmd.addArg("--patch-module"); + cmd.addArg(entry.getKey(), String.join(FS.pathSeparator(), entry.getValue())); } for (Map.Entry> entry : _jmodOpens.entrySet()) { - cmd.addRawArg("--add-opens"); - cmd.addRawArg(entry.getKey() + "=" + String.join(",", entry.getValue())); + cmd.addArg("--add-opens"); + cmd.addArg(entry.getKey(), String.join(",", entry.getValue())); } for (Map.Entry> entry : _jmodExports.entrySet()) { - cmd.addRawArg("--add-exports"); - cmd.addRawArg(entry.getKey() + "=" + String.join(",", entry.getValue())); + cmd.addArg("--add-exports"); + cmd.addArg(entry.getKey(), String.join(",", entry.getValue())); } for (Map.Entry> entry : _jmodReads.entrySet()) { - cmd.addRawArg("--add-reads"); - cmd.addRawArg(entry.getKey() + "=" + String.join(",", entry.getValue())); + cmd.addArg("--add-reads"); + cmd.addArg(entry.getKey(), String.join(",", entry.getValue())); } } @@ -1154,9 +1157,14 @@ public StartEnvironment parse(StartEnvironment environment, String arg, String s int colon = arg.indexOf('='); for (String part : arg.substring(colon + 1).split(",")) { + if ("multiline".equalsIgnoreCase(part)) + { + multiLine = true; + continue; + } + if (!ALL_PARTS.contains(part)) throw new UsageException(UsageException.ERR_BAD_ARG, "Unrecognized --dry-run=\"%s\" in %s", part, source); - dryRunParts.add(part); } dryRun = true; @@ -1464,11 +1472,6 @@ public void setProperty(StartEnvironment environment, String key, String value, JavaVersion ver = JavaVersion.parse(value); properties.setProperty("java.version.platform", Integer.toString(ver.getPlatform()), source); - // @deprecated - below will be removed in Jetty 10.x - properties.setProperty("java.version.major", Integer.toString(ver.getMajor()), "Deprecated"); - properties.setProperty("java.version.minor", Integer.toString(ver.getMinor()), "Deprecated"); - properties.setProperty("java.version.micro", Integer.toString(ver.getMicro()), "Deprecated"); - // ALPN feature exists properties.setProperty("runtime.feature.alpn", Boolean.toString(isMethodAvailable(javax.net.ssl.SSLParameters.class, "getApplicationProtocols", null)), source); } diff --git a/jetty-core/jetty-start/src/main/resources/org/eclipse/jetty/start/usage.txt b/jetty-core/jetty-start/src/main/resources/org/eclipse/jetty/start/usage.txt index 64f8461abbf8..08ca777b23ce 100644 --- a/jetty-core/jetty-start/src/main/resources/org/eclipse/jetty/start/usage.txt +++ b/jetty-core/jetty-start/src/main/resources/org/eclipse/jetty/start/usage.txt @@ -60,18 +60,21 @@ Report Commands: --dry-run Prints the command line that start.jar generates, - then exits. + in a format usable by the unix sh command, then exits. This may be used to generate command lines into scripts: $ java -jar start.jar --dry-run > jetty.sh --dry-run=(,)* - Prints specific parts of the command line. The parts are: + Prints specific parts of the command line in a format usable by + the unix sh command. The parts are: o "java" - the JVM to run o "opts" - the JVM options (e.g. -D, -X and -XX flags) o "path" - the JVM class-path and/or the JPMS module-path o "main" - the main class to run o "args" - the arguments passed to the main class o "envs" - the generated XML files to create the environments + o "multiline" - a fake part that causes the command to be + generated over multiple lines Configure Commands: ------------------- diff --git a/jetty-core/jetty-start/src/test/java/org/eclipse/jetty/start/CommandLineBuilderTest.java b/jetty-core/jetty-start/src/test/java/org/eclipse/jetty/start/CommandLineBuilderTest.java index 06753229e7e9..0525e094dd28 100644 --- a/jetty-core/jetty-start/src/test/java/org/eclipse/jetty/start/CommandLineBuilderTest.java +++ b/jetty-core/jetty-start/src/test/java/org/eclipse/jetty/start/CommandLineBuilderTest.java @@ -13,7 +13,12 @@ package org.eclipse.jetty.start; +import java.util.stream.Stream; + import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; @@ -23,43 +28,88 @@ public class CommandLineBuilderTest @Test public void testSimpleCommandline() { - CommandLineBuilder cmd = new CommandLineBuilder("java"); - cmd.addEqualsArg("-Djava.io.tmpdir", "/home/java/temp dir/"); + CommandLineBuilder cmd = new CommandLineBuilder(); + cmd.addArg("java"); + cmd.addArg("-D", "java.io.tmpdir", "/home/java/temp dir/"); cmd.addArg("--version"); - assertThat(cmd.toQuotedString(), is("java '-Djava.io.tmpdir=/home/java/temp dir/' --version")); + assertThat(cmd.toCommandLine(), is("java -Djava.io.tmpdir=\"/home/java/temp dir/\" --version")); } @Test public void testSimpleHomeNoSpace() { - CommandLineBuilder cmd = new CommandLineBuilder("java"); - cmd.addEqualsArg("-Djetty.home", "/opt/jetty"); - assertThat(cmd.toQuotedString(), is("java -Djetty.home=/opt/jetty")); + CommandLineBuilder cmd = new CommandLineBuilder(); + cmd.addArg("java"); + cmd.addArg("-Djetty.home", "/opt/jetty"); + assertThat(cmd.toCommandLine(), is("java -Djetty.home=/opt/jetty")); } @Test public void testSimpleHomeWithSpace() { - CommandLineBuilder cmd = new CommandLineBuilder("java"); - cmd.addEqualsArg("-Djetty.home", "/opt/jetty 10/home"); - assertThat(cmd.toQuotedString(), is("java '-Djetty.home=/opt/jetty 10/home'")); + CommandLineBuilder cmd = new CommandLineBuilder(); + cmd.addArg("java"); + cmd.addArg("-Djetty.home", "/opt/jetty 10/home"); + assertThat(cmd.toCommandLine(), is("java -Djetty.home=\"/opt/jetty 10/home\"")); } @Test public void testEscapedFormattingString() { - CommandLineBuilder cmd = new CommandLineBuilder("java"); - cmd.addEqualsArg("-Djetty.home", "/opt/jetty"); - cmd.addEqualsArg("jetty.requestlog.formatter", "%{client}a - %u %{dd/MMM/yyyy:HH:mm:ss ZZZ|GMT}t \"%r\" %s %O \"%{Referer}i\" \"%{User-Agent}i\""); - assertThat(cmd.toQuotedString(), is("java -Djetty.home=/opt/jetty 'jetty.requestlog.formatter=%{client}a - %u %{dd/MMM/yyyy:HH:mm:ss ZZZ|GMT}t \"%r\" %s %O \"%{Referer}i\" \"%{User-Agent}i\"'")); + CommandLineBuilder cmd = new CommandLineBuilder(); + cmd.addArg("java"); + cmd.addArg("-Djetty.home", "/opt/jetty"); + cmd.addArg("jetty.requestlog.formatter", "%{client}a - %u %{dd/MMM/yyyy:HH:mm:ss ZZZ|GMT}t \"%r\" %s %O \"%{Referer}i\" \"%{User-Agent}i\""); + assertThat(cmd.toCommandLine(), is("java -Djetty.home=/opt/jetty jetty.requestlog.formatter=\"%{client}a - %u %{dd/MMM/yyyy:HH:mm:ss ZZZ|GMT}t \\\"%r\\\" %s %O \\\"%{Referer}i\\\" \\\"%{User-Agent}i\\\"\"")); } @Test public void testEscapeUnicode() { - CommandLineBuilder cmd = new CommandLineBuilder("java"); - cmd.addEqualsArg("-Djetty.home", "/opt/jetty"); - cmd.addEqualsArg("monetary.symbol", "€"); - assertThat(cmd.toQuotedString(), is("java -Djetty.home=/opt/jetty monetary.symbol=€")); + CommandLineBuilder cmd = new CommandLineBuilder(); + cmd.addArg("java"); + cmd.addArg("-Djetty.home", "/opt/jetty"); + cmd.addArg("monetary.symbol", "€"); + assertThat(cmd.toCommandLine(), is("java -Djetty.home=/opt/jetty monetary.symbol=\"€\"")); + } + + public static Stream shellQuoting() + { + return Stream.of( + Arguments.of(null, null), + Arguments.of("", "\"\""), + Arguments.of("Hello", "Hello"), + Arguments.of("Hell0", "Hell0"), + Arguments.of("Hello$World", "\"Hello\\$World\""), + Arguments.of("Hello\\World", "\"Hello\\\\World\""), + Arguments.of("Hello`World", "\"Hello\\`World\""), + Arguments.of("\"Hello World\"", "\"\\\"Hello World\\\"\""), + Arguments.of("H-llo_world", "H-llo_world"), + Arguments.of("H:llo/world", "H:llo/world"), + Arguments.of("Hello World", "\"Hello World\""), + Arguments.of("foo\\bar", "\"foo\\\\bar\""), + Arguments.of("'single'", "\"'single'\"") + ); + } + + @ParameterizedTest + @MethodSource("shellQuoting") + public void testShellQuoting(String string, String expected) + { + assertThat(CommandLineBuilder.shellQuoteIfNeeded(string), is(expected)); + } + + @Test + public void testMultiLine() + { + CommandLineBuilder cmd = new CommandLineBuilder(true); + cmd.addArg("java"); + cmd.addArg("-Djetty.home", "/opt/jetty"); + cmd.addArg("monetary.symbol", "€"); + assertThat(cmd.toCommandLine(), + is(""" + java \\ + -Djetty.home=/opt/jetty \\ + monetary.symbol="€\"""")); } -} +} \ No newline at end of file diff --git a/jetty-core/jetty-start/src/test/java/org/eclipse/jetty/start/MainTest.java b/jetty-core/jetty-start/src/test/java/org/eclipse/jetty/start/MainTest.java index 036668f152f5..98051e391eb8 100644 --- a/jetty-core/jetty-start/src/test/java/org/eclipse/jetty/start/MainTest.java +++ b/jetty-core/jetty-start/src/test/java/org/eclipse/jetty/start/MainTest.java @@ -177,7 +177,7 @@ public void testJvmArgExpansion() throws Exception assertThat("jetty.base", baseHome.getBase(), is(homePath.toString())); CommandLineBuilder commandLineBuilder = args.getMainArgs(StartArgs.ALL_PARTS); - String commandLine = commandLineBuilder.toString("\n"); + String commandLine = commandLineBuilder.toString(); String expectedExpansion = String.format("-Xloggc:%s/logs/gc-%s.log", baseHome.getBase(), System.getProperty("java.version") );