Skip to content

Commit d1dc6f1

Browse files
committed
[gh-11363] Fix pipe symbol parsing in .mvn/jvm.config
The concat_lines function in the Unix mvn script was using xargs -n 1 which split arguments by whitespace, causing pipe symbols (|) in JVM arguments to be interpreted as shell command separators. This fix replaces the problematic implementation with a line-by-line reader that preserves quoted arguments and special characters while maintaining all existing functionality: - Comments are still removed - Empty lines are still filtered out - Variable substitution (MAVEN_PROJECTBASEDIR) still works - Pipe symbols and other special characters are preserved The Windows mvn.cmd seems too limited, so we use a custom java class to parse the jvm.config file and process it. Added integration test to verify pipe symbols in jvm.config work correctly and don't cause shell parsing errors.
1 parent 304791e commit d1dc6f1

File tree

7 files changed

+293
-51
lines changed

7 files changed

+293
-51
lines changed

apache-maven/src/assembly/component.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ under the License.
6868
<includes>
6969
<include>*.cmd</include>
7070
<include>*.conf</include>
71+
<include>*.java</include>
7172
</includes>
7273
<lineEnding>dos</lineEnding>
7374
</fileSet>
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
import java.io.IOException;
21+
import java.nio.charset.StandardCharsets;
22+
import java.nio.file.Files;
23+
import java.nio.file.Path;
24+
import java.nio.file.Paths;
25+
import java.util.ArrayList;
26+
import java.util.List;
27+
import java.util.stream.Stream;
28+
29+
/**
30+
* Parses .mvn/jvm.config file for Windows batch scripts.
31+
* This avoids the complexity of parsing special characters (pipes, quotes, etc.) in batch scripts.
32+
*
33+
* Usage: java JvmConfigParser.java <jvm.config-path> <maven-project-basedir>
34+
*
35+
* Outputs: Single line with space-separated quoted arguments (safe for batch scripts)
36+
*/
37+
public class JvmConfigParser {
38+
public static void main(String[] args) {
39+
if (args.length != 2) {
40+
System.err.println("Usage: java JvmConfigParser.java <jvm.config-path> <maven-project-basedir>");
41+
System.exit(1);
42+
}
43+
44+
Path jvmConfigPath = Paths.get(args[0]);
45+
String mavenProjectBasedir = args[1];
46+
47+
if (!Files.exists(jvmConfigPath)) {
48+
// No jvm.config file - output nothing
49+
return;
50+
}
51+
52+
try (Stream<String> lines = Files.lines(jvmConfigPath, StandardCharsets.UTF_8)) {
53+
StringBuilder result = new StringBuilder();
54+
55+
lines.forEach(line -> {
56+
// Remove comments
57+
int commentIndex = line.indexOf('#');
58+
if (commentIndex >= 0) {
59+
line = line.substring(0, commentIndex);
60+
}
61+
62+
// Trim whitespace
63+
line = line.trim();
64+
65+
// Skip empty lines
66+
if (line.isEmpty()) {
67+
return;
68+
}
69+
70+
// Replace MAVEN_PROJECTBASEDIR placeholders
71+
line = line.replace("${MAVEN_PROJECTBASEDIR}", mavenProjectBasedir);
72+
line = line.replace("$MAVEN_PROJECTBASEDIR", mavenProjectBasedir);
73+
74+
// Parse line into individual arguments (split on spaces, respecting quotes)
75+
List<String> parsed = parseArguments(line);
76+
77+
// Append each argument quoted
78+
for (String arg : parsed) {
79+
if (result.length() > 0) {
80+
result.append(' ');
81+
}
82+
result.append('"').append(arg).append('"');
83+
}
84+
});
85+
86+
System.out.print(result.toString());
87+
} catch (IOException e) {
88+
System.err.println("Error reading jvm.config: " + e.getMessage());
89+
System.exit(1);
90+
}
91+
}
92+
93+
/**
94+
* Parse a line into individual arguments, respecting quoted strings.
95+
* Quotes are stripped from the arguments.
96+
*/
97+
private static List<String> parseArguments(String line) {
98+
List<String> args = new ArrayList<>();
99+
StringBuilder current = new StringBuilder();
100+
boolean inQuotes = false;
101+
boolean inSingleQuotes = false;
102+
103+
for (int i = 0; i < line.length(); i++) {
104+
char c = line.charAt(i);
105+
106+
if (c == '"' && !inSingleQuotes) {
107+
inQuotes = !inQuotes;
108+
// Don't include the quote character itself
109+
} else if (c == '\'' && !inQuotes) {
110+
inSingleQuotes = !inSingleQuotes;
111+
// Don't include the quote character itself
112+
} else if (c == ' ' && !inQuotes && !inSingleQuotes) {
113+
// Space outside quotes - end of argument
114+
if (current.length() > 0) {
115+
args.add(current.toString());
116+
current.setLength(0);
117+
}
118+
} else {
119+
current.append(c);
120+
}
121+
}
122+
123+
// Add last argument
124+
if (current.length() > 0) {
125+
args.add(current.toString());
126+
}
127+
128+
return args;
129+
}
130+
}
131+

