diff --git a/app/src/main/java/io/xpipe/app/prefs/ExternalApplicationHelper.java b/app/src/main/java/io/xpipe/app/prefs/ExternalApplicationHelper.java index 9f12697fd..9a6c4d691 100644 --- a/app/src/main/java/io/xpipe/app/prefs/ExternalApplicationHelper.java +++ b/app/src/main/java/io/xpipe/app/prefs/ExternalApplicationHelper.java @@ -6,11 +6,13 @@ import io.xpipe.app.process.CommandBuilder; import io.xpipe.app.process.CommandSupport; import io.xpipe.app.process.LocalShell; import io.xpipe.app.util.FilePath; +import io.xpipe.app.util.OsType; import java.io.IOException; import java.util.Arrays; import java.util.Locale; -import java.util.stream.Collectors; +import java.util.regex.Matcher; +import java.util.regex.Pattern; public class ExternalApplicationHelper { @@ -30,27 +32,43 @@ public class ExternalApplicationHelper { } raw = raw.strip(); - var split = Arrays.asList(raw.split("\\s+")); - if (split.size() == 0) { + if (raw.isEmpty()) { return; } + var split = Arrays.asList(raw.split("\\s+")); + String exec; String args; - if (raw.startsWith("\"")) { - var end = raw.substring(1).indexOf("\""); + // The executable can be quoted with either double or single quotes + if (raw.startsWith("\"") || raw.startsWith("'")) { + var quote = raw.charAt(0); + var end = raw.indexOf(quote, 1); if (end == -1) { return; } - end++; exec = raw.substring(1, end); args = raw.substring(end + 1).strip(); } else { exec = split.getFirst(); - args = split.stream().skip(1).collect(Collectors.joining(" ")); + // Keep the original spacing, as it might be part of quoted arguments + args = raw.substring(exec.length()).strip(); } - startAsync(CommandBuilder.of().addFile(exec).add(args)); + // On Windows, the arguments are passed as-is to the program, which only understands double quotes + if (OsType.ofLocal() == OsType.WINDOWS) { + args = convertSingleQuotedArguments(args); + } + + startAsync(CommandBuilder.of().addFile(exec).addIf(!args.isEmpty(), args)); + } + + private static final Pattern SINGLE_QUOTED_ARGUMENT = Pattern.compile("'((?:[^']|'')*)'"); + + private static String convertSingleQuotedArguments(String args) { + return SINGLE_QUOTED_ARGUMENT + .matcher(args) + .replaceAll(m -> Matcher.quoteReplacement("\"" + m.group(1).replace("''", "'") + "\"")); } public static void startAsync(CommandBuilder b) throws Exception { diff --git a/app/src/main/java/io/xpipe/app/prefs/ExternalEditorType.java b/app/src/main/java/io/xpipe/app/prefs/ExternalEditorType.java index 41bed7a14..b8322f8b0 100644 --- a/app/src/main/java/io/xpipe/app/prefs/ExternalEditorType.java +++ b/app/src/main/java/io/xpipe/app/prefs/ExternalEditorType.java @@ -502,12 +502,11 @@ public interface ExternalEditorType extends PrefsChoiceValue { var format = customCommand.toLowerCase(Locale.ROOT).contains("$file") ? customCommand : customCommand + " $FILE"; - var command = CommandBuilder.of() - .add(ExternalApplicationHelper.replaceVariableArgument(format, "FILE", file.toString(), true)); + var command = ExternalApplicationHelper.replaceVariableArgument(format, "FILE", file.toString(), true); if (AppPrefs.get().customEditorCommandInTerminal().get()) { TerminalLaunch.builder() .title(file.toString()) - .localScript(sc -> new ShellScript(command.buildFull(sc))) + .localScript(sc -> new ShellScript(command)) .logIfEnabled(false) .preferTabs(false) .pauseOnExit(false) diff --git a/app/src/main/java/io/xpipe/app/rdp/CustomRdpClient.java b/app/src/main/java/io/xpipe/app/rdp/CustomRdpClient.java index 537210c26..4224bd1aa 100644 --- a/app/src/main/java/io/xpipe/app/rdp/CustomRdpClient.java +++ b/app/src/main/java/io/xpipe/app/rdp/CustomRdpClient.java @@ -45,13 +45,12 @@ public class CustomRdpClient implements ExternalApplicationType, ExternalRdpClie } var format = command.toLowerCase(Locale.ROOT).contains("$file") ? command : command + " $FILE"; - ExternalApplicationHelper.startAsync(CommandBuilder.of() - .add(ExternalApplicationHelper.replaceVariableArgument( + ExternalApplicationHelper.startAsync(ExternalApplicationHelper.replaceVariableArgument( format, "FILE", writeRdpConfigFile(configuration.getTitle(), configuration.getConfig()) .toString(), - true))); + true)); } @Override diff --git a/app/src/main/java/io/xpipe/app/spice/CustomSpiceClient.java b/app/src/main/java/io/xpipe/app/spice/CustomSpiceClient.java index 2d071f06d..8bdba539c 100644 --- a/app/src/main/java/io/xpipe/app/spice/CustomSpiceClient.java +++ b/app/src/main/java/io/xpipe/app/spice/CustomSpiceClient.java @@ -45,7 +45,7 @@ public class CustomSpiceClient implements ExternalSpiceClient { var format = command.toLowerCase(Locale.ROOT).contains("$file") ? command : command + " $FILE"; var toExecute = ExternalApplicationHelper.replaceVariableArgument( format, "FILE", configuration.getFile().toString(), true); - ExternalApplicationHelper.startAsync(CommandBuilder.of().add(toExecute)); + ExternalApplicationHelper.startAsync(toExecute); } @Override diff --git a/app/src/main/java/io/xpipe/app/terminal/CustomTerminalType.java b/app/src/main/java/io/xpipe/app/terminal/CustomTerminalType.java index 7519cc2bc..cbd38cc4a 100644 --- a/app/src/main/java/io/xpipe/app/terminal/CustomTerminalType.java +++ b/app/src/main/java/io/xpipe/app/terminal/CustomTerminalType.java @@ -35,18 +35,11 @@ public class CustomTerminalType implements ExternalApplicationType, ExternalTerm } var format = custom.toLowerCase(Locale.ROOT).contains("$cmd") ? custom : custom + " $CMD"; - try (var sc = LocalShell.getShell()) { - var toExecute = ExternalApplicationHelper.replaceVariableArgument( - format, "CMD", configuration.single().getScriptFile().toString(), true); - // We can't be sure whether the command is blocking or not, so always make it not blocking - if (sc.getOsType() == OsType.WINDOWS) { - toExecute = CommandBuilder.of().add("start").addLiteral(configuration.getCleanTitle()).add(toExecute).buildFull(sc); - } else { - var async = sc.getShellDialect().launchAsync(CommandBuilder.of().add(toExecute), true); - toExecute = async.buildFull(sc); - } - sc.command(toExecute).execute(); - } + ExternalApplicationHelper.startAsync(ExternalApplicationHelper.replaceVariableArgument( + format, + "CMD", + configuration.single().getScriptFile().toString(), + true)); } @Override diff --git a/app/src/main/java/io/xpipe/app/vnc/CustomVncClient.java b/app/src/main/java/io/xpipe/app/vnc/CustomVncClient.java index ae3df8a6b..1976d7c22 100644 --- a/app/src/main/java/io/xpipe/app/vnc/CustomVncClient.java +++ b/app/src/main/java/io/xpipe/app/vnc/CustomVncClient.java @@ -45,7 +45,7 @@ public class CustomVncClient implements ExternalVncClient { var address = configuration.getHost() + ":" + configuration.getPort(); var format = command.toLowerCase(Locale.ROOT).contains("$address") ? command : command + " $ADDRESS"; var toExecute = ExternalApplicationHelper.replaceVariableArgument(format, "ADDRESS", address, true); - ExternalApplicationHelper.startAsync(CommandBuilder.of().add(toExecute)); + ExternalApplicationHelper.startAsync(toExecute); } @Override