Skip to content

Just me trying to understand how ProcessBuilder works - mostly looking at cmd line escape rules for Windows

Notifications You must be signed in to change notification settings

lread/info-process-builder

Repository files navigation

info-process-builder

Rationale

As a Clojure developer I use Java’s ProcessBuilder to spawn scripts and command line applications. I’d like to better understand how it behaves.

Windows command line escaping is what initially confused me (and still does), more recently I’ve become interested in program resolution.

Command Line Escaping

Initial notes, have barely started to understand. May return to study this more someday.

The command line escaping curiosity is borne from a Babashka script I was writing for rewrite-cljc and discussions continued with @borkdude’s babashka/process library. In my rewrite-cljc Babashka script, I wanted to pass the following clj-kondo config on the command line:

{:output {:include-files ["^src" "^test" "^script"]}}

After a few attempts to make this work on Windows, I gave up and moved the config from the command line to a file.

For Windows developers who have embraced WSL2, maybe all these Windows escaping rules less of a concern? I am still curious.

I find I am often confused by the command escaping rules when for Java’s ProcessBuilder on the Windows platform. Although it is Windows that has me confused, I’ll also explore command escaping rules when invoking bash scripts and native executables on Linux and macOS.

There are likely different/better ways to what I’ve discovered here. I’ll update this work as I learn more.

Method

I’ve created little executables that echo the args they have received.

Example output of such a script called with args one two three:

argc: 3
arg1: one
arg2: two
arg3: three

A variable in the equation is how these args get displayed. Does the method of display affect the output? I think, but am not yet entirely confident, that I’ve got the right incantation to tell .bat and .cmd files not to interpret on echo, but a side effect is that args will be display "with double quotes"

For Windows I explore:

  • .ps1 - Powershell

  • .bat - Batch file

  • .cmd - Cmd file

  • .exe - Native exe

Note that batch and cmd script variants are identical and expected to behave the same. I’m including them both to verify that assumption is true.

For Linux and macOS:

  • .sh - Bash script

  • Native exe

A ptest.clj Clojure program launches these scripts with various args and is where I experiment with escaping rules. It might be more prudent, as I move forward, to make this a unit test.

References

Windows command line parsing, it turns out, is complex. Here are some references to work through those complexities:

Java Bugs Filed

There have been many Java bugs filed against Windows behavior, here are a couple:

The Java team seem have concluded that:

Unfortunately, the Windows API maps most directly to an interface where Runtime.exec(String) passes the arg string directly to the OS, but that method is specified to break up its arguments using StringTokenizer, which is surprisingly rarely what you want.

It’s very hard to make changes in this area, because - they change the behavior, and so are incompatible - the current behavior (how to pass strings to the OS) is unspecified, and so is spec-compliant.

If there’s any way at all that the current behavior can be relied upon, then we really can’t change it.

Which I’ll paraphrase to: we can’t generalize to a gong-show. I sympathize and empathize with the Java developers who had to contend with this. But I do wonder if modern Microsoft has stabilized here.

Current Java Sources

From OpenJDK source, we have:

Exploring Windows Escaping

For command line escaping we wonder what is happening with the following players:

  • Java

  • The Windows shell, if involved

  • Windows createProcessW

  • Windows OS

That’s a lot of wondering.

In the following sections I’ll use:

  • Command shell - to mean the shell invoked by cmd.exe

  • PowerShell - to mean the shell invoked by powershell.exe

Cmd & Bat

Observe the following behavior under the Command shell:

>echo "Ar"g"um"e"n"t" W"it"h Sp"aces""
"Ar"g"um"e"n"t" W"it"h Sp"aces""
>exe-test.exe "Ar"g"um"e"n"t" W"it"h Sp"aces""
argc: 1
arg1: Argument With Spaces

This can seem baffling to the uninitiated.

PowerShell

PowerShell seems less complex.

Direct Invocation

TODO…​

Observations

  • On Windows, ProcessBuilder will invoke .bat., .cmd and .exe files directly, but a Powershell .ps1 script needs to be explicitly invoked via powershell executable.

Current Open Questions

  • What’s with the caret? Some docs say the OS deals with this escape character. True?

  • The escape rules for .bat and .cmd are on the complex side. I can’t say I understand how to properly escape for these targets yet.

  • Escaping requirements depend on what, if anything, finally ends up being called by script/exe.

  • Does the shell that Java is invoked from have any affect?

  • UTF8 in command lines?

