fish-shell icon indicating copy to clipboard operation
fish-shell copied to clipboard

`__fish_macos_set_env` does not handle lines with colons

Open injust opened this issue 1 year ago • 15 comments

Using: fish 3.7.1 macOS Sonoma 14.5 Ghostty terminal


Please help me understand what https://github.com/fish-shell/fish-shell/blob/master/share/config.fish#L190-L192 is doing, as its behaviour seems incorrect to me.

I use Ghostty terminal, which appends itself to MANPATH. fish inherits MANPATH as:

:/Applications/Ghostty.app/Contents/Resources/ghostty/../man

fish then runs https://github.com/fish-shell/fish-shell/blob/master/share/config.fish#L190-L192, which sets MANPATH to:

/usr/share/man:/usr/local/share/man::/Applications/Ghostty.app/Contents/Resources/ghostty/../man

That is, fish prepends the paths from /etc/manpaths to MANPATH. (My /etc/manpaths.d/ is empty.)

man manpath says:

IMPLEMENTATION NOTES
     The manpath utility constructs the manual path from two sources:
     1.   From each component of the user's PATH for the first of:
          -   pathname/man
          -   pathname/MAN
          -   If pathname ends with /bin: pathname/../share/man and pathname/../man
     2.   The configuration files listed in the FILES section for MANPATH entries.
     The information from these locations is then concatenated together.

     If the -L flag is set, the manpath utility will search the configuration files listed in the FILES section for MANLOCALE entries.

My default /etc/man.conf already contains everything in /etc/manpaths (i.e. /usr/share/man and /usr/local/share/man), so fish is prepending paths that already exist in man's search path.

Effectively, fish is moving those two paths from 2) and putting them before 1), but only if MANPATH is non-empty. This means that the system man pages in these 2 paths override anything in your PATH. Why is fish doing this?


For context, some Homebrew packages shadow macOS built-ins, so Homebrew installs them under alternate names (e.g. gsed if you install gnu-sed). If you add /usr/local/opt/gnu-sed/libexec/gnubin to your PATH, then sed will run GNU sed, and man sed should show the man page for GNU sed. This doesn't work in fish, and I think this is why.

injust avatar Aug 26 '24 05:08 injust

As the comment says: This is what macOS' path_helper does in login shells, so we do the same thing.

It seems weird to me that Ghosttty would set $MANPATH just to its own path, in systems I know this would override the system default manpath.

faho avatar Aug 26 '24 05:08 faho

It seems weird to me that Ghosttty would set $MANPATH just to its own path, in systems I know this would override the system default manpath.

@faho I missed a : in my original post (now edited). Ghostty appends itself to MANPATH.

injust avatar Aug 26 '24 05:08 injust

As the comment says: This is what macOS' path_helper does in login shells, so we do the same thing.

__fish_macos_set_env leaves appended values as appended, but I noticed that path_helper converts appended values to prepended values. You can notice this in zsh, for example, which runs path_helper on macOS.

@faho Is __fish_macos_set_env supposed to mirror path_helper's behaviour exactly? I'm not sure if this difference is intentional or not.

injust avatar Aug 26 '24 23:08 injust

__fish_macos_set_env leaves appended values as appended, but I noticed that path_helper converts appended values to prepended values

The source we have as well as this github mirror appends.

Our code seems to be a fairly straightforward translation of that - read /etc/manpath, read files in /etc/manpaths.d/, read $MANPATH, append each directory not already included.

If you're saying that doesn't happen, I'm going to need some more information about that, because given the code I don't see a difference.

faho avatar Aug 27 '24 09:08 faho

If I'm reading the path_helper source correctly, append_path_segment drops empty segments. So when processing Ghostty's :/Applications/Ghostty.app/Contents/Resources/ghostty/../man, the leading : is effectively dropped.

So it appends /usr/share/man, /usr/local/share/man, and then /Applications/Ghostty.app/Contents/Resources/ghostty/../man, resulting in:

/usr/share/man:/usr/local/share/man:/Applications/Ghostty.app/Contents/Resources/ghostty/../man:

