diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/launch/DefaultLauncher.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/launch/DefaultLauncher.java index 56e5bb059..6653e51f8 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/launch/DefaultLauncher.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/launch/DefaultLauncher.java @@ -430,7 +430,7 @@ public class DefaultLauncher extends Launcher { // To guarantee that when failed to generate launch command line, we will not call pre-launch command List rawCommandLine = command.commandLine.asList(); if (StringUtils.isNotBlank(options.getWrapper())) { - rawCommandLine.addAll(0, StringUtils.parseCommand(options.getWrapper(), getEnvVars())); + rawCommandLine.addAll(0, StringUtils.tokenize(options.getWrapper(), getEnvVars())); } if (command.tempNativeFolder != null) { @@ -452,7 +452,7 @@ public class DefaultLauncher extends Launcher { File runDirectory = repository.getRunDirectory(version.getId()); if (StringUtils.isNotBlank(options.getPreLaunchCommand())) { - ProcessBuilder builder = new ProcessBuilder(StringUtils.parseCommand(options.getPreLaunchCommand(), getEnvVars())).directory(runDirectory); + ProcessBuilder builder = new ProcessBuilder(StringUtils.tokenize(options.getPreLaunchCommand(), getEnvVars())).directory(runDirectory); builder.environment().putAll(getEnvVars()); SystemUtils.callExternalProcess(builder); } @@ -683,7 +683,7 @@ public class DefaultLauncher extends Launcher { if (StringUtils.isNotBlank(options.getPostExitCommand())) { try { - ProcessBuilder builder = new ProcessBuilder(StringUtils.parseCommand(options.getPostExitCommand(), getEnvVars())).directory(options.getGameDir()); + ProcessBuilder builder = new ProcessBuilder(StringUtils.tokenize(options.getPostExitCommand(), getEnvVars())).directory(options.getGameDir()); builder.environment().putAll(getEnvVars()); SystemUtils.callExternalProcess(builder); } catch (Throwable e) { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java index 298a41849..06a1f8ab7 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java @@ -207,93 +207,135 @@ public final class StringUtils { return false; } - public static List tokenize(String str) { - if (isBlank(str)) { - return new ArrayList<>(); - } else { - // Split the string with ' and space cleverly. - ArrayList parts = new ArrayList<>(); - - boolean hasValue = false; - StringBuilder current = new StringBuilder(str.length()); - for (int i = 0; i < str.length(); ) { - char c = str.charAt(i); - if (c == '\'') { - hasValue = true; - int end = str.indexOf(c, i + 1); - if (end < 0) { - end = str.length(); - } - current.append(str, i + 1, end); - i = end + 1; - - } else if (c == '"') { - hasValue = true; - i++; - while (i < str.length()) { - c = str.charAt(i++); - if (c == '"') { - break; - } else if (c == '\\' && i < str.length()) { - c = str.charAt(i++); - switch (c) { - case 'n': - c = '\n'; - break; - case 'r': - c = '\r'; - break; - case 't': - c = '\t'; - break; - case 'v': - c = '\u000b'; - break; - case 'a': - c = '\u0007'; - break; - } - current.append(c); - } else { - current.append(c); - } - } - } else if (c == ' ') { - if (hasValue) { - parts.add(current.toString()); - current.setLength(0); - hasValue = false; - } - i++; - } else { - hasValue = true; - current.append(c); - i++; - } - } - if (hasValue) { - parts.add(current.toString()); - } - - return parts; - } + private static boolean isVarNameStart(char ch) { + return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch == '_'; } - public static List parseCommand(String command, Map env) { - StringBuilder stringBuilder = new StringBuilder(command); - for (Map.Entry entry : env.entrySet()) { - String key = "$" + entry.getKey(); - int i = 0; - while (true) { - i = stringBuilder.indexOf(key, i); - if (i == -1) { + private static boolean isVarNamePart(char ch) { + return isVarNameStart(ch) || (ch >= '0' && ch <= '9'); + } + + private static int findVarEnd(String str, int offset) { + if (offset < str.length() - 1 && isVarNameStart(str.charAt(offset))) { + int end = offset + 1; + while (end < str.length()) { + if (!isVarNamePart(str.charAt(end))) { break; } - stringBuilder.replace(i, i + key.length(), entry.getValue()); + end++; } + return end; } - return tokenize(stringBuilder.toString()); + return -1; + } + + public static List tokenize(String str) { + return tokenize(str, null); + } + + public static List tokenize(String str, Map vars) { + if (isBlank(str)) { + return new ArrayList<>(); + } + + if (vars == null) { + vars = Collections.emptyMap(); + } + + // Split the string with ' and space cleverly. + ArrayList parts = new ArrayList<>(); + int varEnd; + + boolean hasValue = false; + StringBuilder current = new StringBuilder(str.length()); + for (int i = 0; i < str.length(); ) { + char c = str.charAt(i); + if (c == '\'') { + hasValue = true; + int end = str.indexOf(c, i + 1); + if (end < 0) { + end = str.length(); + } + current.append(str, i + 1, end); + i = end + 1; + + } else if (c == '"') { + hasValue = true; + i++; + while (i < str.length()) { + c = str.charAt(i++); + if (c == '"') { + break; + } else if (c == '`' && i < str.length()) { + c = str.charAt(i++); + switch (c) { + case 'a': + c = '\u0007'; + break; + case 'b': + c = '\b'; + break; + case 'f': + c = '\f'; + break; + case 'n': + c = '\n'; + break; + case 'r': + c = '\r'; + break; + case 't': + c = '\t'; + break; + case 'v': + c = '\u000b'; + break; + } + current.append(c); + } else if (c == '$' && (varEnd = findVarEnd(str, i)) >= 0) { + String key = str.substring(i, varEnd); + String value = vars.get(key); + if (value != null) { + current.append(value); + } else { + current.append('$').append(key); + } + + i = varEnd; + } else { + current.append(c); + } + } + } else if (c == ' ') { + if (hasValue) { + parts.add(current.toString()); + current.setLength(0); + hasValue = false; + } + i++; + } else if (c == '$' && (varEnd = findVarEnd(str, i + 1)) >= 0) { + hasValue = true; + String key = str.substring(i + 1, varEnd); + String value = vars.get(key); + if (value != null) { + current.append(value); + } else { + current.append('$').append(key); + } + + i = varEnd; + } else { + hasValue = true; + current.append(c); + i++; + } + } + if (hasValue) { + parts.add(current.toString()); + } + + return parts; } public static String parseColorEscapes(String original) { diff --git a/HMCLCore/src/test/java/org/jackhuang/hmcl/util/TokenizerTest.java b/HMCLCore/src/test/java/org/jackhuang/hmcl/util/TokenizerTest.java index 8516a85b9..432a610f1 100644 --- a/HMCLCore/src/test/java/org/jackhuang/hmcl/util/TokenizerTest.java +++ b/HMCLCore/src/test/java/org/jackhuang/hmcl/util/TokenizerTest.java @@ -1,40 +1,61 @@ package org.jackhuang.hmcl.util; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; -public class TokenizerTest { - private void test(String source, String... expected) { - Assertions.assertEquals(Arrays.asList(expected), StringUtils.tokenize(source)); - } +import static org.junit.jupiter.api.Assertions.assertEquals; + +public final class TokenizerTest { @Test public void textTokenizer() { - test( - "\"C:/Program Files/Bellsoft/JDK-11/bin.java.exe\" -version \"a.b.c\" something else", - "C:/Program Files/Bellsoft/JDK-11/bin.java.exe", "-version", "a.b.c", "something", "else" + assertEquals( + Arrays.asList("C:/Program Files/Bellsoft/JDK-11/bin.java.exe", "-version", "a.b.c", "something", "else"), + StringUtils.tokenize("\"C:/Program Files/Bellsoft/JDK-11/bin.java.exe\" -version \"a.b.c\" something else") ); - test( - "\"Another\"Text something else", - "AnotherText", "something", "else" + assertEquals( + Arrays.asList("AnotherText", "something", "else"), + StringUtils.tokenize("\"Another\"Text something else") ); - test( - "Text without quote", - "Text", "without", "quote" + assertEquals( + Arrays.asList("Text", "without", "quote"), + StringUtils.tokenize("Text without quote") ); - test( - "Text with multiple spaces", - "Text", "with", "multiple", "spaces" + assertEquals( + Arrays.asList("Text", "with", "multiple", "spaces"), + StringUtils.tokenize("Text with multiple spaces") ); - test( - "Text with empty part ''", - "Text", "with", "empty", "part", "" + assertEquals( + Arrays.asList("Text", "with", "empty", "part", ""), + StringUtils.tokenize("Text with empty part ''") ); - test( - "head\"abc\\n\\\\\\\"\"end", - "headabc\n\\\"end" + assertEquals( + Arrays.asList("headabc\n`\"$end"), + StringUtils.tokenize("head\"abc`n```\"\"$end") + ); + + String instName = "1.20.4"; + String instDir = "C:\\Program Files (x86)\\Minecraft\\"; + + Map env = new HashMap<>(); + env.put("INST_NAME", instName); + env.put("INST_DIR", instDir); + env.put("EMPTY", ""); + + assertEquals( + Arrays.asList("cd", instDir), + StringUtils.tokenize("cd $INST_DIR", env) + ); + assertEquals( + Arrays.asList("Text", "with", "empty", "part", ""), + StringUtils.tokenize("Text with empty part $EMPTY", env) + ); + assertEquals( + Arrays.asList("head", "1.20.4", "$UNKNOWN", instDir, "", instDir + instName + "$UNKNOWN" + instDir + "$INST_DIR\n$UNKNOWN $$"), + StringUtils.tokenize("head $INST_NAME $UNKNOWN $INST_DIR $EMPTY $INST_DIR$INST_NAME$UNKNOWN\"$INST_DIR`$INST_DIR`n$UNKNOWN $EMPTY$\"$", env) ); } }