Program Resolution

How are programs found by ProcessBuilder on Windows? How does this compare with how programs are found when running PowerShell and CMD shells? Windows has suprised me in the past, so I’m going to explore cases that might seem obvious to those already familiar with how Windows works.

We’ll test using 3 distinct folders:

  • cwd - we’ll always launch from this folder

  • workdir - we’ll optionally specify this as the directory workdir for ProcessBuilder tests

  • on-path - we’ll add this to our PATH

The above folders are populated with variants of print-dirs. In the tables below, we’ll describe what we’ve load up under the "Scenario" column, for example:

cwd     [ps1]
workdir [bat cmd com exe ps1]
on-path [bat cmd com exe ps1]

For easier to read results we’ll rename print-dirs to a. Example output from running a:

exepath: Z:\lread\info-process-builder\resolve-test\scenario\on-path\a.com
workdir: Z:\lread\info-process-builder\resolve-test\scenario\cwd

We’ll shorten this output in our tables below to the easier to scan:

on-path\a.com
cwd

A Simpler World: macOS & Linux

For comparison, let’s first have a look at how Linux behaves (we can assume macOS behaves the same).

Scenario Bash ProcessBuilder

a.sh

cwd     [sh]
workdir [sh]
on-path [sh]
on-path/a.sh
cwd
on-path/a.sh
cwd

./a.sh

cwd     [sh]
workdir [sh]
on-path [sh]
cwd/a.sh
cwd
cwd/a.ash
cwd

$(readlink -f ../workdir/a.sh) (absolute path)

cwd     [sh]
workdir [sh]
on-path [sh]
workdir/a.sh
cwd
workdir/a.sh
cwd

And when specifying a workdir:

Scenario ProcessBuilder

a.sh

cwd     [sh]
workdir [sh]
on-path [sh]
on-path/a.sh
workdir

./a.sh

cwd     [sh]
workdir [sh]
on-path [sh]
workdir/a.sh
workdir

$(readlink -f ../cwd/a.sh) (absolute path)

cwd     [sh]
workdir [sh]
on-path [sh]
cwd/a.sh
workdir

Oh such sweet, consistent, and clearly understandable simplicity.

  • If program has no path prefix, PATH is searched.

  • If program has a relative path prefix, program is resolved against:

    • cwd

    • or, if specified via ProcessBuilder, directory workdir

  • If program has an absolute path, the absolute path is used.

Windows

Our scenario folders are populated with copies .com,.exe,.bat,.cmd,.ps1 variants of print-dirs.

Note that .com is obscure these days, and I don’t know how to build a proper one, so print-dirs.com is actually just a copy of print-dirs.exe.

Source for print-dirs.exe is in print-dirs.go which can be built on any OS via:

GOOS=windows GOARCH=amd64 go build -o print-dirs.exe print-dirs.go

Or if you want to build via docker (and apply tricks for smaller .exe)

docker run --rm \
  -v "$PWD":/src \
  -w /src \
  devopsworks/golang-upx:latest \
  bash -c 'GOOS=windows GOARCH=amd64 \
    go build -ldflags "-s -w" -o print-dirs.exe print-dirs.go &&
    upx --ultra-brute print-dirs.exe'

As mentioned above, we rename print-dirs to a when populating scenarios for briefer results.

Observations & Notes

All tests run on Windows 10 with all updates applied as of 2024-07-18.

Program extension auto-resolution (in order):

  • PowerShell: .ps1,.com,.exe,.bat,.cmd

  • CMD Shell: .com,.exe,.bat,.cmd

  • ProcessBuilder: .exe

Directory auto-resolution (in order):

  • Powershell: PATH

  • CMD Shell: cwd, PATH

  • ProcessBuilder:

    • no workdir: cwd, PATH

    • with workdir:

      • .com, .exe: cwd, PATH (resolving to cwd seems odd because we’ve specified a workdir)

      • .bat, .cmd: workdir, PATH (but maybe intended to be PATH, not sure, buggy)

When invoking .\a.ps1 from a CMD shell, notepad launches with file contents. This kind of mucks up our testing, so I temporarily changed Windows to point to fake-notepad.exe.

  • Registry key: HKEY_CLASSES_ROOT\Microsoft.PowerShellScript.1\Shell\Open\Command

  • Old Registry value: "C:\Windows\System32\notepad.exe" "%1"

Impact of host shell on ProcessBuilder:

  • I saw no difference between launching ProcessBuilder tests from CMD Shell or PowerShell.