This is what I meant by path_helper converting appended values to prepended values -- the order is preserved, but Ghostty's path gets moved before the trailing : (I'm actually not sure where the trailing : is being added).


Because fish processes the empty segments, you get (notice the :: in the middle and no trailing :):

/usr/share/man:/usr/local/share/man::/Applications/Ghostty.app/Contents/Resources/ghostty/../man

This honestly makes more sense to me, but it just doesn't match path_helper.

I haven't tested, but there should be a similar discrepancy if $MANPATH contains a :: in the middle.

injust avatar Aug 30 '24 01:08 injust

Our code also drops empty segments:

https://github.com/fish-shell/fish-shell/blob/325b51aca0974bd5409c6007e8e98b49a68f03c8/share/config.fish#L172-L173

faho avatar Sep 05 '24 20:09 faho

Oh, hmm. I have no idea then. Somehow I am getting

/usr/share/man:/usr/local/share/man::/Applications/Ghostty.app/Contents/Resources/ghostty/../man

from fish, and I don't know why. zsh (which I believe actually runs path_helper) gives me

/usr/share/man:/usr/local/share/man:/Applications/Ghostty.app/Contents/Resources/ghostty/../man:

injust avatar Sep 05 '24 20:09 injust

Hold on, the entry is ":/Applications/Ghostty.app/Contents/Resources/ghostty/../man".

That's gonna be read as one line, and then it's gonna be added as one line, then fish's "path variable" magic turns it into two entries.

We expect the path files to have one path per line instead of the awkward ":" gunk, and this breaks that assumption.

I'm going to reopen this for someone on macos to write a patch that handles ":" in path lines because I can't test it.

faho avatar Sep 05 '24 20:09 faho

I would like to take a jab at this if it’s unclaimed. I encountered this issue today when trying ghostty too. If it’s game, can I have a little guidance on where to start?

westernwontons avatar Dec 28 '24 09:12 westernwontons

Sure. In macos_set_env: https://github.com/fish-shell/fish-shell/blob/a579abb81bf883486f8d89181ce99ba291d84893/share/config.fish#L170-L175

You probably want to string split : before you check if the entry is already included.

faho avatar Dec 28 '24 09:12 faho

Cheers!

westernwontons avatar Dec 28 '24 10:12 westernwontons

This seems to work with Ghostty, although I'm not really confident about the nested while loops:

while read -l entry
    string split : $entry | while read -l split_entry
        if not contains -- $split_entry $result
            test -n "$split_entry"
            and set -a result $split_entry
        end
    end
end <$path_file

I tried passing -d : to while, but that wasn't doing what I expected it to do. With the nested while, all leading/trailing colons are removed.

Funny thing tho that I noticed is that the behaviour between fish and zsh are inverted in my case compared to how @injust is experiencing it. Starting Ghostty with command set to zsh --login -interactive gives me:

$ echo $MANPATH
/Users/redacted/.zi/polaris/man::/usr/share/man:/usr/local/share/man:/Applications/Ghostty.app/Contents/Resources/ghostty/../man

while for fish, I get the following:

$ echo $MANPATH
/usr/share/man /usr/local/share/man  /Applications/Ghostty.app/Contents/Resources/ghostty/../man

This revelation also made me find out that I was using zsh in Ghostty and not fish as I thought I was.

westernwontons avatar Dec 28 '24 13:12 westernwontons

although I'm not really confident about the nested while loops:

You don't need them if you redirect to string:

string split : <$path_file | while read

or use a for-loop instead:

for entry in (string split : <$path_file)

faho avatar Dec 28 '24 14:12 faho

That for-loop sits better with my eyes imo. Here's the final version:

for path_file in $argv[2] $argv[3]/*
    if test -f $path_file
        for entry in (string split : <$path_file)
            if not contains -- $entry $result
                test -n "$entry"
                and set -a result $entry
            end
        end
    end
end

westernwontons avatar Dec 28 '24 15:12 westernwontons

I'm using fish 4.0.0 now, which includes https://github.com/fish-shell/fish-shell/commit/95d61ea0fbd90ed01b6ea39a790ca0d1b6a14689 and so this should be fixed.

But the MANPATH behaviour hasn't changed and I'm still getting the same MANPATH as https://github.com/fish-shell/fish-shell/issues/10684#issuecomment-2564331523.

@westernwontons Is this fixed for you in Ghostty? I'm not sure if I've done something wrong elsewhere.

injust avatar Mar 02 '25 21:03 injust

I'm using fish 4.0.0 now, which includes https://github.com/fish-shell/fish-shell/commit/95d61ea0fbd90ed01b6ea39a790ca0d1b6a14689

It doesn't. It appears we never committed it to the 4.0 branch.

Picking it for 4.0.1 now.

faho avatar Mar 02 '25 21:03 faho

But the MANPATH behaviour hasn't changed and I'm still getting the same MANPATH as #10684 (comment).

I'm using fish 4.0.2 (which hopefully includes the fix) but my MANPATH still doesn't seem correct.

injust avatar Jun 10 '25 04:06 injust

can you insert some debugging statements around __fish_macos_set_env in share/config.fish (check inputs, outputs etc.)

krobelus avatar Jun 10 '25 07:06 krobelus

On fish 4.1.2 now, with this added to the beginning and end of __fish_macos_set_env:

if test "$argv[1]" = MANPATH
    echo \$MANPATH = "$MANPATH"
end

outputs:

$MANPATH = :/Applications/Ghostty.app/Contents/Resources/ghostty/../man
$MANPATH = /usr/share/man:/usr/local/share/man::/Applications/Ghostty.app/Contents/Resources/ghostty/../man

With completely unconfigured zsh, I get:

$MANPATH = /usr/share/man:/usr/local/share/man:/Applications/Ghostty.app/Contents/Resources/ghostty/../man:

injust avatar Oct 23 '25 15:10 injust

With completely unconfigured zsh, I get:

Confirmed that I get the same result when running path_helper from fish

> MANPATH=:/Applications/Ghostty.app/Contents/Resources/ghostty/../man /usr/libexec/path_helper | grep MANPATH
MANPATH="/usr/share/man:/usr/local/share/man:/Applications/Ghostty.app/Contents/Resources/ghostty/../man:"; export MANPATH;

So the only difference is that we're putting the second colon first instead of last

krobelus avatar Oct 23 '25 16:10 krobelus

ok I think I'm close to a fix for the extra middle colon, but where does the extra colon at the end come from? https://raw.githubusercontent.com/apple-opensource-mirror/shell_cmds/4ea11424b80a117ad1e401867b1453bb2362feb3/path_helper/path_helper.c doesn't seem to set it

Given MANPATH=:/my/humble/manpath, the last paragraph of construct_path should execute as

result = /usr/share/man:/usr/local/share/man str = :/my/humble/manpath str = \0/my/humble/manpath append_path_segment(&result, str); // no-op str = /my/humble/manpath append_path_segment(&result, str); result = /usr/share/man:/usr/local/share/man:/my/humble/manpath

krobelus avatar Oct 23 '25 16:10 krobelus

ok I think I'm close to a fix for the extra middle colon, but where does the extra colon at the end come from? apple-opensource-mirror/shell_cmds@4ea1142/path_helper/path_helper.c (raw) doesn't seem to set it

I've been wondering the exact same thing...

injust avatar Oct 23 '25 16:10 injust

When I compile the code myself, the output is different from the path_helper that ships with macOS...

$ MANPATH= ./a.out | grep MANPATH
MANPATH="/usr/share/man:/usr/local/share/man"; export MANPATH;

$ MANPATH= /usr/libexec/path_helper | grep MANPATH
MANPATH="/usr/share/man:/usr/local/share/man:"; export MANPATH;

injust avatar Oct 23 '25 16:10 injust

Seems like https://raw.githubusercontent.com/apple-opensource-mirror/shell_cmds/4ea11424b80a117ad1e401867b1453bb2362feb3/path_helper/path_helper.c is outdated.

https://github.com/apple-oss-distributions/shell_cmds/blob/main/path_helper/path_helper.c gives me behaviour matching the /usr/libexec/path_helper shipped with macOS. It explicitly adds the trailing colon in main().

injust avatar Oct 23 '25 17:10 injust

nice, that's certainly easier than trying to attach a debugger ;) Makes me wonder why we don't use path_helper directly.. We could do eval "$(/usr/libexec/path_helper -c)" since we have a compatibility wrapper for csh's setenv. But path_helper doesn't do any quoting, so quotes or dollar signs in PATH could inject code.. so I guess we either do some sanitization or stick to our reimplementation until Apple adds path_helper -f

krobelus avatar Oct 23 '25 17:10 krobelus

The more I dig into this, the more I feel like path_helper is a PITA...

It turns MANPATH=:foo (something that is appended to man's default search path) into MANPATH=/usr/share/man:/usr/local/share/man:foo: (which is now prepended to man's search path).

The prepend basically blows away anything that comes from man's default search path (which takes your $PATH into consideration).

😭

injust avatar Oct 23 '25 17:10 injust

it basically blows away anything that comes from man's default search path

Right, and surprisingly the default search path has some components interpsersed between /usr/local/share/man (which is the 1st entry below) and /usr/share/man (the 3rd), God knows why 🤷

$ manpath
/usr/local/share/man:/System/Cryptexes/App/usr/share/man:/usr/share/man:/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/share/man:/Library/Developer/CommandLineTools/usr/share/man

which takes your $PATH into consideration

not sure how

krobelus avatar Oct 23 '25 17:10 krobelus

I pushed a "fix" to the faithful-macos-manpath branch. Not sure if we should take it though, it matches macOS behavior but it seems just wrong.

fish's current behavior, i.e.

MANPATH=:/Applications/Ghostty.app/Contents/Resources/ghostty/../man fish -lc 'echo $MANPATH'
/usr/share/man:/usr/local/share/man::/Applications/Ghostty.app/Contents/Resources/ghostty/../man

seems exactly what the user intended to happen - the system default manpath is inserted before the Ghostty one. (God knows why /usr/share/man etc. must be prepended(!) explicitly rather than leaving this to the default manpath..)

krobelus avatar Oct 23 '25 17:10 krobelus

it basically blows away anything that comes from man's default search path

Right, and surprisingly the default search path has some components interpsersed between /usr/local/share/man (which is the 1st entry below) and /usr/share/man (the 3rd), God knows why 🤷

$ manpath
/usr/local/share/man:/System/Cryptexes/App/usr/share/man:/usr/share/man:/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/share/man:/Library/Developer/CommandLineTools/usr/share/man

which takes your $PATH into consideration

not sure how

The manpath man page:

IMPLEMENTATION NOTES
     The manpath utility constructs the manual path from two sources:
     1.   From each component of the user's PATH for the first of:
          -   pathname/man
          -   pathname/MAN
          -   If pathname ends with /bin: pathname/../share/man and pathname/../man
     2.   The configuration files listed in the FILES section for MANPATH entries.
     The information from these locations is then concatenated together.

     If the -L flag is set, the manpath utility will search the configuration files listed in the FILES section for MANLOCALE entries.

injust avatar Oct 23 '25 17:10 injust

I pushed a "fix" to the faithful-macos-manpath branch. Not sure if we should take it though, it matches macOS behavior but it seems just wrong.

fish's current behavior, i.e.

MANPATH=:/Applications/Ghostty.app/Contents/Resources/ghostty/../man fish -lc 'echo $MANPATH'
/usr/share/man:/usr/local/share/man::/Applications/Ghostty.app/Contents/Resources/ghostty/../man

seems exactly what the user intended to happen - the system default manpath is inserted before the Ghostty one. (God knows why /usr/share/man etc. must be prepended(!) explicitly rather than leaving this to the default manpath..)

Agreed, it seems incorrect that path_helper just discards empty segments when parsing the existing $MANPATH.

injust avatar Oct 23 '25 17:10 injust