apache-maven/src/assembly/maven/bin/mvn

Lines changed: 38 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -168,28 +168,48 @@ find_file_argument_basedir() {
168168
# concatenates all lines of a file and replaces variables
169169
concat_lines() {
170170
if [ -f "$1" ]; then
171-
# First convert all CR to LF using tr
171+
# Convert CR to LF, remove empty lines and comments, perform variable substitution,
172+
# and escape pipe symbols for eval
172173
tr '\r' '\n' < "$1" | \
173174
sed -e '/^$/d' -e 's/#.*$//' | \
174-
# Replace LF with NUL for xargs
175-
tr '\n' '\0' | \
176-
# Split into words and process each argument
177-
# Use -0 with NUL to avoid special behaviour on quotes
178-
xargs -n 1 -0 | \
179-
while read -r arg; do
180-
# Replace variables first
181-
arg=$(echo "$arg" | sed \
182-
-e "s@\${MAVEN_PROJECTBASEDIR}@$MAVEN_PROJECTBASEDIR@g" \
183-
-e "s@\$MAVEN_PROJECTBASEDIR@$MAVEN_PROJECTBASEDIR@g")
184-
185-
echo "$arg"
186-
done | \
187-
tr '\n' ' '
175+
sed \
176+
-e "s@\${MAVEN_PROJECTBASEDIR}@$MAVEN_PROJECTBASEDIR@g" \
177+
-e "s@\$MAVEN_PROJECTBASEDIR@$MAVEN_PROJECTBASEDIR@g" | \
178+
awk '
179+
{
180+
result = ""
181+
in_quotes = 0
182+
for (i = 1; i <= length($0); i++) {
183+
char = substr($0, i, 1)
184+
if (char == "\"") {
185+
in_quotes = !in_quotes
186+
result = result char
187+
} else if (char == "|" && !in_quotes) {
188+
# Escape unquoted pipes for eval
189+
result = result "\\|"
190+
} else {
191+
result = result char
192+
}
193+
}
194+
# Accumulate lines with space separator
195+
if (NR > 1) printf " "
196+
printf "%s", result
197+
}
198+
END { if (NR > 0) print "" }
199+
'
188200
fi
189201
}
190202

191203
MAVEN_PROJECTBASEDIR="`find_maven_basedir "$@"`"
192-
MAVEN_OPTS="$MAVEN_OPTS `concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config"`"
204+
# Read JVM config and append to MAVEN_OPTS, preserving special characters
205+
_jvm_config="`concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config"`"
206+
if [ -n "$_jvm_config" ]; then
207+
if [ -n "$MAVEN_OPTS" ]; then
208+
MAVEN_OPTS="$MAVEN_OPTS $_jvm_config"
209+
else
210+
MAVEN_OPTS="$_jvm_config"
211+
fi
212+
fi
193213
LAUNCHER_JAR=`echo "$MAVEN_HOME"/boot/plexus-classworlds-*.jar`
194214
LAUNCHER_CLASS=org.codehaus.plexus.classworlds.launcher.Launcher
195215

@@ -239,6 +259,7 @@ handle_args() {
239259
handle_args "$@"
240260
MAVEN_MAIN_CLASS=${MAVEN_MAIN_CLASS:=org.apache.maven.cling.MavenCling}
241261

262+
# Build command string for eval
242263
cmd="\"$JAVACMD\" \
243264
$MAVEN_OPTS \
244265
$MAVEN_DEBUG_OPTS \
@@ -251,13 +272,10 @@ cmd="\"$JAVACMD\" \
251272
\"-Dmaven.multiModuleProjectDirectory=$MAVEN_PROJECTBASEDIR\" \
252273
$LAUNCHER_CLASS \
253274
$MAVEN_ARGS"
275+
254276
# Add remaining arguments with proper quoting
255277
for arg in "$@"; do
256278
cmd="$cmd \"$arg\""
257279
done
258280

259-
# Debug: print the command that will be executed
260-
#echo "About to execute:"
261-
#echo "$cmd"
262-
263281
eval exec "$cmd"

apache-maven/src/assembly/maven/bin/mvn.cmd

Lines changed: 8 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -179,36 +179,13 @@ cd /d "%EXEC_DIR%"
179179

180180
if not exist "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadJvmConfig
181181

182-
@setlocal EnableExtensions EnableDelayedExpansion
183-
set JVM_CONFIG_MAVEN_OPTS=
184-
for /F "usebackq tokens=* delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do (
185-
set "line=%%a"
186-
187-
rem Skip empty lines and full-line comments
188-
echo !line! | findstr /b /r /c:"[ ]*#" >nul
189-
if errorlevel 1 (
190-
rem Handle end-of-line comments by taking everything before #
191-
for /f "tokens=1* delims=#" %%i in ("!line!") do set "line=%%i"
192-
193-
rem Trim leading/trailing spaces while preserving spaces in quotes
194-
set "trimmed=!line!"
195-
for /f "tokens=* delims= " %%i in ("!trimmed!") do set "trimmed=%%i"
196-
for /l %%i in (1,1,100) do if "!trimmed:~-1!"==" " set "trimmed=!trimmed:~0,-1!"
197-
198-
rem Replace MAVEN_PROJECTBASEDIR placeholders
199-
set "trimmed=!trimmed:${MAVEN_PROJECTBASEDIR}=%MAVEN_PROJECTBASEDIR%!"
200-
set "trimmed=!trimmed:$MAVEN_PROJECTBASEDIR=%MAVEN_PROJECTBASEDIR%!"
201-
202-
if not "!trimmed!"=="" (
203-
if "!JVM_CONFIG_MAVEN_OPTS!"=="" (
204-
set "JVM_CONFIG_MAVEN_OPTS=!trimmed!"
205-
) else (
206-
set "JVM_CONFIG_MAVEN_OPTS=!JVM_CONFIG_MAVEN_OPTS! !trimmed!"
207-
)
208-
)
209-
)
210-
)
211-
@endlocal & set JVM_CONFIG_MAVEN_OPTS=%JVM_CONFIG_MAVEN_OPTS%
182+
rem Use Java to parse jvm.config to avoid batch script parsing issues with special characters
183+
rem This handles pipes, quotes, and other special characters correctly
184+
set "JVM_CONFIG_TEMP=%TEMP%\mvn-jvm-config-%RANDOM%.txt"
185+
"%JAVACMD%" "%MAVEN_HOME%\bin\JvmConfigParser.java" "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" "%MAVEN_PROJECTBASEDIR%" > "%JVM_CONFIG_TEMP%" 2>nul
186+
rem Read the single line from temp file
187+
set /p JVM_CONFIG_MAVEN_OPTS=<"%JVM_CONFIG_TEMP%"
188+
del "%JVM_CONFIG_TEMP%" 2>nul
212189

213190
:endReadJvmConfig
214191

@@ -286,4 +263,4 @@ if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd"
286263
@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
287264
if "%MAVEN_BATCH_PAUSE%"=="on" pause
288265

289-
exit /b %ERROR_CODE%
266+
exit /b %ERROR_CODE%
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.maven.it;
20+
21+
import java.nio.file.Path;
22+
import java.util.Properties;
23+
24+
import org.junit.jupiter.api.Test;
25+
26+
import static org.junit.jupiter.api.Assertions.assertEquals;
27+
28+
/**
29+
* This is a test set for <a href="https://github.com/apache/maven/issues/11363">gh-11363</a>:
30+
* Verify that pipe symbols in .mvn/jvm.config are properly handled and don't cause shell command parsing errors.
31+
*/
32+
public class MavenITgh11363PipeSymbolsInJvmConfigTest extends AbstractMavenIntegrationTestCase {
33+
34+
/**
35+
* Verify that pipe symbols in .mvn/jvm.config are properly handled
36+
*/
37+
@Test
38+
void testPipeSymbolsInJvmConfig() throws Exception {
39+
Path basedir = extractResources("/gh-11363-pipe-symbols-jvm-config")
40+
.getAbsoluteFile()
41+
.toPath();
42+
43+
Verifier verifier = newVerifier(basedir.toString());
44+
verifier.setForkJvm(true); // Use forked JVM to test .mvn/jvm.config processing
45+
verifier.addCliArguments("validate");
46+
verifier.execute();
47+
verifier.verifyErrorFreeLog();
48+
49+
Properties props = verifier.loadProperties("target/pom.properties");
50+
assertEquals("de|*.de|my.company.mirror.de", props.getProperty("project.properties.pom.prop.nonProxyHosts"));
51+
assertEquals("value|with|pipes", props.getProperty("project.properties.pom.prop.with.pipes"));
52+
}
53+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Test for MNG-11363: Maven 4 fails to parse pipe symbols in .mvn/jvm.config
2+
-Dhttp.nonProxyHosts=de|*.de|my.company.mirror.de
3+
-Dprop.with.pipes="value|with|pipes"
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!--
3+
Licensed to the Apache Software Foundation (ASF) under one
4+
or more contributor license agreements. See the NOTICE file
5+
distributed with this work for additional information
6+
regarding copyright ownership. The ASF licenses this file
7+
to you under the Apache License, Version 2.0 (the
8+
"License"); you may not use this file except in compliance
9+
with the License. You may obtain a copy of the License at
10+
11+
http://www.apache.org/licenses/LICENSE-2.0
12+
13+
Unless required by applicable law or agreed to in writing,
14+
software distributed under the License is distributed on an
15+
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16+
KIND, either express or implied. See the License for the
17+
specific language governing permissions and limitations
18+
under the License.
19+
-->
20+
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
21+
<modelVersion>4.0.0</modelVersion>
22+
23+
<groupId>org.apache.maven.its.mng11363</groupId>
24+
<artifactId>test</artifactId>
25+
<version>1.0</version>
26+
27+
<name>Maven Integration Test :: MNG-11363</name>
28+
<description>Verify that JVM args can contain pipe symbols in .mvn/jvm.config.</description>
29+
30+
<properties>
31+
<pom.prop.nonProxyHosts>${http.nonProxyHosts}</pom.prop.nonProxyHosts>
32+
<pom.prop.with.pipes>${prop.with.pipes}</pom.prop.with.pipes>
33+
</properties>
34+
35+
<build>
36+
<plugins>
37+
<plugin>
38+
<groupId>org.apache.maven.its.plugins</groupId>
39+
<artifactId>maven-it-plugin-expression</artifactId>
40+
<version>2.1-SNAPSHOT</version>
41+
<executions>
42+
<execution>
43+
<id>test</id>
44+
<goals>
45+
<goal>eval</goal>
46+
</goals>
47+
<phase>validate</phase>
48+
<configuration>
49+
<outputFile>target/pom.properties</outputFile>
50+
<expressions>
51+
<expression>project/properties</expression>
52+
</expressions>
53+
</configuration>
54+
</execution>
55+
</executions>
56+
</plugin>
57+
</plugins>
58+
</build>
59+
</project>

0 commit comments

Comments
 (0)