Impact of JDK version on ProcessBuilder:

  • I repeated tests on JDK versions 8, 11, 17, 21 and 22 and saw no difference.

How does a resolve from cwd?

Nothing terribly suprising here.

  • Both CMD Shell and ProcessBuilder resolve a from the current working directory.

  • PowerShell, like linux/macOS does not automatically resolve from current working directory.

  • CMD Shell, of course, does not resolve to a.ps1.

  • Perhaps a bit suprising is that ProcessBuilder only resolves to a.exe.

Scenario Cmd Shell PowerShell ProcessBuilder

a

cwd:     [bat cmd com exe ps1]
workdir: []
on-path: []
cwd\a.com
cwd

<not found>

cwd\a.exe
cwd

a

cwd:     [bat cmd exe ps1]
workdir: []
on-path: []
cwd\a.exe
cwd

<not found>

cwd\a.exe
cwd

a

cwd:     [bat cmd ps1]
workdir: []
on-path: []
cwd\a.bat
cwd

<not found>

<not found>

a

cwd:     [cmd ps1]
workdir: []
on-path: []
cwd\a.cmd
cwd

<not found>

<not found>

a

cwd:     [ps1]
workdir: []
on-path: []

<not found>

<not found>

<not found>

How does a.<ext> resolve from cwd?

Nothing terribly suprising here:

  • CMD Shell cannot run .ps1 files, and neither can ProcessBuilder.

  • PowerShell, like linux/macOS does not automatically resolve from current working directory.

Scenario Cmd Shell PowerShell ProcessBuilder

a.ps1

cwd:     [bat cmd com exe ps1]
workdir: []
on-path: []

<opened notepad with cwd\a.ps1>

<not found>

<not a valid Win32 application>

a.com

cwd:     [bat cmd com exe ps1]
workdir: []
on-path: []
cwd\a.com
cwd

<not found>

cwd\a.com
cwd

a.exe

cwd:     [bat cmd com exe ps1]
workdir: []
on-path: []
cwd\a.exe
cwd

<not found>

cwd\a.exe
cwd

a.bat

cwd:     [bat cmd com exe ps1]
workdir: []
on-path: []
cwd\a.bat
cwd

<not found>

cwd\a.bat
cwd

a.cmd

cwd:     [bat cmd com exe ps1]
workdir: []
on-path: []
cwd\a.cmd
cwd

<not found>

cwd\a.cmd
cwd

How does a.<ext> resolve from cwd when also on PATH?

Scenario Cmd Shell PowerShell ProcessBuilder

a.ps1

cwd:     [bat cmd com exe ps1]
workdir: []
on-path: [bat cmd com exe ps1]

<opened notepad with cwd\a.ps1>

on-path\a.ps1
cwd

<not a valid Win32 application>

a.com

cwd:     [bat cmd com exe ps1]
workdir: []
on-path: [bat cmd com exe ps1]
cwd\a.com
cwd
on-path\a.com
cwd
cwd\a.com
cwd

a.exe

cwd:     [bat cmd com exe ps1]
workdir: []
on-path: [bat cmd com exe ps1]
cwd\a.exe
cwd
on-path\a.exe
cwd
cwd\a.exe
cwd

a.bat

cwd:     [bat cmd com exe ps1]
workdir: []
on-path: [bat cmd com exe ps1]
cwd\a.bat
cwd
on-path\a.bat
cwd
cwd\a.bat
cwd

a.cmd

cwd:     [bat cmd com exe ps1]
workdir: []
on-path: [bat cmd com exe ps1]
cwd\a.cmd
cwd
on-path\a.cmd
cwd
cwd\a.cmd
cwd

a.ps1

cwd:     []
workdir: []
on-path: [bat cmd com exe ps1]

<opened notepad with on-path\a.ps1>

on-path\a.ps1
cwd

<not a valid Win32 application>

a.com

cwd:     []
workdir: []
on-path: [bat cmd com exe ps1]
on-path\a.com
cwd
on-path\a.com
cwd
on-path\a.com
cwd

a.exe

cwd:     []
workdir: []
on-path: [bat cmd com exe ps1]
on-path\a.exe
cwd
on-path\a.exe
cwd
on-path\a.exe
cwd

a.bat

cwd:     []
workdir: []
on-path: [bat cmd com exe ps1]
on-path\a.bat
cwd
on-path\a.bat
cwd
on-path\a.bat
cwd

