This is an automated email from the ASF dual-hosted git repository.

gnodet pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/maven.git


The following commit(s) were added to refs/heads/master by this push:
     new da5f27ef2e Fix special characters in .mvn/jvm.config (fix #11363, 
#11485 and #11486) (#11365)
da5f27ef2e is described below

commit da5f27ef2e04893bf7ca707332bf3b7808600d15
Author: Guillaume Nodet <[email protected]>
AuthorDate: Wed Dec 10 17:40:06 2025 +0100

    Fix special characters in .mvn/jvm.config (fix #11363, #11485 and #11486) 
(#11365)
    
    Replace shell-based jvm.config parsing with a Java-based parser to fix 
issues
    with special characters (pipes, @, quotes) that cause shell command errors.
    
    Problems fixed:
    - MNG-11363: Pipe symbols (|) in jvm.config cause shell parsing errors
    - GH-11485: @ character in paths (common in Jenkins workspaces like
      project_PR-350@2) causes sed failures
    - MNG-11486: POSIX compliance issues with xargs -0 on AIX, FreeBSD, etc.
    
    Solution:
    Add JvmConfigParser.java that runs via Java source-launch mode (JDK 11+) to
    parse jvm.config files. This avoids all shell parsing complexities and works
    consistently across Unix and Windows platforms.
    
    Changes:
    - Add apache-maven/bin/JvmConfigParser.java: Java parser that handles quoted
      arguments, comments, line continuations, and ${MAVEN_PROJECTBASEDIR}
      substitution
    - Update mvn (Unix): Use JvmConfigParser instead of tr/sed/xargs pipeline
    - Update mvn.cmd (Windows): Use JvmConfigParser with direct file output to
      avoid Windows file locking issues with shell redirection
    - Add MAVEN_DEBUG_SCRIPT environment variable for debug logging in both
      scripts to aid troubleshooting
    - Add integration tests for pipe symbols and @ character handling
    - Improve Verifier to save stdout/stderr to separate files for debugging
    
    The parser outputs arguments as quoted strings, preserving special 
characters
    that would otherwise be interpreted by the shell.
---
 apache-maven/src/assembly/component.xml            |   1 +
 .../src/assembly/maven/bin/JvmConfigParser.java    | 177 +++++++++++++++++++++
 apache-maven/src/assembly/maven/bin/mvn            |  81 +++++++---
 apache-maven/src/assembly/maven/bin/mvn.cmd        |  84 ++++++----
 .../MavenITgh10937QuotedPipesInMavenOptsTest.java  |   2 +
 ... MavenITgh11363PipeSymbolsInJvmConfigTest.java} |  24 +--
 .../it/MavenITgh11485AtSignInJvmConfigTest.java    |  88 ++++++++++
 .../it/MavenITmng4559SpacesInJvmOptsTest.java      |   2 +
 .../maven/it/MavenITmng6255FixConcatLines.java     |  14 +-
 .../.mvn/jvm.config                                |   3 +
 .../gh-11363-pipe-symbols-jvm-config/pom.xml       |  59 +++++++
 .../resources/gh-11485-at-sign/.mvn/jvm.config     |   3 +
 .../src/test/resources/gh-11485-at-sign/pom.xml    |  70 ++++++++
 .../main/java/org/apache/maven/it/Verifier.java    |  50 +++++-
 14 files changed, 586 insertions(+), 72 deletions(-)

diff --git a/apache-maven/src/assembly/component.xml 
b/apache-maven/src/assembly/component.xml
index 4d75c9a38c..5f55a310c8 100644
--- a/apache-maven/src/assembly/component.xml
+++ b/apache-maven/src/assembly/component.xml
@@ -68,6 +68,7 @@ under the License.
       <includes>
         <include>*.cmd</include>
         <include>*.conf</include>
+        <include>*.java</include>
       </includes>
       <lineEnding>dos</lineEnding>
     </fileSet>
diff --git a/apache-maven/src/assembly/maven/bin/JvmConfigParser.java 
b/apache-maven/src/assembly/maven/bin/JvmConfigParser.java
new file mode 100644
index 0000000000..41b87569dc
--- /dev/null
+++ b/apache-maven/src/assembly/maven/bin/JvmConfigParser.java
@@ -0,0 +1,177 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import java.io.IOException;
+import java.io.Writer;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Parses .mvn/jvm.config file for Windows batch/Unix shell scripts.
+ * This avoids the complexity of parsing special characters (pipes, quotes, 
etc.) in scripts.
+ *
+ * Usage: java JvmConfigParser.java <jvm.config-path> <maven-project-basedir> 
[output-file]
+ *
+ * If output-file is provided, writes result to that file (avoids Windows file 
locking issues).
+ * Otherwise, outputs to stdout.
+ *
+ * Outputs: Single line with space-separated quoted arguments (safe for batch 
scripts)
+ */
+public class JvmConfigParser {
+    public static void main(String[] args) {
+        if (args.length < 2 || args.length > 3) {
+            System.err.println("Usage: java JvmConfigParser.java 
<jvm.config-path> <maven-project-basedir> [output-file]");
+            System.exit(1);
+        }
+
+        Path jvmConfigPath = Paths.get(args[0]);
+        String mavenProjectBasedir = args[1];
+        Path outputFile = args.length == 3 ? Paths.get(args[2]) : null;
+
+        if (!Files.exists(jvmConfigPath)) {
+            // No jvm.config file - output nothing (create empty file if 
output specified)
+            if (outputFile != null) {
+                try {
+                    Files.writeString(outputFile, "", StandardCharsets.UTF_8);
+                } catch (IOException e) {
+                    System.err.println("ERROR: Failed to write output file: " 
+ e.getMessage());
+                    System.err.flush();
+                    System.exit(1);
+                }
+            }
+            return;
+        }
+
+        try {
+            String result = parseJvmConfig(jvmConfigPath, mavenProjectBasedir);
+            if (outputFile != null) {
+                // Write directly to file - this ensures proper file handle 
cleanup on Windows
+                // Add newline at end for Windows 'for /f' command 
compatibility
+                try (Writer writer = Files.newBufferedWriter(outputFile, 
StandardCharsets.UTF_8)) {
+                    writer.write(result);
+                    if (!result.isEmpty()) {
+                        writer.write(System.lineSeparator());
+                    }
+                }
+            } else {
+                System.out.print(result);
+                System.out.flush();
+            }
+        } catch (IOException e) {
+            // If jvm.config exists but can't be read, this is a configuration 
error
+            // Print clear error and exit with error code to prevent Maven 
from running
+            System.err.println("ERROR: Failed to read .mvn/jvm.config: " + 
e.getMessage());
+            System.err.println("Please check file permissions and syntax.");
+            System.err.flush();
+            System.exit(1);
+        }
+    }
+
+    /**
+     * Parse jvm.config file and return formatted arguments.
+     * Package-private for testing.
+     */
+    static String parseJvmConfig(Path jvmConfigPath, String 
mavenProjectBasedir) throws IOException {
+        StringBuilder result = new StringBuilder();
+
+        for (String line : Files.readAllLines(jvmConfigPath, 
StandardCharsets.UTF_8)) {
+            line = processLine(line, mavenProjectBasedir);
+            if (line.isEmpty()) {
+                continue;
+            }
+
+            List<String> parsed = parseArguments(line);
+            appendQuotedArguments(result, parsed);
+        }
+
+        return result.toString();
+    }
+
+    /**
+     * Process a single line: remove comments, trim whitespace, and replace 
placeholders.
+     */
+    private static String processLine(String line, String mavenProjectBasedir) 
{
+        // Remove comments
+        int commentIndex = line.indexOf('#');
+        if (commentIndex >= 0) {
+            line = line.substring(0, commentIndex);
+        }
+
+        // Trim whitespace
+        line = line.trim();
+
+        // Replace MAVEN_PROJECTBASEDIR placeholders
+        line = line.replace("${MAVEN_PROJECTBASEDIR}", mavenProjectBasedir);
+        line = line.replace("$MAVEN_PROJECTBASEDIR", mavenProjectBasedir);
+
+        return line;
+    }
+
+    /**
+     * Append parsed arguments as quoted strings to the result builder.
+     */
+    private static void appendQuotedArguments(StringBuilder result, 
List<String> args) {
+        for (String arg : args) {
+            if (result.length() > 0) {
+                result.append(' ');
+            }
+            result.append('"').append(arg).append('"');
+        }
+    }
+
+    /**
+     * Parse a line into individual arguments, respecting quoted strings.
+     * Quotes are stripped from the arguments.
+     */
+    private static List<String> parseArguments(String line) {
+        List<String> args = new ArrayList<>();
+        StringBuilder current = new StringBuilder();
+        boolean inDoubleQuotes = false;
+        boolean inSingleQuotes = false;
+
+        for (int i = 0; i < line.length(); i++) {
+            char c = line.charAt(i);
+
+            if (c == '"' && !inSingleQuotes) {
+                inDoubleQuotes = !inDoubleQuotes;
+            } else if (c == '\'' && !inDoubleQuotes) {
+                inSingleQuotes = !inSingleQuotes;
+            } else if (c == ' ' && !inDoubleQuotes && !inSingleQuotes) {
+                // Space outside quotes - end of argument
+                if (current.length() > 0) {
+                    args.add(current.toString());
+                    current.setLength(0);
+                }
+            } else {
+                current.append(c);
+            }
+        }
+
+        // Add last argument
+        if (current.length() > 0) {
+            args.add(current.toString());
+        }
+
+        return args;
+    }
+}
\ No newline at end of file
diff --git a/apache-maven/src/assembly/maven/bin/mvn 
b/apache-maven/src/assembly/maven/bin/mvn
index 8559d47af5..1a8e6a2fdc 100755
--- a/apache-maven/src/assembly/maven/bin/mvn
+++ b/apache-maven/src/assembly/maven/bin/mvn
@@ -166,30 +166,66 @@ find_file_argument_basedir() {
 }
 
 # concatenates all lines of a file and replaces variables
+# Uses Java-based parser to handle all special characters correctly
+# This avoids shell parsing issues with pipes, quotes, @, and other special 
characters
+# and ensures POSIX compliance (no xargs -0, awk, or complex sed needed)
+# Set MAVEN_DEBUG_SCRIPT=1 to enable debug logging
 concat_lines() {
   if [ -f "$1" ]; then
-    # First convert all CR to LF using tr
-    tr '\r' '\n' < "$1" | \
-    sed -e '/^$/d' -e 's/#.*$//' | \
-    # Replace LF with NUL for xargs
-    tr '\n' '\0' | \
-    # Split into words and process each argument
-    # Use -0 with NUL to avoid special behaviour on quotes
-    xargs -n 1 -0 | \
-    while read -r arg; do
-      # Replace variables first
-      arg=$(echo "$arg" | sed \
-        -e "s@\${MAVEN_PROJECTBASEDIR}@$MAVEN_PROJECTBASEDIR@g" \
-        -e "s@\$MAVEN_PROJECTBASEDIR@$MAVEN_PROJECTBASEDIR@g")
-
-      echo "$arg"
-    done | \
-    tr '\n' ' '
+    # Use Java source-launch mode (JDK 11+) to run JvmConfigParser directly
+    # This avoids the need for compilation and temporary directories
+
+    # Debug logging
+    if [ -n "$MAVEN_DEBUG_SCRIPT" ]; then
+      echo "[DEBUG] Found jvm.config file at: $1" >&2
+      echo "[DEBUG] Running JvmConfigParser with Java: $JAVACMD" >&2
+      echo "[DEBUG] Parser arguments: $MAVEN_HOME/bin/JvmConfigParser.java $1 
$MAVEN_PROJECTBASEDIR" >&2
+    fi
+
+    # Verify Java is available
+    "$JAVACMD" -version >/dev/null 2>&1 || {
+      echo "Error: Java not found. Please set JAVA_HOME." >&2
+      return 1
+    }
+
+    # Run the parser using source-launch mode
+    # Capture both stdout and stderr for comprehensive error reporting
+    parser_output=$("$JAVACMD" "$MAVEN_HOME/bin/JvmConfigParser.java" "$1" 
"$MAVEN_PROJECTBASEDIR" 2>&1)
+    parser_exit=$?
+
+    if [ -n "$MAVEN_DEBUG_SCRIPT" ]; then
+      echo "[DEBUG] JvmConfigParser exit code: $parser_exit" >&2
+      echo "[DEBUG] JvmConfigParser output: $parser_output" >&2
+    fi
+
+    if [ $parser_exit -ne 0 ]; then
+      # Parser failed - print comprehensive error information
+      echo "ERROR: JvmConfigParser failed with exit code $parser_exit" >&2
+      echo "  jvm.config path: $1" >&2
+      echo "  Maven basedir: $MAVEN_PROJECTBASEDIR" >&2
+      echo "  Java command: $JAVACMD" >&2
+      echo "  Parser output:" >&2
+      echo "$parser_output" | sed 's/^/    /' >&2
+      exit 1
+    fi
+
+    echo "$parser_output"
   fi
 }
 
 MAVEN_PROJECTBASEDIR="`find_maven_basedir "$@"`"
-MAVEN_OPTS="$MAVEN_OPTS `concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config"`"
+# Read JVM config and append to MAVEN_OPTS, preserving special characters
+_jvm_config="`concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config"`"
+if [ -n "$_jvm_config" ]; then
+  if [ -n "$MAVEN_OPTS" ]; then
+    MAVEN_OPTS="$MAVEN_OPTS $_jvm_config"
+  else
+    MAVEN_OPTS="$_jvm_config"
+  fi
+fi
+if [ -n "$MAVEN_DEBUG_SCRIPT" ]; then
+  echo "[DEBUG] Final MAVEN_OPTS: $MAVEN_OPTS" >&2
+fi
 LAUNCHER_JAR=`echo "$MAVEN_HOME"/boot/plexus-classworlds-*.jar`
 LAUNCHER_CLASS=org.codehaus.plexus.classworlds.launcher.Launcher
 
@@ -239,6 +275,7 @@ handle_args() {
 handle_args "$@"
 MAVEN_MAIN_CLASS=${MAVEN_MAIN_CLASS:=org.apache.maven.cling.MavenCling}
 
+# Build command string for eval
 cmd="\"$JAVACMD\" \
   $MAVEN_OPTS \
   $MAVEN_DEBUG_OPTS \
@@ -251,13 +288,15 @@ cmd="\"$JAVACMD\" \
   \"-Dmaven.multiModuleProjectDirectory=$MAVEN_PROJECTBASEDIR\" \
   $LAUNCHER_CLASS \
   $MAVEN_ARGS"
+
 # Add remaining arguments with proper quoting
 for arg in "$@"; do
     cmd="$cmd \"$arg\""
 done
 
-# Debug: print the command that will be executed
-#echo "About to execute:"
-#echo "$cmd"
+if [ -n "$MAVEN_DEBUG_SCRIPT" ]; then
+  echo "[DEBUG] Launching JVM with command:" >&2
+  echo "[DEBUG]   $cmd" >&2
+fi
 
 eval exec "$cmd"
diff --git a/apache-maven/src/assembly/maven/bin/mvn.cmd 
b/apache-maven/src/assembly/maven/bin/mvn.cmd
index a3e8600df3..f25f85858f 100644
--- a/apache-maven/src/assembly/maven/bin/mvn.cmd
+++ b/apache-maven/src/assembly/maven/bin/mvn.cmd
@@ -177,38 +177,57 @@ cd /d "%EXEC_DIR%"
 
 :endDetectBaseDir
 
+rem Initialize JVM_CONFIG_MAVEN_OPTS to empty to avoid inheriting from 
environment
+set JVM_CONFIG_MAVEN_OPTS=
+
 if not exist "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadJvmConfig
 
-@setlocal EnableExtensions EnableDelayedExpansion
-set JVM_CONFIG_MAVEN_OPTS=
-for /F "usebackq tokens=* delims=" %%a in 
("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do (
-    set "line=%%a"
-
-    rem Skip empty lines and full-line comments
-    echo !line! | findstr /b /r /c:"[ ]*#" >nul
-    if errorlevel 1 (
-        rem Handle end-of-line comments by taking everything before #
-        for /f "tokens=1* delims=#" %%i in ("!line!") do set "line=%%i"
-
-        rem Trim leading/trailing spaces while preserving spaces in quotes
-        set "trimmed=!line!"
-        for /f "tokens=* delims= " %%i in ("!trimmed!") do set "trimmed=%%i"
-        for /l %%i in (1,1,100) do if "!trimmed:~-1!"==" " set 
"trimmed=!trimmed:~0,-1!"
-
-        rem Replace MAVEN_PROJECTBASEDIR placeholders
-        set "trimmed=!trimmed:${MAVEN_PROJECTBASEDIR}=%MAVEN_PROJECTBASEDIR%!"
-        set "trimmed=!trimmed:$MAVEN_PROJECTBASEDIR=%MAVEN_PROJECTBASEDIR%!"
-
-        if not "!trimmed!"=="" (
-            if "!JVM_CONFIG_MAVEN_OPTS!"=="" (
-                set "JVM_CONFIG_MAVEN_OPTS=!trimmed!"
-            ) else (
-                set "JVM_CONFIG_MAVEN_OPTS=!JVM_CONFIG_MAVEN_OPTS! !trimmed!"
-            )
-        )
-    )
+rem Use Java source-launch mode (JDK 11+) to parse jvm.config
+rem This avoids batch script parsing issues with special characters (pipes, 
quotes, @, etc.)
+rem Use temp file approach with cmd /c to ensure proper file handle release
+
+set "JVM_CONFIG_TEMP=%TEMP%\mvn-jvm-config-%RANDOM%-%RANDOM%.txt"
+
+rem Debug logging (set MAVEN_DEBUG_SCRIPT=1 to enable)
+if defined MAVEN_DEBUG_SCRIPT (
+  echo [DEBUG] Found .mvn\jvm.config file at: 
%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config
+  echo [DEBUG] Using temp file: %JVM_CONFIG_TEMP%
+  echo [DEBUG] Running JvmConfigParser with Java: %JAVACMD%
+  echo [DEBUG] Parser arguments: "%MAVEN_HOME%\bin\JvmConfigParser.java" 
"%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" "%MAVEN_PROJECTBASEDIR%" 
"%JVM_CONFIG_TEMP%"
+)
+
+rem Run parser with output file as third argument - Java writes directly to 
file to avoid Windows file locking issues
+"%JAVACMD%" "%MAVEN_HOME%\bin\JvmConfigParser.java" 
"%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" "%MAVEN_PROJECTBASEDIR%" 
"%JVM_CONFIG_TEMP%"
+set JVM_CONFIG_EXIT=%ERRORLEVEL%
+
+if defined MAVEN_DEBUG_SCRIPT (
+  echo [DEBUG] JvmConfigParser exit code: %JVM_CONFIG_EXIT%
+)
+
+rem Check if parser failed
+if %JVM_CONFIG_EXIT% neq 0 (
+  echo ERROR: Failed to parse .mvn/jvm.config file 1>&2
+  echo   jvm.config path: %MAVEN_PROJECTBASEDIR%\.mvn\jvm.config 1>&2
+  echo   Java command: %JAVACMD% 1>&2
+  if exist "%JVM_CONFIG_TEMP%" (
+    del "%JVM_CONFIG_TEMP%" 2>nul
+  )
+  exit /b 1
+)
+
+rem Read the output file
+if exist "%JVM_CONFIG_TEMP%" (
+  if defined MAVEN_DEBUG_SCRIPT (
+    echo [DEBUG] Temp file contents:
+    type "%JVM_CONFIG_TEMP%"
+  )
+  for /f "usebackq tokens=*" %%i in ("%JVM_CONFIG_TEMP%") do set 
"JVM_CONFIG_MAVEN_OPTS=%%i"
+  del "%JVM_CONFIG_TEMP%" 2>nul
+)
+
+if defined MAVEN_DEBUG_SCRIPT (
+  echo [DEBUG] Final JVM_CONFIG_MAVEN_OPTS: %JVM_CONFIG_MAVEN_OPTS%
 )
-@endlocal & set JVM_CONFIG_MAVEN_OPTS=%JVM_CONFIG_MAVEN_OPTS%
 
 :endReadJvmConfig
 
@@ -251,6 +270,11 @@ for %%i in ("%MAVEN_HOME%"\boot\plexus-classworlds-*) do 
set LAUNCHER_JAR="%%i"
 set LAUNCHER_CLASS=org.codehaus.plexus.classworlds.launcher.Launcher
 if "%MAVEN_MAIN_CLASS%"=="" @set 
MAVEN_MAIN_CLASS=org.apache.maven.cling.MavenCling
 
+if defined MAVEN_DEBUG_SCRIPT (
+  echo [DEBUG] Launching JVM with command:
+  echo [DEBUG]   "%JAVACMD%" %INTERNAL_MAVEN_OPTS% %MAVEN_OPTS% 
%JVM_CONFIG_MAVEN_OPTS% %MAVEN_DEBUG_OPTS% --enable-native-access=ALL-UNNAMED 
-classpath %LAUNCHER_JAR% "-Dclassworlds.conf=%CLASSWORLDS_CONF%" 
"-Dmaven.home=%MAVEN_HOME%" "-Dmaven.mainClass=%MAVEN_MAIN_CLASS%" 
"-Dlibrary.jline.path=%MAVEN_HOME%\lib\jline-native" 
"-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %LAUNCHER_CLASS% 
%MAVEN_ARGS% %*
+)
+
 "%JAVACMD%" ^
   %INTERNAL_MAVEN_OPTS% ^
   %MAVEN_OPTS% ^
@@ -286,4 +310,4 @@ if exist "%USERPROFILE%\mavenrc_post.cmd" call 
"%USERPROFILE%\mavenrc_post.cmd"
 @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
 if "%MAVEN_BATCH_PAUSE%"=="on" pause
 
-exit /b %ERROR_CODE%
+exit /b %ERROR_CODE%
\ No newline at end of file
diff --git 
a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITgh10937QuotedPipesInMavenOptsTest.java
 
b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITgh10937QuotedPipesInMavenOptsTest.java
index 9d954afb4f..0765147c80 100644
--- 
a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITgh10937QuotedPipesInMavenOptsTest.java
+++ 
b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITgh10937QuotedPipesInMavenOptsTest.java
@@ -42,6 +42,8 @@ void testIt() throws Exception {
 
         Verifier verifier = newVerifier(basedir.toString());
         verifier.setEnvironmentVariable("MAVEN_OPTS", 
"-Dprop.maven-opts=\"foo|bar\"");
+        // Enable debug logging for launcher script to diagnose jvm.config 
parsing issues
+        verifier.setEnvironmentVariable("MAVEN_DEBUG_SCRIPT", "1");
         verifier.addCliArguments("validate");
         verifier.execute();
         verifier.verifyErrorFreeLog();
diff --git 
a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITgh10937QuotedPipesInMavenOptsTest.java
 
b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITgh11363PipeSymbolsInJvmConfigTest.java
similarity index 58%
copy from 
its/core-it-suite/src/test/java/org/apache/maven/it/MavenITgh10937QuotedPipesInMavenOptsTest.java
copy to 
its/core-it-suite/src/test/java/org/apache/maven/it/MavenITgh11363PipeSymbolsInJvmConfigTest.java
index 9d954afb4f..4603bf09cf 100644
--- 
a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITgh10937QuotedPipesInMavenOptsTest.java
+++ 
b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITgh11363PipeSymbolsInJvmConfigTest.java
@@ -26,28 +26,30 @@
 import static org.junit.jupiter.api.Assertions.assertEquals;
 
 /**
- * This is a test set for <a 
href="https://github.com/apache/maven/issues/10937";>gh-10937</a>.
- * @since 3.0.0
- *
+ * This is a test set for <a 
href="https://github.com/apache/maven/issues/11363";>gh-11363</a>:
+ * Verify that pipe symbols in .mvn/jvm.config are properly handled and don't 
cause shell command parsing errors.
  */
-class MavenITgh10937QuotedPipesInMavenOptsTest extends 
AbstractMavenIntegrationTestCase {
+public class MavenITgh11363PipeSymbolsInJvmConfigTest extends 
AbstractMavenIntegrationTestCase {
 
     /**
-     *  Verify the dependency management of the consumer POM is computed 
correctly
+     * Verify that pipe symbols in .mvn/jvm.config are properly handled
      */
     @Test
-    void testIt() throws Exception {
-        Path basedir =
-                
extractResources("/gh-10937-pipes-maven-opts").getAbsoluteFile().toPath();
+    void testPipeSymbolsInJvmConfig() throws Exception {
+        Path basedir = extractResources("/gh-11363-pipe-symbols-jvm-config")
+                .getAbsoluteFile()
+                .toPath();
 
         Verifier verifier = newVerifier(basedir.toString());
-        verifier.setEnvironmentVariable("MAVEN_OPTS", 
"-Dprop.maven-opts=\"foo|bar\"");
+        verifier.setForkJvm(true); // Use forked JVM to test .mvn/jvm.config 
processing
+        // Enable debug logging for launcher script to diagnose jvm.config 
parsing issues
+        verifier.setEnvironmentVariable("MAVEN_DEBUG_SCRIPT", "1");
         verifier.addCliArguments("validate");
         verifier.execute();
         verifier.verifyErrorFreeLog();
 
         Properties props = verifier.loadProperties("target/pom.properties");
-        assertEquals("foo|bar", 
props.getProperty("project.properties.pom.prop.jvm-opts"));
-        assertEquals("foo|bar", 
props.getProperty("project.properties.pom.prop.maven-opts"));
+        assertEquals("de|*.de|my.company.mirror.de", 
props.getProperty("project.properties.pom.prop.nonProxyHosts"));
+        assertEquals("value|with|pipes", 
props.getProperty("project.properties.pom.prop.with.pipes"));
     }
 }
diff --git 
a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITgh11485AtSignInJvmConfigTest.java
 
b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITgh11485AtSignInJvmConfigTest.java
new file mode 100644
index 0000000000..c23128946e
--- /dev/null
+++ 
b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITgh11485AtSignInJvmConfigTest.java
@@ -0,0 +1,88 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.maven.it;
+
+import java.io.File;
+import java.util.Properties;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * This is a test set for <a 
href="https://github.com/apache/maven/issues/11485";>GH-11485</a>:
+ * Verify that @ character in .mvn/jvm.config values is handled correctly.
+ * This is important for Jenkins workspaces like workspace/project_PR-350@2
+ */
+public class MavenITgh11485AtSignInJvmConfigTest extends 
AbstractMavenIntegrationTestCase {
+
+    @Test
+    public void testAtSignInJvmConfig() throws Exception {
+        File testDir = extractResources("/gh-11485-at-sign");
+
+        Verifier verifier = newVerifier(testDir.getAbsolutePath());
+        verifier.addCliArgument(
+                "-Dexpression.outputFile=" + new File(testDir, 
"target/pom.properties").getAbsolutePath());
+        verifier.setForkJvm(true); // custom .mvn/jvm.config
+        // Enable debug logging for launcher script to diagnose jvm.config 
parsing issues
+        verifier.setEnvironmentVariable("MAVEN_DEBUG_SCRIPT", "1");
+        verifier.addCliArgument("validate");
+        verifier.execute();
+        verifier.verifyErrorFreeLog();
+
+        Properties props = verifier.loadProperties("target/pom.properties");
+        String expectedPath = testDir.getAbsolutePath().replace('\\', '/');
+        assertEquals(
+                expectedPath + "/workspace@2/test",
+                
props.getProperty("project.properties.pathWithAtProp").replace('\\', '/'),
+                "Path with @ character should be preserved");
+        assertEquals(
+                "value@test",
+                props.getProperty("project.properties.propWithAtProp"),
+                "Property value with @ character should be preserved");
+    }
+
+    @Test
+    public void testAtSignInCommandLineProperty() throws Exception {
+        File testDir = extractResources("/gh-11485-at-sign");
+
+        Verifier verifier = newVerifier(testDir.getAbsolutePath());
+        verifier.addCliArgument(
+                "-Dexpression.outputFile=" + new File(testDir, 
"target/pom.properties").getAbsolutePath());
+        verifier.setForkJvm(true); // custom .mvn/jvm.config
+        // Pass a path with @ character via command line (simulating Jenkins 
workspace)
+        String jenkinsPath = testDir.getAbsolutePath().replace('\\', '/') + 
"/jenkins.workspace/proj@2";
+        verifier.addCliArgument("-Dcmdline.path=" + jenkinsPath);
+        verifier.addCliArgument("-Dcmdline.value=test@value");
+        verifier.addCliArgument("validate");
+        verifier.execute();
+        verifier.verifyErrorFreeLog();
+
+        Properties props = verifier.loadProperties("target/pom.properties");
+        assertEquals(
+                jenkinsPath,
+                
props.getProperty("project.properties.cmdlinePath").replace('\\', '/'),
+                "Command-line path with @ character should be preserved");
+        assertEquals(
+                "test@value",
+                props.getProperty("project.properties.cmdlineValue"),
+                "Command-line value with @ character should be preserved");
+    }
+}
+
diff --git 
a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng4559SpacesInJvmOptsTest.java
 
b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng4559SpacesInJvmOptsTest.java
index e727984226..78799608bd 100644
--- 
a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng4559SpacesInJvmOptsTest.java
+++ 
b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng4559SpacesInJvmOptsTest.java
@@ -42,6 +42,8 @@ void testIt() throws Exception {
 
         Verifier verifier = newVerifier(basedir.toString());
         verifier.setEnvironmentVariable("MAVEN_OPTS", "-Dprop.maven-opts=\"foo 
bar\"");
+        // Enable debug logging for launcher script to diagnose jvm.config 
parsing issues
+        verifier.setEnvironmentVariable("MAVEN_DEBUG_SCRIPT", "1");
         verifier.addCliArguments("validate");
         verifier.execute();
         verifier.verifyErrorFreeLog();
diff --git 
a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng6255FixConcatLines.java
 
b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng6255FixConcatLines.java
index 687aa9a4dc..6ac39e4c89 100644
--- 
a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng6255FixConcatLines.java
+++ 
b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng6255FixConcatLines.java
@@ -46,7 +46,7 @@ class MavenITmng6255FixConcatLines extends 
AbstractMavenIntegrationTestCase {
     @Test
     @Disabled
     void testJvmConfigFileCR() throws Exception {
-        runWithLineEndings("\r");
+        runWithLineEndings("\r", "cr");
     }
 
     /**
@@ -56,7 +56,7 @@ void testJvmConfigFileCR() throws Exception {
      */
     @Test
     void testJvmConfigFileLF() throws Exception {
-        runWithLineEndings("\n");
+        runWithLineEndings("\n", "lf");
     }
 
     /**
@@ -66,10 +66,10 @@ void testJvmConfigFileLF() throws Exception {
      */
     @Test
     void testJvmConfigFileCRLF() throws Exception {
-        runWithLineEndings("\r\n");
+        runWithLineEndings("\r\n", "crlf");
     }
 
-    protected void runWithLineEndings(String lineEndings) throws Exception {
+    protected void runWithLineEndings(String lineEndings, String test) throws 
Exception {
         File baseDir = extractResources("/mng-6255");
         File mvnDir = new File(baseDir, ".mvn");
 
@@ -77,14 +77,16 @@ protected void runWithLineEndings(String lineEndings) 
throws Exception {
         createJvmConfigFile(jvmConfig, lineEndings, "-Djvm.config=ok", 
"-Xms256m", "-Xmx512m");
 
         Verifier verifier = newVerifier(baseDir.getAbsolutePath());
+        // Use different log file for each test to avoid overwriting
+        verifier.setLogFileName("log-" + test + ".txt");
         verifier.addCliArgument(
-                "-Dexpression.outputFile=" + new File(baseDir, 
"expression.properties").getAbsolutePath());
+                "-Dexpression.outputFile=" + new File(baseDir, "expression-" + 
test + ".properties").getAbsolutePath());
         verifier.setForkJvm(true); // custom .mvn/jvm.config
         verifier.addCliArgument("validate");
         verifier.execute();
         verifier.verifyErrorFreeLog();
 
-        Properties props = verifier.loadProperties("expression.properties");
+        Properties props = verifier.loadProperties("expression-" + test + 
".properties");
         assertEquals("ok", props.getProperty("project.properties.jvm-config"));
     }
 
diff --git 
a/its/core-it-suite/src/test/resources/gh-11363-pipe-symbols-jvm-config/.mvn/jvm.config
 
b/its/core-it-suite/src/test/resources/gh-11363-pipe-symbols-jvm-config/.mvn/jvm.config
new file mode 100644
index 0000000000..fa129e3da2
--- /dev/null
+++ 
b/its/core-it-suite/src/test/resources/gh-11363-pipe-symbols-jvm-config/.mvn/jvm.config
@@ -0,0 +1,3 @@
+# Test for MNG-11363: Maven 4 fails to parse pipe symbols in .mvn/jvm.config
+-Dhttp.nonProxyHosts=de|*.de|my.company.mirror.de
+-Dprop.with.pipes="value|with|pipes"
diff --git 
a/its/core-it-suite/src/test/resources/gh-11363-pipe-symbols-jvm-config/pom.xml 
b/its/core-it-suite/src/test/resources/gh-11363-pipe-symbols-jvm-config/pom.xml
new file mode 100644
index 0000000000..52f90ad941
--- /dev/null
+++ 
b/its/core-it-suite/src/test/resources/gh-11363-pipe-symbols-jvm-config/pom.xml
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+    Licensed to the Apache Software Foundation (ASF) under one
+    or more contributor license agreements.  See the NOTICE file
+    distributed with this work for additional information
+    regarding copyright ownership.  The ASF licenses this file
+    to you under the Apache License, Version 2.0 (the
+    "License"); you may not use this file except in compliance
+    with the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing,
+    software distributed under the License is distributed on an
+    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+    KIND, either express or implied.  See the License for the
+    specific language governing permissions and limitations
+    under the License.
+-->
+<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";>
+  <modelVersion>4.0.0</modelVersion>
+
+  <groupId>org.apache.maven.its.mng11363</groupId>
+  <artifactId>test</artifactId>
+  <version>1.0</version>
+
+  <name>Maven Integration Test :: MNG-11363</name>
+  <description>Verify that JVM args can contain pipe symbols in 
.mvn/jvm.config.</description>
+
+  <properties>
+    <pom.prop.nonProxyHosts>${http.nonProxyHosts}</pom.prop.nonProxyHosts>
+    <pom.prop.with.pipes>${prop.with.pipes}</pom.prop.with.pipes>
+  </properties>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.its.plugins</groupId>
+        <artifactId>maven-it-plugin-expression</artifactId>
+        <version>2.1-SNAPSHOT</version>
+        <executions>
+          <execution>
+            <id>test</id>
+            <goals>
+              <goal>eval</goal>
+            </goals>
+            <phase>validate</phase>
+            <configuration>
+              <outputFile>target/pom.properties</outputFile>
+              <expressions>
+                <expression>project/properties</expression>
+              </expressions>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+    </plugins>
+  </build>
+</project>
diff --git 
a/its/core-it-suite/src/test/resources/gh-11485-at-sign/.mvn/jvm.config 
b/its/core-it-suite/src/test/resources/gh-11485-at-sign/.mvn/jvm.config
new file mode 100644
index 0000000000..ec92d7c5f5
--- /dev/null
+++ b/its/core-it-suite/src/test/resources/gh-11485-at-sign/.mvn/jvm.config
@@ -0,0 +1,3 @@
+-Dpath.with.at=${MAVEN_PROJECTBASEDIR}/workspace@2/test
+-Dprop.with.at=value@test
+
diff --git a/its/core-it-suite/src/test/resources/gh-11485-at-sign/pom.xml 
b/its/core-it-suite/src/test/resources/gh-11485-at-sign/pom.xml
new file mode 100644
index 0000000000..9fdbc2444b
--- /dev/null
+++ b/its/core-it-suite/src/test/resources/gh-11485-at-sign/pom.xml
@@ -0,0 +1,70 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+-->
+<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";>
+  <modelVersion>4.0.0</modelVersion>
+
+  <groupId>org.apache.maven.its.gh11485</groupId>
+  <artifactId>test</artifactId>
+  <version>1.0</version>
+  <packaging>pom</packaging>
+
+  <name>Test @ character in jvm.config</name>
+  <description>
+    Verify that @ character in jvm.config values is handled correctly.
+    This is important for Jenkins workspaces like workspace/project_PR-350@2
+  </description>
+
+  <properties>
+    <pathWithAtProp>${path.with.at}</pathWithAtProp>
+    <propWithAtProp>${prop.with.at}</propWithAtProp>
+    <cmdlinePath>${cmdline.path}</cmdlinePath>
+    <cmdlineValue>${cmdline.value}</cmdlineValue>
+  </properties>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.its.plugins</groupId>
+        <artifactId>maven-it-plugin-expression</artifactId>
+        <version>2.1-SNAPSHOT</version>
+        <executions>
+          <execution>
+            <phase>validate</phase>
+            <goals>
+              <goal>eval</goal>
+            </goals>
+            <configuration>
+              <outputFile>target/pom.properties</outputFile>
+              <expressions>
+                <expression>project/properties/pathWithAtProp</expression>
+                <expression>project/properties/propWithAtProp</expression>
+                <expression>project/properties/cmdlinePath</expression>
+                <expression>project/properties/cmdlineValue</expression>
+              </expressions>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+    </plugins>
+  </build>
+</project>
+
diff --git 
a/its/core-it-support/maven-it-helper/src/main/java/org/apache/maven/it/Verifier.java
 
b/its/core-it-support/maven-it-helper/src/main/java/org/apache/maven/it/Verifier.java
index 1904d97152..d8c6780580 100644
--- 
a/its/core-it-support/maven-it-helper/src/main/java/org/apache/maven/it/Verifier.java
+++ 
b/its/core-it-support/maven-it-helper/src/main/java/org/apache/maven/it/Verifier.java
@@ -296,6 +296,30 @@ public void execute() throws VerificationException {
                     System.err.println("Warning: Could not prepend command 
line to log file: " + e.getMessage());
                 }
             }
+
+            // Save stdout/stderr to files if not empty (captures shell script 
debug output)
+            if (logFileName != null) {
+                String logBaseName = logFileName.endsWith(".txt")
+                        ? logFileName.substring(0, logFileName.length() - 4)
+                        : logFileName;
+                if (stdout.size() > 0) {
+                    try {
+                        Path stdoutFile = basedir.resolve(logBaseName + 
"-stdout.txt");
+                        Files.writeString(stdoutFile, 
stdout.toString(StandardCharsets.UTF_8), StandardCharsets.UTF_8);
+                    } catch (IOException e) {
+                        System.err.println("Warning: Could not write stdout 
file: " + e.getMessage());
+                    }
+                }
+                if (stderr.size() > 0) {
+                    try {
+                        Path stderrFile = basedir.resolve(logBaseName + 
"-stderr.txt");
+                        Files.writeString(stderrFile, 
stderr.toString(StandardCharsets.UTF_8), StandardCharsets.UTF_8);
+                    } catch (IOException e) {
+                        System.err.println("Warning: Could not write stderr 
file: " + e.getMessage());
+                    }
+                }
+            }
+
             if (ret > 0) {
                 String dump;
                 try {
@@ -478,15 +502,18 @@ private String formatCommandLine(ExecutorRequest request, 
ExecutorHelper.Mode mo
             }
         }
 
-        // Add environment variables that would be set
+        // Add environment variables that would be set (excluding MAVEN_OPTS 
which is handled separately)
         if (request.environmentVariables().isPresent() && 
!request.environmentVariables().get().isEmpty()) {
             cmdLine.append("\n# Environment variables:");
             for (Map.Entry<String, String> entry : 
request.environmentVariables().get().entrySet()) {
-                cmdLine.append("\n# 
").append(entry.getKey()).append("=").append(entry.getValue());
+                if (!"MAVEN_OPTS".equals(entry.getKey())) {
+                    cmdLine.append("\n# 
").append(entry.getKey()).append("=").append(entry.getValue());
+                }
             }
         }
 
-        // Add JVM arguments that would be set via MAVEN_OPTS
+        // Compute the final MAVEN_OPTS value (combining env var + jvmArgs)
+        // This matches what ForkedMavenExecutor does
         List<String> jvmArgs = new ArrayList<>();
         if 
(!request.userHomeDirectory().equals(ExecutorRequest.getCanonicalPath(Paths.get(System.getProperty("user.home")))))
 {
             jvmArgs.add("-Duser.home=" + 
request.userHomeDirectory().toString());
@@ -500,8 +527,23 @@ private String formatCommandLine(ExecutorRequest request, 
ExecutorHelper.Mode mo
                     .toList());
         }
 
+        // Build the final MAVEN_OPTS value
+        StringBuilder mavenOpts = new StringBuilder();
+        if (request.environmentVariables().isPresent()) {
+            String existingMavenOpts = 
request.environmentVariables().get().get("MAVEN_OPTS");
+            if (existingMavenOpts != null && !existingMavenOpts.isEmpty()) {
+                mavenOpts.append(existingMavenOpts);
+            }
+        }
         if (!jvmArgs.isEmpty()) {
-            cmdLine.append("\n# MAVEN_OPTS=").append(String.join(" ", 
jvmArgs));
+            if (mavenOpts.length() > 0) {
+                mavenOpts.append(" ");
+            }
+            mavenOpts.append(String.join(" ", jvmArgs));
+        }
+
+        if (mavenOpts.length() > 0) {
+            cmdLine.append("\n# MAVEN_OPTS=").append(mavenOpts.toString());
         }
 
         if (request.skipMavenRc()) {


Reply via email to