Skip to content

Look a bare executable name up on the overlay PATH [patch] - #137

Merged
matt-edmondson merged 4 commits into
mainfrom
fix/122-resolve-on-overlay-path
Nov 4, 2026
Merged

matt-edmondson merged 4 commits into
mainfrom
fix/122-resolve-on-overlay-path

Conversation

@matt-edmondson

Copy link
Copy Markdown
Contributor

Fixes #122

What changed

With UseShellExecute = false, Process.Start searches the calling process's PATH for a bare FileName, not the PATH in ProcessStartInfo.Environment. An overlay that set PATH to pin a toolchain either threw Win32Exception for a tool that only the overlay held, or silently started a same-named tool from the caller's PATH.

CreateStartInfo now looks a bare name up on the overlay's PATH once the overlay has been applied:

  • It applies only when the overlay sets PATH and the name has no directory part (/, plus \ or : on Windows).
  • The first matching file wins. Relative PATH entries are skipped, so the result doesn't depend on the caller's current directory.
  • On Windows the key compares case-insensitively, and a name with no extension is tried with each PATHEXT entry, taken from the command's environment, with a default of .COM;.EXE;.BAT;.CMD.
  • On Unix (net7.0+) the file needs an execute bit, as it does for a shell. Older targets accept any existing file.
  • If nothing matches, or the overlay doesn't set PATH, or it removes PATH with null, the lookup falls back to the current behaviour.

The README's Environment Variables section now describes this lookup.

Tests

The three new tests run on Unix and are inconclusive on Windows, following the existing pattern:

  • ExecuteAsyncShouldStartAToolThatOnlyTheOverlayPathHolds: without the fix this threw Win32Exception.
  • ExecuteAsyncShouldPreferTheOverlayPathOverTheCallersPath: an echo script on the overlay PATH runs instead of /usr/bin/echo.
  • ExecuteAsyncShouldSkipANonExecutableFileOnTheOverlayPath: a file without an execute bit is passed over for the next directory.

All three fail with the fix reverted and pass with it. The full suite on net10.0 (Linux) gives 67 passed, 2 skipped (the elevation tests) and 0 failed. The library builds clean on every target framework.

Coordination with #131

#131 resolves relative paths such as ./build.sh against WorkingDirectory. This PR resolves bare names on the overlay PATH. The two cases don't overlap. I placed the helpers below CreateStartInfo so that this branch and #131 merge without a textual conflict (checked with git merge-tree).


Generated by Claude Code

With UseShellExecute off, Process.Start searches the calling process's
PATH, not the PATH in ProcessStartInfo.Environment. An overlay that set
PATH to pin a toolchain either failed to start a tool only it held, or
silently started a same-named tool from the caller's PATH.

When the overlay sets PATH and the name has no directory part, the name is
now resolved against the overlay's PATH: first executable match wins,
relative entries are skipped, and PATHEXT is applied on Windows. With no
match, or no PATH in the overlay, the old lookup is kept.

Fixes #122

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017vfJW5RAKCxdwrsVmygQDP
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017vfJW5RAKCxdwrsVmygQDP
Comment thread RunCommand/RunCommand.cs
Comment on lines +416 to +422
foreach (KeyValuePair<string, string?> variable in environmentVariables)
{
if (string.Equals(variable.Key, "PATH", comparison))
{
path = variable.Value;
}
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This stays as a loop. The suggestion only changes style, and the loop already gives the "last match wins" result the alert describes. That result matters on Windows, where the overlay dictionary can hold both Path and PATH. System.Linq is currently imported only for netstandard2.0, so switching to LINQ would mean adding a using for every target, with no change in behaviour.


Generated by Claude Code

Comment thread RunCommand/RunCommand.cs
return null;
}

string candidate = Path.Combine(directory, fileName);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I traced this, and it can't happen through the current caller. ResolveOnSearchPath returns early for any name that contains /, or on Windows \ or :, and a PATHEXT entry only appends an extension. So fileName never reaches TryExecutable rooted. The guard costs one condition, so 030eae5 adds it anyway: a rooted name is now skipped instead of being combined.


Generated by Claude Code

@sonarqubecloud

sonarqubecloud Bot commented Nov 3, 2026

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Setting PATH in CommandOptions.EnvironmentVariables doesn't change which program runs: a bare fileName is still resolved on the caller's PATH

2 participants