a.cmd

cwd:     []
workdir: []
on-path: [bat cmd com exe ps1]
on-path\a.cmd
cwd
on-path\a.cmd
cwd
on-path\a.cmd
cwd

How does a resolve from cwd when also on PATH?

Nothing terribly suprising:

  • We see again that ProcessBuilder only resolves a.exe

  • We see again that ProcessBuilder and CMD Shell prefer cwd over PATH

  • As expected, PowerShell resolves to a.ps1

  • We see again that PowerShell behaves more like macOS/Linux.

Scenario Cmd Shell PowerShell ProcessBuilder

a

cwd:     [bat cmd com exe ps1]
workdir: []
on-path: [bat cmd com exe ps1]
cwd\a.com
cwd
on-path\a.ps1
cwd
cwd\a.exe
cwd

a

cwd:     [bat cmd exe ps1]
workdir: []
on-path: [bat cmd com exe ps1]
cwd\a.exe
cwd
on-path\a.ps1
cwd
cwd\a.exe
cwd

a

cwd:     [bat cmd ps1]
workdir: []
on-path: [bat cmd com exe ps1]
cwd\a.bat
cwd
on-path\a.ps1
cwd
on-path\a.exe
cwd

a

cwd:     [cmd ps1]
workdir: []
on-path: [bat cmd com exe ps1]
cwd\a.cmd
cwd
on-path\a.ps1
cwd
on-path\a.exe
cwd

a

cwd:     [ps1]
workdir: []
on-path: [bat cmd com exe ps1]
on-path\a.com
cwd
on-path\a.ps1
cwd
on-path\a.exe
cwd

a

cwd:     [ps1]
workdir: []
on-path: [bat cmd com exe]
on-path\a.com
cwd
on-path\a.com
cwd
on-path\a.exe
cwd

a

cwd:     [ps1]
workdir: []
on-path: [bat cmd exe]
on-path\a.exe
cwd
on-path\a.exe
cwd
on-path\a.exe
cwd

a

cwd:     [ps1]
workdir: []
on-path: [bat cmd]
on-path\a.bat
cwd
on-path\a.bat
cwd

<not found>

a

cwd:     [ps1]
workdir: []
on-path: [cmd]
on-path\a.cmd
cwd
on-path\a.cmd
cwd

<not found>

a

cwd:     [ps1]
workdir: []
on-path: []

<not found>

<not found>

<not found>

How does .\a resolve from cwd?

Nothing terribly surprising:

  • We’ve already learned that ProcessBuilder only resolves to .exe

Scenario Cmd Shell PowerShell ProcessBuilder

.\a

cwd:     [bat cmd com exe ps1]
workdir: []
on-path: []
cwd\a.com
cwd
cwd\a.ps1
cwd
cwd\a.exe
cwd

.\a

cwd:     [bat cmd com exe]
workdir: []
on-path: []
cwd\a.com
cwd
cwd\a.com
cwd
cwd\a.exe
cwd

.\a

cwd:     [bat cmd exe]
workdir: []
on-path: []
cwd\a.exe
cwd
cwd\a.exe
cwd
cwd\a.exe
cwd

.\a

cwd:     [bat cmd]
workdir: []
on-path: []
cwd\a.bat
cwd
cwd\a.bat
cwd

<not found>

.\a

cwd:     [cmd]
workdir: []
on-path: []
cwd\a.cmd
cwd
cwd\a.cmd
cwd

<not found>

How does .\a.<ext> resolve from cwd?

No real suprises:

  • It is expected that CMD Shell cannot run .ps1 scripts.

  • ProcessBuilder cannot run .ps1 scripts directly either.

Scenario Cmd Shell PowerShell ProcessBuilder

.\a.ps1

cwd:     [bat cmd com exe ps1]
workdir: []
on-path: []

<opened notepad with cwd\a.ps1>

cwd\a.ps1
cwd

<not a valid Win32 application>

.\a.com

cwd:     [bat cmd com exe ps1]
workdir: []
on-path: []
cwd\a.com
cwd
cwd\a.com
cwd
cwd\a.com
cwd

.\a.exe

cwd:     [bat cmd com exe ps1]
workdir: []
on-path: []
cwd\a.exe
cwd
cwd\a.exe
cwd
cwd\a.exe
cwd

.\a.bat

cwd:     [bat cmd com exe ps1]
workdir: []
on-path: []
cwd\a.bat
cwd
cwd\a.bat
cwd
cwd\a.bat
cwd

