Skip to content

Commit fd2b5da

Browse files
committed
fix: handle long command lines for .bat and .cmd
On Windows when running command using Java's `ProcessBuilder` to run .bat or .cmd files we have to ensure that the command line does not exceed the maximum length allowed by the Windows command line. Fixes #2033
1 parent 3222dee commit fd2b5da

File tree

8 files changed

+111
-41
lines changed

8 files changed

+111
-41
lines changed

src/main/java/dev/jbang/cli/App.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ private static void installShellScript(Path file, String scriptRef, List<String>
178178
cmd.add(scriptRef);
179179
cmd.addAll(runArgs);
180180
CommandBuffer cb = CommandBuffer.of(cmd);
181-
List<String> lines = Arrays.asList("#!/bin/sh", cb.asCommandLine(Util.Shell.bash) + " \"$@\"");
181+
List<String> lines = Arrays.asList("#!/bin/sh", cb.shell(Util.Shell.bash).asCommandLine() + " \"$@\"");
182182
Files.write(file, lines, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE);
183183
if (!Util.isWindows()) {
184184
Util.setExecutable(file);
@@ -193,7 +193,7 @@ private static void installCmdScript(Path file, String scriptRef, List<String> r
193193
cmd.add(scriptRef);
194194
cmd.addAll(runArgs);
195195
CommandBuffer cb = CommandBuffer.of(cmd);
196-
List<String> lines = Arrays.asList("@echo off", cb.asCommandLine(Util.Shell.cmd) + " %*");
196+
List<String> lines = Arrays.asList("@echo off", cb.shell(Util.Shell.cmd).asCommandLine() + " %*");
197197
Files.write(file, lines, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE);
198198
}
199199

@@ -205,7 +205,7 @@ private static void installPSScript(Path file, String scriptRef, List<String> ru
205205
cmd.add(scriptRef);
206206
cmd.addAll(runArgs);
207207
CommandBuffer cb = CommandBuffer.of(cmd);
208-
List<String> lines = Collections.singletonList(cb.asCommandLine(Util.Shell.powershell) + " @args");
208+
List<String> lines = Collections.singletonList(cb.shell(Util.Shell.powershell).asCommandLine() + " @args");
209209
Files.write(file, lines, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE);
210210
}
211211

src/main/java/dev/jbang/cli/Edit.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -261,10 +261,10 @@ private boolean openEditor(String projectPathString, List<String> additionalFile
261261

262262
String[] cmd;
263263
if (Util.getShell() == Shell.bash) {
264-
final String editorCommand = CommandBuffer.of(optionList).asCommandLine(Shell.bash);
264+
final String editorCommand = CommandBuffer.of(optionList).shell(Shell.bash).asCommandLine();
265265
cmd = new String[] { "sh", "-c", editorCommand };
266266
} else {
267-
final String editorCommand = CommandBuffer.of(optionList).asCommandLine(Shell.cmd);
267+
final String editorCommand = CommandBuffer.of(optionList).shell(Shell.cmd).asCommandLine();
268268
cmd = new String[] { "cmd", "/c", editorCommand };
269269
}
270270
verboseMsg("Running `" + String.join(" ", cmd) + "`");

src/main/java/dev/jbang/source/buildsteps/CompileBuildStep.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,8 +133,7 @@ private boolean hasModuleInfoFile() {
133133

134134
protected void runCompiler(List<String> optionList) throws IOException {
135135
runCompiler(CommandBuffer.of(optionList)
136-
.applyWindowsMaxLengthLimit(CommandBuffer.MAX_LENGTH_WINPROCBUILDER,
137-
Util.getShell())
136+
.applyWindowsMaxLengthLimit()
138137
.asProcessBuilder()
139138
.inheritIO());
140139
}

src/main/java/dev/jbang/source/buildsteps/NativeBuildStep.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ protected void runNativeBuilder(List<String> optionList) throws IOException {
7272
Util.verboseMsg("native-image: " + String.join(" ", optionList));
7373

7474
ProcessBuilder pb = CommandBuffer.of(optionList)
75-
.applyWindowsMaxLengthLimit(32000, Util.getShell())
75+
.applyWindowsMaxLengthLimit()
7676
.asProcessBuilder()
7777
.inheritIO();
7878

src/main/java/dev/jbang/source/generators/BaseCmdGenerator.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,6 @@ public String generate() throws IOException {
5656

5757
protected String generateCommandLineString(List<String> fullArgs) throws IOException {
5858
CommandBuffer cb = CommandBuffer.of(fullArgs);
59-
return cb.asCommandLine(shell);
59+
return cb.shell(shell).asCommandLine();
6060
}
6161
}

src/main/java/dev/jbang/source/generators/JarCmdGenerator.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -199,8 +199,9 @@ protected List<String> generateCommandLineList() throws IOException {
199199

200200
protected String generateCommandLineString(List<String> fullArgs) throws IOException {
201201
return CommandBuffer.of(fullArgs)
202-
.applyWindowsMaxLengthLimit(CommandBuffer.MAX_LENGTH_WINCLI, shell)
203-
.asCommandLine(shell);
202+
.shell(shell)
203+
.applyWindowsMaxLengthLimit(CommandBuffer.MAX_LENGTH_WINCLI)
204+
.asCommandLine();
204205
}
205206

206207
private static void addPropertyFlags(Map<String, String> properties, String def, List<String> result) {

src/main/java/dev/jbang/util/CommandBuffer.java

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
public class CommandBuffer {
1515
private List<String> arguments;
16+
private Util.Shell shell = Util.getShell();
1617

1718
// 8192 character command line length limit imposed by CMD.EXE
1819
public static final int MAX_LENGTH_WINCLI = 8000;
@@ -52,26 +53,26 @@ public CommandBuffer(String... arguments) {
5253
this.arguments = new ArrayList<>(Arrays.asList(arguments));
5354
}
5455

55-
public ProcessBuilder asProcessBuilder() {
56-
return asProcessBuilder(Util.getShell());
56+
public CommandBuffer shell(Util.Shell shell) {
57+
this.shell = shell;
58+
return this;
5759
}
5860

59-
public ProcessBuilder asProcessBuilder(Util.Shell shell) {
61+
public ProcessBuilder asProcessBuilder() {
6062
List<String> args = arguments.stream()
6163
.map(a -> escapeProcessBuilderArgument(a, shell))
6264
.collect(Collectors.toList());
6365
return new ProcessBuilder(args);
6466
}
6567

6668
public String asCommandLine() {
67-
return asCommandLine(Util.getShell());
68-
}
69-
70-
public String asCommandLine(Util.Shell shell) {
7169
return String.join(" ", escapeShellArguments(arguments, shell));
7270
}
7371

7472
public CommandBuffer usingArgsFile() throws IOException {
73+
if (arguments.size() < 2 || arguments.get(1).startsWith("@")) {
74+
return this;
75+
}
7576
// @-files avoid problems on Windows with very long command lines
7677
final Path argsFile = Files.createTempFile("jbang", ".args");
7778
try (PrintWriter pw = new PrintWriter(argsFile.toFile())) {
@@ -83,10 +84,19 @@ public CommandBuffer usingArgsFile() throws IOException {
8384
return CommandBuffer.of(arguments.get(0), "@" + argsFile);
8485
}
8586

86-
public CommandBuffer applyWindowsMaxLengthLimit(int maxLength, Util.Shell shell) throws IOException {
87-
String args = asCommandLine(shell);
87+
public CommandBuffer applyWindowsMaxLengthLimit() throws IOException {
88+
int maxLength = MAX_LENGTH_WINPROCBUILDER;
89+
String cmd = arguments.get(0).toLowerCase();
90+
if (cmd.endsWith(".bat") || cmd.endsWith(".cmd")) {
91+
maxLength = MAX_LENGTH_WINCLI;
92+
}
93+
return applyWindowsMaxLengthLimit(maxLength);
94+
}
95+
96+
public CommandBuffer applyWindowsMaxLengthLimit(int maxLength) throws IOException {
97+
String args = asCommandLine();
8898
// Check if we can and need to use @-files on Windows
89-
if (args.length() > maxLength && Util.getShell() != Util.Shell.bash) {
99+
if (args.length() > maxLength && shell != Util.Shell.bash) {
90100
return usingArgsFile();
91101
} else {
92102
return this;

src/test/java/dev/jbang/util/TestCommandBuffer.java

Lines changed: 80 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,42 +4,102 @@
44
import static org.hamcrest.MatcherAssert.assertThat;
55
import static org.hamcrest.Matchers.*;
66

7+
import java.io.IOException;
8+
79
import org.junit.jupiter.api.Test;
10+
import org.junit.jupiter.api.condition.EnabledOnOs;
11+
import org.junit.jupiter.api.condition.OS;
812

913
import dev.jbang.BaseTest;
1014

1115
public class TestCommandBuffer extends BaseTest {
1216

1317
@Test
18+
@EnabledOnOs(OS.WINDOWS)
1419
void testRunWinBat() {
15-
if (Util.getOS() == Util.OS.windows) {
16-
String out = Util.runCommand(examplesTestFolder.resolve("echo.bat").toString(), "abc def", "abc;def",
17-
"abc=def", "abc,def");
18-
assertThat(out, containsString("ARG = abc def"));
19-
assertThat(out, containsString("ARG = abc;def"));
20-
assertThat(out, containsString("ARG = abc=def"));
21-
assertThat(out, containsString("ARG = abc,def"));
22-
}
20+
String out = Util.runCommand(examplesTestFolder.resolve("echo.bat").toString(), "abc def", "abc;def",
21+
"abc=def", "abc,def");
22+
assertThat(out, containsString("ARG = abc def"));
23+
assertThat(out, containsString("ARG = abc;def"));
24+
assertThat(out, containsString("ARG = abc=def"));
25+
assertThat(out, containsString("ARG = abc,def"));
2326
}
2427

2528
@Test
29+
@EnabledOnOs(OS.WINDOWS)
2630
void testRunWinBatFromBash() {
27-
if (Util.getOS() == Util.OS.windows) {
28-
environmentVariables.set(JBANG_RUNTIME_SHELL, "bash");
29-
String out = Util.runCommand(examplesTestFolder.resolve("echo.bat").toString(), "abc def", "abc;def",
30-
"abc=def", "abc,def");
31-
assertThat(out, containsString("ARG = abc def"));
32-
assertThat(out, containsString("ARG = abc;def"));
33-
assertThat(out, containsString("ARG = abc=def"));
34-
assertThat(out, containsString("ARG = abc,def"));
35-
}
31+
environmentVariables.set(JBANG_RUNTIME_SHELL, "bash");
32+
String out = Util.runCommand(examplesTestFolder.resolve("echo.bat").toString(), "abc def", "abc;def",
33+
"abc=def", "abc,def");
34+
assertThat(out, containsString("ARG = abc def"));
35+
assertThat(out, containsString("ARG = abc;def"));
36+
assertThat(out, containsString("ARG = abc=def"));
37+
assertThat(out, containsString("ARG = abc,def"));
3638
}
3739

3840
@Test
41+
@EnabledOnOs(OS.WINDOWS)
3942
void testRunWinPS1() {
40-
if (Util.getOS() == Util.OS.windows) {
41-
String out = CommandBuffer.of("abc def", "abc;def").asCommandLine(Util.Shell.powershell);
42-
assertThat(out, equalTo("'abc def' 'abc;def'"));
43+
String out = CommandBuffer.of("abc def", "abc;def").shell(Util.Shell.powershell).asCommandLine();
44+
assertThat(out, equalTo("'abc def' 'abc;def'"));
45+
}
46+
47+
@Test
48+
void testApplyWindowsMaxLengthLimitExe() throws IOException {
49+
ProcessBuilder pb = CommandBuffer.of(argsTooLong("foo.exe"))
50+
.shell(Util.Shell.cmd)
51+
.applyWindowsMaxLengthLimit()
52+
.asProcessBuilder();
53+
assertThat(pb.command().size(), greaterThan(2));
54+
assertThat(pb.command().get(1), not(startsWith("@")));
55+
}
56+
57+
@Test
58+
void testApplyWindowsMaxLengthLimitBat() throws IOException {
59+
ProcessBuilder pb = CommandBuffer.of(argsTooLong("foo.bat"))
60+
.shell(Util.Shell.cmd)
61+
.applyWindowsMaxLengthLimit()
62+
.asProcessBuilder();
63+
assertThat(pb.command().size(), equalTo(2));
64+
assertThat(pb.command().get(1), startsWith("@"));
65+
}
66+
67+
@Test
68+
void testApplyWindowsMaxLengthLimitCmd() throws IOException {
69+
ProcessBuilder pb = CommandBuffer.of(argsTooLong("foo.cmd"))
70+
.shell(Util.Shell.cmd)
71+
.applyWindowsMaxLengthLimit()
72+
.asProcessBuilder();
73+
assertThat(pb.command().size(), equalTo(2));
74+
assertThat(pb.command().get(1), startsWith("@"));
75+
}
76+
77+
@Test
78+
void testUsingArgsFileWith1Arg() throws IOException {
79+
ProcessBuilder pb = CommandBuffer.of("abc").usingArgsFile().asProcessBuilder();
80+
assertThat(pb.command().size(), equalTo(1));
81+
}
82+
83+
@Test
84+
void testUsingArgsFileWith3Args() throws IOException {
85+
ProcessBuilder pb = CommandBuffer.of("abc", "def", "ghi").usingArgsFile().asProcessBuilder();
86+
assertThat(pb.command().size(), equalTo(2));
87+
assertThat(pb.command().get(1), startsWith("@"));
88+
}
89+
90+
@Test
91+
void testUsingArgsFileNoDup() throws IOException {
92+
CommandBuffer cmd = CommandBuffer.of("abc", "def", "ghi").usingArgsFile();
93+
CommandBuffer cmd2 = cmd.usingArgsFile();
94+
assertThat(cmd, is(cmd2));
95+
}
96+
97+
private String[] argsTooLong(String cmd) {
98+
String[] args = new String[1000];
99+
args[0] = cmd;
100+
for (int i = 1; i < args.length; i++) {
101+
args[i] = "argument " + i;
43102
}
103+
return args;
44104
}
45105
}

0 commit comments

Comments
 (0)