How to add a directory to your PATH

How to add a directory to your PATH

Julia Evans
@b0rk oh god the task sounded much easier earlier today when you mentioned the topic :)
@b0rk Wow, I love it when some seemingly simple task is given such thorough treatment. Thanks for sharing!
@b0rk FYI csh and tcsh are very explicitly not POSIX compatible shells. Csh was probably the first "alternative shell" after the original Bourne shell and it is incompatibly different on many points by design. The POSIX compatible instructions will not work.
@wfk thanks, will delete that from the post. i should know better than to speculate that by now haha
@b0rk one "fun" gotcha in bash is sometimes you have to run hash -r to clear a cache after updating PATH.
@nelson I really wanted to mention that but I tried to reproduce it and couldn't, do you know how to repro that issue?

@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.

@b0rk @nelson I have not had to rehash for a long time now. I assumed this was a legacy shell thing.

@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.

@b0rk

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 interesting it works fairly consistently here... Admittedly I only do zsh and bash, on Linux
@xexaxo @b0rk at least in the case of bash, $SHELL is only set at start-up if it's not already set in the environment, which would make it unreliable for nested shells.
@xexaxo huh, i came here to suggest the same thing, why isn't it reliable? it exists but can get wrong then it might be worth noting that on the instructions
@b0rk Thanks for this. One one of my platforms for the ps portion I get: ps -p $$ -o pid,comm=
ps: invalid option -- 'p'
BusyBox v1.24.1 (2025-01-08 02:04:44 CST) multi-call binary.

@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"

@zwol interesting, why? I have spaces in my PATH but it seems to work fine for me without quotes, what are you seeing?

@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 Hm. I don't want to be paternalistic. Maybe I shouldn't phrase this advice in terms of bad habits? In my head it's about suggesting that people can make their lives easier by not bothering to remember something. Do you think that's a less obnoxious way to put it?
@zwol I mean if that were something I did I might say something like "I like to quote $PATH even though it's not strictly necessary because it gets me in the habit of remembering to quote shell variables and it helps me avoid making quoting mistakes in my shell scripts”
@zwol (my approach to “not forgetting to quote variables in shell scripts" is to always automatically run every shell script I type through `shellcheck`, which is I guess a different way of achieving the same thing and works better for me personally)
@b0rk shellcheck is great but unfortunately it cannot be used on autoconf scripts (all the problems could be fixed but no one has the energy)
@zwol that that makes a lot of sense! honestly the reason I love hearing why people have specific preferences is that everyone's workflow/restrictions are different (like "I write autoconf scripts so I can't use shellcheck") and it's fun to hear how people end up in different places as a result
@b0rk What I tend to do (example for `ghcup`):
```
# Settings for ghcup:
# ===================
if [ -d "${HOME}/.ghcup/bin" ]
then
case ":${PATH}:" in
*:${HOME}/.ghcup/bin:*)
;;
*)
export PATH=${HOME}/.ghcup/bin${PATH:+:${PATH}}
;;
esac
fi
```
The `if` ensures that the thing is installed (on the one hand to still use the same `.bashrc` on machines with and without the thing, on the other hand in case I deinstall again and forget it).

@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.

@b0rk

> 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 OK, I was expecting *some* formatting to work... I guess I was wrong 😅

@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.

@b0rk do you mind if I share my favourite $PATH deduplication trick?
@gnomon @b0rk is this the cursed one involving little-known readline shortcuts

@nev @b0rk SPOILERS NEV

(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)

Overwriting a environment variable in linux

I wish to add a path to the PATH variable. I have copied the $PATH variable into a file called t1 using echo echo $PATH > t1 I then edited t1 nano t1 I added my path /usr/local/batch: to the

Stack Overflow

@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

@gnomon thanks! what's a reason that you re-source you ~/.bashrc? (i guess when updating a config setting?)

@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 no that makes sense! it's a good reminder that I keep forgetting how history works in bash and that those details matter

@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)

How to add a directory to your PATH

How to add a directory to your PATH

Julia Evans
@b0rk I don't think "wishy-washy" is being entirely fair to yourself there, you're very wisely stepping wide of a LARGE kettle of fish there

@gnomon @b0rk

<-- restarts shell session again

@gnomon I think I have no words at that. But then I use an ancient C program to do more or less the same thing and more (it looks to see if the directories actually exist and as a side effect sees through symlinks to list only one copy of the underlying thing). Maybe I should write up the background of it for the blog someday.
@cks please, I'd love to read that!
@gnomon Good news: this is going to be *two* entries, because the background does not fit in a single sensible-sized entry. If you guessed that it involves the fun of ancient Unix days, you are correct.
@cks I love your posts, man. Thank you for writing them.

@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
#...
)

@JamesWidman is this a problem you've run into? curious to hear about how it manifested if so
@b0rk well... i don't recall any symptoms from the past decade-ish that i've been doing that, so... it's probably ok...?
@b0rk I don't think "This will add the directory to your PATH, and automatically update all running fish shells with the new PATH." is correct (anymore?) – unless you pass `-U/--universal` to the call. I use fish_add_path everywhere and it doesn't behave like that at all and it's also not in .config/fish/fish_variables – maybe that changed at some point?

@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

Add fish_add_path, a simple way to add to $PATH by faho · Pull Request #7028 · fish-shell/fish-shell

Description fish_add_path [paths...] fish_add_path (-h | --help) fish_add_path [(-g | --global) | (-U | --universal) | (-P | --path)] [(-m | --move)] [(-a | --append) | (-p | --prepend)] ...

GitHub

@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]

@hynek thanks so much, I updated the post to say something like "fish_add_path can do two different things depending on the situation and so I find it confusing”