.\a.cmd

cwd:     [bat cmd com exe ps1]
workdir: []
on-path: []
cwd\a.cmd
cwd
cwd\a.cmd
cwd
cwd\a.cmd
cwd

We expect absolute path of a.<ext> to work fine, do they?

Yes they do (note that I abbreviated absolute paths with …​ for easier reading).

  • We did not expect CMD Shell to run a .ps1

  • We’ve already learned that PowerShell does not run .ps1 scripts directly.

Scenario Cmd Shell PowerShell ProcessBuilder

Z:\...\cwd\a.ps1

cwd:     [bat cmd com exe ps1]
workdir: [bat cmd com exe ps1]
on-path: [bat cmd com exe ps1]

<opened notepad with cwd\a.ps1>

cwd\a.ps1
cwd

<not a valid Win32 application>

Z:\...\cwd\a.com

cwd:     [bat cmd com exe ps1]
workdir: [bat cmd com exe ps1]
on-path: [bat cmd com exe ps1]
cwd\a.com
cwd
cwd\a.com
cwd
cwd\a.com
cwd

Z:\...\cwd\a.exe

cwd:     [bat cmd com exe ps1]
workdir: [bat cmd com exe ps1]
on-path: [bat cmd com exe ps1]
cwd\a.exe
cwd
cwd\a.exe
cwd
cwd\a.exe
cwd

Z:\...\cwd\a.bat

cwd:     [bat cmd com exe ps1]
workdir: [bat cmd com exe ps1]
on-path: [bat cmd com exe ps1]
cwd\a.bat
cwd
cwd\a.bat
cwd
cwd\a.bat
cwd

Z:\...\cwd\a.cmd

cwd:     [bat cmd com exe ps1]
workdir: [bat cmd com exe ps1]
on-path: [bat cmd com exe ps1]
cwd\a.cmd
cwd
cwd\a.cmd
cwd
cwd\a.cmd
cwd

Z:\...\workdir\a.ps1

cwd:     [bat cmd com exe ps1]
workdir: [bat cmd com exe ps1]
on-path: [bat cmd com exe ps1]

<opened notepad with workdir\a.ps1>

workdir\a.ps1
cwd

<not a valid Win32 application>

Z:\...\workdir\a.com

cwd:     [bat cmd com exe ps1]
workdir: [bat cmd com exe ps1]
on-path: [bat cmd com exe ps1]
workdir\a.com
cwd
workdir\a.com
cwd
workdir\a.com
cwd

Z:\...\workdir\a.exe

cwd:     [bat cmd com exe ps1]
workdir: [bat cmd com exe ps1]
on-path: [bat cmd com exe ps1]
workdir\a.exe
cwd
workdir\a.exe
cwd
workdir\a.exe
cwd

Z:\...\workdir\a.bat

cwd:     [bat cmd com exe ps1]
workdir: [bat cmd com exe ps1]
on-path: [bat cmd com exe ps1]
workdir\a.bat
cwd
workdir\a.bat
cwd
workdir\a.bat
cwd

Z:\...\workdir\a.cmd

cwd:     [bat cmd com exe ps1]
workdir: [bat cmd com exe ps1]
on-path: [bat cmd com exe ps1]
workdir\a.cmd
cwd
workdir\a.cmd
cwd
workdir\a.cmd
cwd

Z:\...\on-path\a.ps1

cwd:     [bat cmd com exe ps1]
workdir: [bat cmd com exe ps1]
on-path: [bat cmd com exe ps1]

<opened notepad with on-path\a.ps1>

on-path\a.ps1
cwd

<not a valid Win32 application>

Z:\...\on-path\a.com

cwd:     [bat cmd com exe ps1]
workdir: [bat cmd com exe ps1]
on-path: [bat cmd com exe ps1]
on-path\a.com
cwd
on-path\a.com
cwd
on-path\a.com
cwd

Z:\...\on-path\a.exe

cwd:     [bat cmd com exe ps1]
workdir: [bat cmd com exe ps1]
on-path: [bat cmd com exe ps1]
on-path\a.exe
cwd
on-path\a.exe
cwd
on-path\a.exe
cwd

Z:\...\on-path\a.bat

cwd:     [bat cmd com exe ps1]
workdir: [bat cmd com exe ps1]
on-path: [bat cmd com exe ps1]
on-path\a.bat
cwd
on-path\a.bat
cwd
on-path\a.bat
cwd

