8000 Fix $0 for scripts resolved through PATH by theduardomaciel · Pull Request #4 · hex2f/gash · GitHub
[go: up one dir, main page]

Skip to content

Fix $0 for scripts resolved through PATH - #4

Open
theduardomaciel wants to merge 1 commit into
hex2f:mainfrom
theduardomaciel:fix/script-arg0
Open

theduardomaciel wants to merge 1 commit into
hex2f:mainfrom
theduardomaciel:fix/script-arg0

Conversation

@theduardomaciel
Copy link
Copy Markdown

Problem

When gash runs a shell script found through PATH, it sets $0 to the bare command name instead of the resolved path:

# /tmp/hello
#!/bin/sh
echo "$0"
PATH=/tmp:$PATH
hello
# gash:     hello
# bash:     /tmp/hello

This breaks generated launchers that locate files relative to $0. Concretely, pnpm's bin/pnpm does:

link="$0"
case "$link" in
  */*) ;;
  *) link="./$link" ;;   # gash takes this branch
esac
...
exec "$basedir/../global/v11/<hash>/node_modules/@pnpm/exe/pnpm.exe" "$@"

so pnpm <cmd> fails with:

pnpm: line 57: ./../global/v11/<hash>/node_modules/@pnpm/exe/pnpm.exe: command not found

Fix

Match bash semantics:

  • command found via PATH -> $0 is the resolved path
  • command containing a path separator -> keep the word as typed
  • pass the POSIX path when running the script through an external child shell

Testing

  • the reproducer above now prints the full path
  • pnpm --version works with pnpm's stock, unmodified shim
  • binary built with the project's release workflow (no local Rust toolchain in this environment)

@hex2f
hex2f commented Nov 6, 2026
Copy link
Copy Markdown
Owner

Hello! Happy to review and potentially merge this. But first, could you please attribute your git commits to which model (if any) were used.

Bash sets $0 to the full path when a command is found via PATH, and keeps
the word as typed when it contains a separator. Generated launchers such as
pnpm's bin/pnpm use $0 to locate their target, so a bare $0 makes them fail
with a relative path.

Use the resolved POSIX path for PATH lookups, and pass the POSIX form when
running a script through an external child shell.

Co-authored-by: DeepSeek V4.1 Flash <noreply@opencode.ai>
@theduardomaciel
theduardomaciel commented Nov 6, 2026 •
Copy link
Copy Markdown
Author

Just did it!

I've used DeepSeek V4.1 Flash to test Gash locally, since I've been having some issues with Git Bash recently.
I got really excited when I came across your project, so I decided to give it a try and was genuinely impressed by the performance boost! 🙏

I ran into a few issues along the way, particularly when using tools like pnpm, which this PR focuses on fixing. I also opened another PR with a small set of additional fixes related to process termination.

Anyway, congratulations on the project, and keep up the great work!

oh, one more note: I've updated the commits to include the model name. However, I was using the OpenCode harness, so if you'd prefer to have it listed as the co-author, just let me know!

10000
@hex2f hex2f left a comment
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Thanks for this. The PATH-lookup fix is correct and matches bash. Line comments below.

Missing regression test: no test covers this change, and no CI ran on the branch. A tests/scripts case would keep it from breaking again. For example: create a temp dir holding a small script, prepend that dir to PATH, run the script by bare name, and print case $0 in */*) echo dir;; *) echo bare;; esac. Then run it again as ./x and check that the typed form is kept. That output is the same on any machine, so it can be diffed against Git Bash.

Comment thread src/exec.rs
ExeKind::Script => {
cmd = Proc::new(&crate::global().exe);
cmd.arg(res.path.as_os_str());
cmd.arg(crate::path::to_posix(&res.path));
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

$0 depends on in-process vs spawned execution. The "keep the word as typed if it contains a separator" rule only exists in run_script_inproc. This branch always passes the resolved path. It runs for background jobs (exec_background sets spawn_only) and when GASH_NO_INPROC is set. As a result:

  • ./foo.sh gives $0 = ./foo.sh
  • ./foo.sh & gives $0 = /c/Users/.../foo.sh

Bash gives ./foo.sh in both cases. Suggestion: compute arg0 once in a small helper used by both paths, and pass that word to the child gash here. The child runs in the same cwd, so a relative ./foo.sh still resolves.

Comment thread src/exec.rs
ExeKind::Script => {
cmd = Proc::new(&crate::global().exe);
cmd.arg(res.path.as_os_str());
cmd.arg(crate::path::to_posix(&res.path));
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Lossy conversion (minor). The old code passed res.path to the child as an OsStr. to_posix goes through to_string_lossy, so a path that isn't valid UTF-16 (an unpaired surrogate is legal on NTFS) reaches the child gash with U+FFFD in it. The child then fails with "No such file or directory" (exit 127). Rare, but it's a regression from passing the path through unchanged.

Comment thread src/exec.rs
let arg0 = if has_path_sep(&argv[0]) {
argv[0].clone()
} else {
crate::path::to_posix(&res.path)
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Verbatim UNC paths come out relative. to_posix calls strip_verbatim, which turns \\?\UNC\server\share\x into server\share\x. Its comment says the caller adds back the leading \\, but to_posix never does, so the result is server/share/x, a relative path. That bug was already in to_posix, but this PR now sends its output into $0 and (at line 1513) into the script path given to the child, which then exits 127. Probably rare, but worth fixing in to_posix while you're here.

Comment thread src/exec.rs
// Bash sets $0 to the resolved path when the command came from a PATH
// lookup, and keeps the word as typed when it contains a separator.
// Generated launchers (pnpm, corepack, ...) rely on $0 having a directory. AACA
let arg0 = if has_path_sep(&argv[0]) {
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Relative or empty PATH entries (nit). find_in_path turns relative or empty PATH entries into absolute paths, so $0 here ends up absolute where bash keeps the form from PATH:

  • PATH=bin:$PATH: bash gives bin/hello, gash gives /c/proj/bin/hello
  • empty entry or .: bash gives ./hello, gash gives /c/proj/./hello

Launchers still work, so this is low priority. It does go against the aim of matching bash, though.

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.

2 participants

0