hash -r to clear a cache after updating PATH.@b0rk I tested this some and was wrong. Setting PATH seems to clear the cache for you. But if you remove a binary from PATH then you need to run hash -r for it to find a different binary in PATH.
bash-5.1$ echo "echo haha" >| /tmp/cat; chmod +x /tmp/cat
bash-5.1$ type cat
cat is /usr/bin/cat
bash-5.1$ hash -t cat
bash: hash: cat: not found
bash-5.1$ cat /etc/issue
Ubuntu 22.04.4 LTS \n \l
bash-5.1$ hash -t cat
/usr/bin/cat
bash-5.1$ PATH=/tmp:$PATH
bash-5.1$ hash -t cat
bash: hash: cat: not found
bash-5.1$ type cat
cat is /tmp/cat
bash-5.1$ cat /etc/issue
haha
bash-5.1$ rm /tmp/cat
bash-5.1$ type cat
cat is hashed (/tmp/cat)
bash-5.1$ hash -t cat
/tmp/cat
bash-5.1$ cat /etc/issue
bash: /tmp/cat: No such file or directory
bash-5.1$ hash -r
bash-5.1$ cat /etc/issue
Ubuntu 22.04.4 LTS \n \l
@b0rk Hey, Phind found the code for me in bash that clears the cache.
/* What to do just after the PATH variable has changed. */
void
sv_path (name)
char *name;
{
/* hash -r */
phash_flush ();
}
So yeah, no need to run hash after changing PATH. But hash -r is still useful in other cases.
@lemay @b0rk @nelson The case where you used to need to do this is if you first run a command from the shell, then you change the PATH so that the same command is found in a different place, but it still exists in the original place. bash consults the hash table first. If it says that foo is found in /usr/bin/foo, then you change PATH to put /home/b0rk/bin first, and there's a foo in that directory, bash would still run /usr/bin/foo unless you reset the hash.
But I just tried this and it now appears that the hash table is cleared if PATH is changed. So I think hash -r is no longer needed.
@lemay @b0rk @nelson I take that back. There's a way to produce the issue, just confirmed.
If the PATH is not changed, you've run a command where bash found your program in, say, the 3rd directory, then you add a command of the same name in an earlier directory, bash will run the wrong command unless you do hash -r .
@lemay @b0rk @nelson Here's a reproducer, assuming ls originally finds /usr/bin/ls :
export PATH=/home/me/dummy:$PATH
ls /usr/bin/ls
touch /home/me/dummy/ls
chmod +x /home/me/dummy/ls
ls /usr/bin/ls
You need hash -r to get bash to run your new ls, because the first ls command put ls -> /usr/bin/ls in the hash table.
My "I just changed PATH (or anything else potentially breaking)" dance has always been:
0) make the change
1) open new shell in a new terminal and ssh
2) test
3) close old shell
Because it sidesteps the rehash and also handles other edge cases like password or sshd changes, and leaves you in a functional shell just in case step 2 blows up.
Learned the hard way on an old sparc with a faulty shift key.
@b0rk awesome, thank you.
Couple of suggestions, if I may:
'echo $SHELL' shout be easier than the ps combo. 'source some/shell/script' also works with zsh &bash
@xexaxo yeah echo $SHELL is easier but sadly it does not work reliably in my experience which is why i didn’t say that
i think ill change ‘.’ to source though, I like it more too
@b0rk I believe all of the bash/zsh examples should be using quoted strings on the right hand side of the assignment:
export PATH="/some/dir:$PATH"
This is not always necessary but it's much easier and safer to always do it than to remember when it isn't necessary.
Unfortunately it has the side effect of blocking expansion of ~, so those need to get replaced with $HOME:
export PATH="$HOME/.npm-global/bin:$PATH"
@b0rk I'm being defensive, not against anything that might go wrong *here*, but against people picking up bad habits.
In bash and zsh (and the entire "Bourne shell" family) it *happens* to be safe to leave the double quotes off the right-hand side of an assignment, even if there is a variable in there that expands to something with spaces inside. Word splitting won't happen. But leaving the double quotes off *any* shell expression that contains variable expansions (or any other kind of expansion) is a bad habit to be in, because in *most* contexts word splitting *will* happen. It is better, in my opinion, not to try to remember when it won't, but instead to *always* double-quote unless you specifically *want* word splitting.
@zwol oh I see! I'm unlikely to tell people that they should quote $PATH in that case
(I'm sure that trying to prevent people from "picking up bad habits" is helpful sometimes but it feels kind of paternalistic to me and it's not really the kind of advice that I give)
@b0rk The `case` prevents adding the same directory to the `$PATH` multiple times (e.g., when starting another subshell).
And the `${PATH:+:${PATH}}` only adds a `:` if the previous `$PATH` is not empty. Arguably an almost impossible edge case for `$PATH`, but I figured a good habit for all kinds of `:`-separated variables.
> Go: go env | grep GOPATH (then append /bin/)
You can also use `go env GOPATH` to directly output the value of that environment variable — which is handy because the output of `go env | grep GOPATH` is in this form:
```
GOPATH='/home/username/go'
```
Instead, the `.bashrc` could just say:
```
export PATH="${PATH}:$(go env GOPATH)/bin"
```
@b0rk this is really good. I didn't know about `which -a` - there's always something still to learn.
I don't know if it's worth adding zsh-specific things, but when I was using zsh I really liked the interactive editing of variables like path via e.g. `vared path`, or append to it with just `path+=newdir`
Being able to just edit the variable and not have to remember to include its previous value, etc. was nice.
(no, not this one)
edit: this was the cursed trick: https://mastodon.social/@gnomon/111206929341206118
@gnomon @b0rk slightly less cursed but still hacky way i found on stackoverflow (https://stackoverflow.com/q/53264235) and have been using ever since:
echo ${PATH} > t1
<editor> t1
export PATH=$(cat t1)
@gnomon yeah i'd be interested, i've never known how to do that really
i'd also be curious about how you use it
@b0rk so first the trick, then the reasons for it:
mapfile -t paths < <(<<< "$PATH" tr ':' $'\n' | awk '!seen[$0]++'); printf -v 'PATH' ':%s' "${paths[@]}"; PATH=${PATH:1}
The real trick is that the awk step deduplicates entries _without changing the order of the path elements_, which of course is crucial in this case. The printf/PATH=... step is just about reassembling the array of results back into a string with minimal opportunities for errors to creep in.
1/2
@b0rk the reason I use this is that my ~/.profile and ~/.bashrc files mix direct assignment to $PATH with appending/prepending values, in some cases because that's how external tools I call have set themselves up. This deduplication hack lets me insert synchronization points that ensure re-source'ing my ~/.bashrc is idempotent, instead of accumulating a longer and longer value which I usually fail to notice.
2/2
@b0rk exactly, yeah. I almost always re-source my bashrc instead of starting a new shell because I have come to consider the last ~100 or so entries in my shell command history part of my peripheral memory, and I hate losing that if I can avoid it. Poor reason, I know.
edit: ugggh, and I just realized upon re-reading it anew that this could have been a one-liner:
```
PATH=$(<<< $PATH awk 'BEGIN{RS=ORS=":"}; !seen[$0]++')
```
Ah well, the shorter version is even less legible.
@gnomon added a couple of more problems to the post based on this ("duplicate PATH entries making it harder to debug" and "losing your history after updating your PATH”) https://jvns.ca/blog/2025/02/13/how-to-add-a-directory-to-your-path/#problem-3-duplicate-path-entries-making-it-harder-to-debug
(though I was a bit wishy washy about how to accomplish the deduplication exactly)
@b0rk what about non-interactive shell invocations? Like, when you have a bash or zsh script file marked as executable, with the appropriate shebang at the top, and you execute it, .bashrc/.zshrc is not executed.
...but you might want to be able to use the same path when those scripts execute...?
@b0rk on 2nd thought, i'm not 100% that you do want that, since some 3rd-party scripts might assume the default path is in effect, which could lead them to invoke programs that they didn't expect if your custom $PATH shadows default binaries.
...but on the other hand, most of the packages you install with e.g. `brew` are careful to avoid shadowing names of factory-default binaries, so maybe it's ok to customize $path in ~/.zshenv (or in "$BASH_ENV" for bash)...?
@b0rk btw, for readability/comment-ability, in my zsh init files i set the 'path' array parameter rather than the PATH scalar parameter, and because path and PATH are tied to one another via `typeset -T PATH path`, setting 'path' implicitly sets PATH:
% typeset -p1 path
prints:
typeset -aT PATH path=(
/foo/bar
/baz/biz
#...
)
@hynek oh interesting `type fish_add_path` on my machine says
> By default it'll prepend the given paths to a universal $fish_user_paths
what does yours say? would love to add a note about what change
it looks like -U has been the default ever since the command was introduced: https://github.com/fish-shell/fish-shell/pull/7028/files#diff-1f319937d88e28421199e0665b819d0c65a3ebe9f7845565217f4d31ac0ea938R37
@b0rk wow so this is fun! Mine says the same thing, but there's also a bunch of logic to decide whether to use global or universal variables!
The most important part first: if you pass `-g`, you're fine either way, so that would be worth a note I think path `fish_add_path --prepend --global foo` is def more idiomatic & less error-prone than futzing with $PATH. [1/2]
@b0rk As for defaults: it won't change scope if an variable names fish_user_paths already exists and I've added an `echo $fish_user_paths` at the top of my config.fish and I've already got three paths in there (2x Nix & official Python).
IOW the default behavior depends on the software you've got installed. *sad trombone*
[2/2]