Z:\...\on-path\a.cmd

cwd:     [bat cmd com exe ps1]
workdir: [bat cmd com exe ps1]
on-path: [bat cmd com exe ps1]
on-path\a.cmd
cwd
on-path\a.cmd
cwd
on-path\a.cmd
cwd

How does a with workdir resolve from cwd, workdir, PATH?

Specifying a workdir only applies to ProcessBuilder so we leave out comparisons with CMD Shell and PowerShell.

This matches up with what we learned so far:

  • ProcessBuilder prefers cwd (overidden here with workdir) then PATH

  • ProcessBuilder only resolves to .exe

Scenario ProcessBuilder

a

cwd:     [bat cmd com exe ps1]
workdir: [bat cmd com exe ps1]
on-path: [bat cmd com exe ps1]
cwd\a.exe
workdir

a

cwd:     [bat cmd com ps1]
workdir: [bat cmd com exe ps1]
on-path: [bat cmd com exe ps1]
on-path\a.exe
workdir

a

cwd:     [bat cmd com ps1]
workdir: [bat cmd com exe ps1]
on-path: [bat cmd com ps1]

<not found>

How does a.<ext> with workdir resolve from cwd, workdir, PATH?

This has some real suprises, see comments in the table.

Scenario ProcessBuilder

a.ps1

cwd:     [bat cmd com exe ps1]
workdir: [bat cmd com exe ps1]
on-path: [bat cmd com exe ps1]

<not a valid Win32 application>

a.ps1

cwd:     [bat cmd com exe]
workdir: [bat cmd com exe ps1]
on-path: [bat cmd com exe ps1]

<not a valid Win32 application>

a.com

cwd:     [bat cmd com exe]
workdir: [bat cmd com exe ps1]
on-path: [bat cmd com exe ps1]
cwd\a.com
workdir

Since we have specified a workdir, it is surprising that this resolves to cwd. Less suprising would have been resolution to workdir, or PATH.

a.com

cwd:     [bat cmd exe]
workdir: [bat cmd com exe ps1]
on-path: [bat cmd com exe ps1]
on-path\a.com
workdir

a.exe

cwd:     [bat cmd exe]
workdir: [bat cmd com exe ps1]
on-path: [bat cmd com exe ps1]
cwd\a.exe
workdir

Since we have specified a workdir, it is surprising that this resolves to cwd. Less suprising would have been resolution to workdir, or PATH. Consistent with a.com.

a.exe

cwd:     [bat cmd]
workdir: [bat cmd com exe ps1]
on-path: [bat cmd com exe ps1]
on-path\a.exe
workdir

a.bat

cwd:     [bat cmd]
workdir: [bat cmd com exe ps1]
on-path: [bat cmd com exe ps1]
workdir\a.bat
workdir

Inconsistent with a.com, a.exe.

a.bat

cwd:     [bat cmd]
workdir: [cmd com exe ps1]
on-path: [bat cmd com exe ps1]
on-path\a.bat
workdir

Ok, favors workdir then PATH.

a.cmd

cwd:     [cmd]
workdir: [cmd]
on-path: [cmd]
workdir\a.cmd
workdir

Same behavior as a.bat.

a.cmd

cwd:     [cmd]
workdir: []
on-path: [cmd]
on-path\a.cmd
workdir

Same behavior as a.bat.

How does .\a resolve from cwd, workdir?

No surprises, we’ve already learned:

  • ProcessBuilder only resolves to .exe

Scenario ProcessBuilder

.\a

cwd:     [bat cmd com exe ps1]
workdir: [bat cmd com exe ps1]
on-path: [bat cmd com exe ps1]
cwd\a.exe
workdir

.\a

cwd:     [bat cmd com ps1]
workdir: [bat cmd com exe ps1]
on-path: [bat cmd com exe ps1]

<not found>

How does .\a.<ext> with workdir resolve from cwd, workdir?

Some surprises here, see comments in table.

Scenario ProcessBuilder

.\a.ps1

cwd:     [bat cmd com exe ps1]
workdir: [bat cmd com exe ps1]
on-path: [bat cmd com exe ps1]

<not a valid Win32 application>

.\a.com

cwd:     [bat cmd com exe ps1]
workdir: [bat cmd com exe ps1]
on-path: [bat cmd com exe ps1]
cwd\a.com
workdir

Consistent with a.com, but like a.com, suprising. We’ve specified a workdir, why are we resolving from cwd.

.\a.exe

cwd:     [bat cmd com exe ps1]
workdir: [bat cmd com exe ps1]
on-path: [bat cmd com exe ps1]
cwd\a.exe
workdir

Consistent with a.exe, but like a.exe, suprising. We’ve specified a workdir, why are we resolving from cwd.

.\a.bat

cwd:     [bat cmd com exe ps1]
workdir: [bat cmd com exe ps1]
on-path: [bat cmd com exe ps1]
workdir\a.bat
workdir

Consistent with a.bat.

.\a.bat

cwd:     [cmd com exe ps1]
workdir: [bat cmd com exe ps1]
on-path: [bat cmd com exe ps1]

<not found>

Happened to notice this, so showing what seems like a bug in ProcessBuilder. (Since workdir is preferred why is the absence of a.bat in cwd causing an error?)

.\a.cmd

cwd:     [bat cmd com exe ps1]
workdir: [bat cmd com exe ps1]
on-path: [bat cmd com exe ps1]
workdir\a.cmd
workdir

.\a.cmd

cwd:     [bat com exe ps1]
workdir: [bat cmd com exe ps1]
on-path: [bat cmd com exe ps1]

<not found>

Happened to notice this, so showing what seems like a bug in ProcessBuilder. (Since workdir is preferred why is the absence of a.cmd in cwd causing an error?)

We expect absolute path of a.<ext> with workdir to work fine, do they?

Yes they do (note that I abbreviated absolute paths with …​ for easier reading).

  • We’ve already learned that PowerShell does not run .ps1 scripts directly.

Scenario ProcessBuilder

Z:\...\cwd\a.ps1

cwd:     [bat cmd com exe ps1]
workdir: [bat cmd com exe ps1]
on-path: [bat cmd com exe ps1]

<not a valid Win32 application>

Z:\...\cwd\a.com

cwd:     [bat cmd com exe ps1]
workdir: [bat cmd com exe ps1]
on-path: [bat cmd com exe ps1]
cwd\a.com
workdir

Z:\...\cwd\a.exe

cwd:     [bat cmd com exe ps1]
workdir: [bat cmd com exe ps1]
on-path: [bat cmd com exe ps1]
cwd\a.exe
workdir

Z:\...\cwd\a.bat

cwd:     [bat cmd com exe ps1]
workdir: [bat cmd com exe ps1]
on-path: [bat cmd com exe ps1]
cwd\a.bat
workdir

Z:\...\cwd\a.cmd

cwd:     [bat cmd com exe ps1]
workdir: [bat cmd com exe ps1]
on-path: [bat cmd com exe ps1]
cwd\a.cmd
workdir

Z:\...\workdir\a.ps1

cwd:     [bat cmd com exe ps1]
workdir: [bat cmd com exe ps1]
on-path: [bat cmd com exe ps1]

<not a valid Win32 application>

Z:\...\workdir\a.com

cwd:     [bat cmd com exe ps1]
workdir: [bat cmd com exe ps1]
on-path: [bat cmd com exe ps1]
workdir\a.com
workdir

Z:\...\workdir\a.exe

cwd:     [bat cmd com exe ps1]
workdir: [bat cmd com exe ps1]
on-path: [bat cmd com exe ps1]
workdir\a.exe
workdir

Z:\...\workdir\a.bat

cwd:     [bat cmd com exe ps1]
workdir: [bat cmd com exe ps1]
on-path: [bat cmd com exe ps1]
workdir\a.bat
workdir

Z:\...\workdir\a.cmd

cwd:     [bat cmd com exe ps1]
workdir: [bat cmd com exe ps1]
on-path: [bat cmd com exe ps1]
workdir\a.cmd
workdir

Z:\...\on-path\a.ps1

cwd:     [bat cmd com exe ps1]
workdir: [bat cmd com exe ps1]
on-path: [bat cmd com exe ps1]

<not a valid Win32 application>

Z:\...\on-path\a.com

cwd:     [bat cmd com exe ps1]
workdir: [bat cmd com exe ps1]
on-path: [bat cmd com exe ps1]
on-path\a.com
workdir

Z:\...\on-path\a.exe

cwd:     [bat cmd com exe ps1]
workdir: [bat cmd com exe ps1]
on-path: [bat cmd com exe ps1]
on-path\a.exe
workdir

Z:\...\on-path\a.bat

cwd:     [bat cmd com exe ps1]
workdir: [bat cmd com exe ps1]
on-path: [bat cmd com exe ps1]
on-path\a.bat
workdir

Z:\...\on-path\a.cmd

cwd:     [bat cmd com exe ps1]
workdir: [bat cmd com exe ps1]
on-path: [bat cmd com exe ps1]
on-path\a.cmd
workdir

Running Tests

These tests only explore behaviour they do not verify behaviour against some expectations.

From resolve-test dir, gen test scripts with:

bb gen_resolve_tests.clj

Run CMD Shell tests from a CMD shell via:

  • run-cmd-tests.bat - CMD shell tests

  • run-pb-cmd-tests.bat - Run ProcessBuilder tests (from CMD)

Run Powershell tests from a Powershell via:

  • run-pwr-tests.ps1 - PowerShell tests

  • run-pb-pwr-tests.ps1 - Run ProcessBuilder tests (from PowerShell)

Why do we run ProcessBuilder from both CMD and PowerShell? I was curious to see any effects on resolving if the launching shell was different. (I found no differences)

The run-pb-* tests should be run against each jdk version you are interested in testing against. (I found no differences on jdk8, 11, 17 and 21)

All raw output is saved under results.

Other source files:

  • PBResolveTest.java - Our launcher for ProcessBuilder

  • scenario.clj - A bb script to setup our test scenario

  • print-dirs.* - Our target programs to launch (copied to a.*) when loaded into scenario dirs

  • fake-notepad.go - A dummy for notepad.exe

  • java-version.clj - A wee script to print out java version in short form

  • compare_results.clj - Compares the ProcessBuilder test results (across shells and JDKs), they should all be the same.

  • summarize.clj - Summarizes the results in to results/summary.adoc. I pasted these results into this doc and annotated with observations.

Manual Tests

If you want to poke around here’s some tips. We assume you are starting from the resolve-test dir.

CMD Shell

Fire up CMD Shell. Alter the PATH

cd scenario
rmdir /S /Q cwd workdir on-path
mkdir cwd workdir on-path
set PATH=%CD%\on-path;%PATH%
cd cwd

Setup a scenario via:

> bb ..\..\scenario.clj set --cwd=bat,com,cmd,ps1 --workdir=exe,ps1 --on-path=exe
cwd     [bat cmd com ps1]
workdir [exe ps1]
on-path [exe]

And launch a however you see fit:

> a
exepath: Z:\lread\info-process-builder\resolve-test\scenario\cwd\a.com
workdir: Z:\lread\info-process-builder\resolve-test\scenario\cwd
> a.exe
exepath: Z:\lread\info-process-builder\resolve-test\scenario\on-path\a.exe
workdir: Z:\lread\info-process-builder\resolve-test\scenario\cwd

PowerShell

Fire up a PowerShell.

cd scenario
mkdir cwd
$env:PATH = "$PWD\on-path;$env:PATH"
cd cwd

Run scenarios and tests as described in CMD Shell above.

ProcessBuilder

Recompile java sources:

javac PBResolveTest.java

Setup your PATH as directed above for CMD Shell or PowerShell above.

Setup scenario as described in CMD Shell above.

Run ProcessBuilder tests from scenario\cwd like so:

> java -cp ..\.. PBResolveTest a
cwd: Z:\lread\info-process-builder\resolve-test\scenario\cwd
args: [a]

running: a dir: null
exepath: Z:\lread\info-process-builder\resolve-test\scenario\on-path\a.exe
workdir: Z:\lread\info-process-builder\resolve-test\scenario\cwd

> java -c ..\.. PBResolveTest a.exe
cwd: Z:\lread\info-process-builder\resolve-test\scenario\cwd
args: [a.exe]

running: a.exe dir: null
exepath: Z:\lread\info-process-builder\resolve-test\scenario\on-path\a.exe
workdir: Z:\lread\info-process-builder\resolve-test\scenario\cwd

Optionally specify a workdir as the 2nd arg:

>java -cp ..\.. PBResolveTest a.exe ..\workdir
cwd: Z:\lread\info-process-builder\resolve-test\scenario\cwd
args: [a.exe, ..\workdir]

running: a.exe dir: ..\workdir
exepath: Z:\lread\info-process-builder\resolve-test\scenario\on-path\a.exe
workdir: Z:\lread\info-process-builder\resolve-test\scenario\workdir

About

Just me trying to understand how ProcessBuilder works - mostly looking at cmd line escape rules for Windows

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published