Reading List
The most recent articles from a list of feeds I subscribe to.
How to add a directory to your PATH
I was talking to a friend about how to add a directory to your PATH today. It’s
something that feels “obvious” to me since I’ve been using the terminal for a
long time, but when I searched for instructions for how to do it, I actually
couldn’t find something that explained all of the steps – a lot of them just
said “add this to ~/.bashrc”, but what if you’re not using bash? What if your
bash config is actually in a different file? And how are you supposed to figure
out which directory to add anyway?
So I wanted to try to write down some more complete directions and mention some of the gotchas I’ve run into over the years.
Here’s a table of contents:
- step 1: what shell are you using?
- step 2: find your shell’s config file
- step 3: figure out which directory to add
- step 4: edit your shell config
- step 5: restart your shell
- problems:
- notes:
step 1: what shell are you using?
If you’re not sure what shell you’re using, here’s a way to find out. Run this:
ps -p $$ -o pid,comm=
- if you’re using bash, it’ll print out
97295 bash - if you’re using zsh, it’ll print out
97295 zsh - if you’re using fish, it’ll print out an error like “In fish, please use
$fish_pid” (
$$isn’t valid syntax in fish, but in any case the error message tells you that you’re using fish, which you probably already knew)
Also bash is the default on Linux and zsh is the default on Mac OS (as of 2024). I’ll only cover bash, zsh, and fish in these directions.
step 2: find your shell’s config file
- in zsh, it’s probably
~/.zshrc - in bash, it might be
~/.bashrc, but it’s complicated, see the note in the next section - in fish, it’s probably
~/.config/fish/config.fish(you can runecho $__fish_config_dirif you want to be 100% sure)
a note on bash’s config file
Bash has three possible config files: ~/.bashrc, ~/.bash_profile, and ~/.profile.
If you’re not sure which one your system is set up to use, I’d recommend testing this way:
- add
echo hi thereto your~/.bashrc - Restart your terminal
- If you see “hi there”, that means
~/.bashrcis being used! Hooray! - Otherwise remove it and try the same thing with
~/.bash_profile - You can also try
~/.profileif the first two options don’t work.
(there are a lot of elaborate flow charts out there that explain how bash decides which config file to use but IMO it’s not worth it to internalize them and just testing is the fastest way to be sure)
step 3: figure out which directory to add
Let’s say that you’re trying to install and run a program called http-server
and it doesn’t work, like this:
$ npm install -g http-server
$ http-server
bash: http-server: command not found
How do you find what directory http-server is in? Honestly in general this is
not that easy – often the answer is something like “it depends on how npm is
configured”. A few ideas:
- Often when setting up a new installer (like
cargo,npm,homebrew, etc), when you first set it up it’ll print out some directions about how to update your PATH. So if you’re paying attention you can get the directions then. - Sometimes installers will automatically update your shell’s config file
to update your
PATHfor you - Sometimes just Googling “where does npm install things?” will turn up the answer
- Some tools have a subcommand that tells you where they’re configured to
install things, like:
- Node/npm:
npm config get prefix(then append/bin/) - Go:
go env GOPATH(then append/bin/) - asdf:
asdf info | grep ASDF_DIR(then append/bin/and/shims/)
- Node/npm:
step 3.1: double check it’s the right directory
Once you’ve found a directory you think might be the right one, make sure it’s
actually correct! For example, I found out that on my machine, http-server is
in ~/.npm-global/bin. I can make sure that it’s the right directory by trying to
run the program http-server in that directory like this:
$ ~/.npm-global/bin/http-server
Starting up http-server, serving ./public
It worked! Now that you know what directory you need to add to your PATH,
let’s move to the next step!
step 4: edit your shell config
Now we have the 2 critical pieces of information we need:
- Which directory you’re trying to add to your PATH (like
~/.npm-global/bin/) - Where your shell’s config is (like
~/.bashrc,~/.zshrc, or~/.config/fish/config.fish)
Now what you need to add depends on your shell:
bash instructions:
Open your shell’s config file, and add a line like this:
export PATH=$PATH:~/.npm-global/bin/
(obviously replace ~/.npm-global/bin with the actual directory you’re trying to add)
zsh instructions:
You can do the same thing as in bash, but zsh also has some slightly fancier syntax you can use if you prefer:
path=(
$path
~/.npm-global/bin
)
fish instructions:
In fish, the syntax is different:
set PATH $PATH ~/.npm-global/bin
(in fish you can also use fish_add_path, some notes on that further down)
step 5: restart your shell
Now, an extremely important step: updating your shell’s config won’t take effect if you don’t restart it!
Two ways to do this:
- open a new terminal (or terminal tab), and maybe close the old one so you don’t get confused
- Run
bashto start a new shell (orzshif you’re using zsh, orfishif you’re using fish)
I’ve found that both of these usually work fine.
And you should be done! Try running the program you were trying to run and hopefully it works now.
If not, here are a couple of problems that you might run into:
problem 1: it ran the wrong program
If the wrong version of a program is running, you might need to add the directory to the beginning of your PATH instead of the end.
For example, on my system I have two versions of python3 installed, which I
can see by running which -a:
$ which -a python3
/usr/bin/python3
/opt/homebrew/bin/python3
The one your shell will use is the first one listed.
If you want to use the Homebrew version, you need to add that directory
(/opt/homebrew/bin) to the beginning of your PATH instead, by putting this in
your shell’s config file (it’s /opt/homebrew/bin/:$PATH instead of the usual $PATH:/opt/homebrew/bin/)
export PATH=/opt/homebrew/bin/:$PATH
or in fish:
set PATH ~/.cargo/bin $PATH
problem 2: the program isn’t being run from your shell
All of these directions only work if you’re running the program from your shell. If you’re running the program from an IDE, from a GUI, in a cron job, or some other way, you’ll need to add the directory to your PATH in a different way, and the exact details might depend on the situation.
in a cron job
Some options:
- use the full path to the program you’re running, like
/home/bork/bin/my-program - put the full PATH you want as the first line of your crontab (something like
PATH=/bin:/usr/bin:/usr/local/bin:….). You can get the full PATH you’re
using in your shell by running
echo "PATH=$PATH".
I’m honestly not sure how to handle it in an IDE/GUI because I haven’t run into that in a long time, will add directions here if someone points me in the right direction.
problem 3: duplicate PATH entries making it harder to debug
If you edit your path and start a new shell by running bash (or zsh, or
fish), you’ll often end up with duplicate PATH entries, because the shell
keeps adding new things to your PATH every time you start your shell.
Personally I don’t think I’ve run into a situation where this kind of
duplication breaks anything, but the duplicates can make it harder to debug
what’s going on with your PATH if you’re trying to understand its contents.
Some ways you could deal with this:
- If you’re debugging your
PATH, open a new terminal to do it in so you get a “fresh” state. This should avoid the duplication. - Deduplicate your
PATHat the end of your shell’s config (for example in zsh apparently you can do this withtypeset -U path) - Check that the directory isn’t already in your
PATHwhen adding it (for example in fish I believe you can do this withfish_add_path --path /some/directory)
How to deduplicate your PATH is shell-specific and there isn’t always a
built in way to do it so you’ll need to look up how to accomplish it in your
shell.
problem 4: losing your history after updating your PATH
Here’s a situation that’s easy to get into in bash or zsh:
- Run a command (it fails)
- Update your
PATH - Run
bashto reload your config - Press the up arrow a couple of times to rerun the failed command (or open a new terminal)
- The failed command isn’t in your history! Why not?
This happens because in bash, by default, history is not saved until you exit the shell.
Some options for fixing this:
- Instead of running
bashto reload your config, runsource ~/.bashrc(orsource ~/.zshrcin zsh). This will reload the config inside your current session. - Configure your shell to continuously save your history instead of only saving the history when the shell exits. (How to do this depends on whether you’re using bash or zsh, the history options in zsh are a bit complicated and I’m not exactly sure what the best way is)
a note on source
When you install cargo (Rust’s installer) for the first time, it gives you
these instructions for how to set up your PATH, which don’t mention a specific
directory at all.
This is usually done by running one of the following (note the leading DOT):
. "$HOME/.cargo/env" # For sh/bash/zsh/ash/dash/pdksh
source "$HOME/.cargo/env.fish" # For fish
The idea is that you add that line to your shell’s config, and their script
automatically sets up your PATH (and potentially other things) for you.
This is pretty common (for example Homebrew suggests you eval brew shellenv), and there are
two ways to approach this:
- Just do what the tool suggests (like adding
. "$HOME/.cargo/env"to your shell’s config) - Figure out which directories the script they’re telling you to run would add
to your PATH, and then add those manually. Here’s how I’d do that:
- Run
. "$HOME/.cargo/env"in my shell (or the fish version if using fish) - Run
echo "$PATH" | tr ':' '\n' | grep cargoto figure out which directories it added - See that it says
/Users/bork/.cargo/binand shorten that to~/.cargo/bin - Add the directory
~/.cargo/binto PATH (with the directions in this post)
- Run
I don’t think there’s anything wrong with doing what the tool suggests (it might be the “best way”!), but personally I usually use the second approach because I prefer knowing exactly what configuration I’m changing.
a note on fish_add_path
fish has a handy function called fish_add_path that you can run to add a directory to your PATH like this:
fish_add_path /some/directory
This is cool (it’s such a simple command!) but I’ve stopped using it for a couple of reasons:
- Sometimes
fish_add_pathwill update thePATHfor every session in the future (with a “universal variable”) and sometimes it will update thePATHjust for the current session and it’s hard for me to tell which one it will do. In theory the docs explain this but I could not understand them. - If you ever need to remove the directory from your
PATHa few weeks or months later because maybe you made a mistake, it’s kind of hard to do (there are instructions in this comments of this github issue though).
that’s all
Hopefully this will help some people. Let me know (on Mastodon or Bluesky) if you there are other major gotchas that have tripped you up when adding a directory to your PATH, or if you have questions about this post!
My Zig Configuration for VS Code
I finally found a solution that makes VS Code work consistently with Zig, so I’m sharing my setup in the hope that it saves someone else a headache.
Zig extension for VS Code working correctly
Before I landed on a working solution, I kept running into issues with Zig version mismatches or VS Code completely failing to recognize Zig semantics and failing over to naive autocomplete.
Style-observer: JS to observe CSS property changes, for reals
I cannot count the number of times in my career I wished I could run JS in response to CSS property changes, regardless of what triggered them: media queries, user actions, or even other JS.
Use cases abound. Here are some of mine:
- Implement higher level custom properties in components, where one custom property changes multiple others in nontrivial ways (e.g. a
--variant: dangerthat sets 10 color tokens). - Polyfill missing CSS features
- Change certain HTML attributes via CSS (hello
--aria-expanded!) - Set CSS properties based on other CSS properties without having to mirror them as custom properties
The most recent time I needed this was to prototype an idea I had for Web Awesome, and I decided this was it: I’d either find a good, bulletproof solution, or I would build it myself.
Spoiler alert: Oops, I did it again
A Brief History of Style Observers
The quest for a JS style observer has been long and torturous. Many have tried to slay this particular dragon, each getting us a little bit closer.
The earliest attempts relied on polling, and thus were also prohibitively slow.
Notable examples were ComputedStyleObserver by Keith Clark in 2018
and StyleObserver by PixelsCommander in 2019.
Jane Ori first asked “Can we do better than polling?” with her css-var-listener in 2019. It parsed the selectors of relevant CSS rules, and used a combination of observers and event listeners to detect changes to the matched elements.
Artem Godin was the first to try using transition events such as transitionstart to detect changes, with his css-variable-observer in 2020.
In fact, for CSS properties that are animatable, such as color or font-size, using transition events is already enough.
But what about the rest, especially custom properties which are probably the top use case?
In addition to pioneering transition events for this purpose, Artem also concocted a brilliant hack to detect changes to custom properties:
he stuffed them into font-variation-settings, which is animatable regardless of whether the axes specified corresponded to any real axes in any actual variable font, and then listened to transitions on that property.
It was brilliant, but also quite limited: it only supported observing changes to custom properties whose values were numbers (otherwise they would make font-variation-settings invalid).
The next breakthrough came four years later, when Bramus Van Damme pioneered a way to do it “properly”, using the (then) newly Baseline transition-behavior: allow-discrete after an idea by Jake Archibald.
His @bramus/style-observer was the closest we’ve ever gotten to a “proper” general solution.
Releasing his work as open source was already a great service to the community, but he didn’t stop there. He stumbled on a ton of browser bugs, which he did an incredible job of documenting and then filing. His conclusion was:
Right now, the only cross-browser way to observe Custom Properties with @bramus/style-observer is to register the property with a syntax of “
<custom-ident>”. Note that<custom-ident>values can not start with a number, so you can’t use this type to store numeric values.
Wait, what? That was still quite the limitation!
My brain started racing with ideas for how to improve on this. What if, instead of trying to work around all of these bugs at once, we detect them so we only have to work around the ones that are actually present?
World, meet style-observer
At first I considered just sending a bunch of PRs, but I wanted to iterate fast, and change too many things.
I took the fact that the domain observe.style was available as a sign from the universe, and decided the time had come for me to take my own crack at this age-old problem, armed with the knowledge of those who came before me and with the help of my trusty apprentice Dmitry Sharabin (hiring him to work full-time on our open source projects is a whole separate blog post).
One of the core ways style-observer achieves better browser support is that
it performs feature detection for many of the bugs Bramus identified.
This way, code can work around them in a targeted way, rather than the same code having to tiptoe around all possible bugs.
As a result, it basically works in every browser that supports transition-behavior: allow-discrete,
i.e. 90% globally.

- Safari transition loop bug (#279012):
StyleObserverdetects this and works around it by debouncing. - Chrome unregistered transition bug (#360159391):
StyleObserverdetects this bug and works around it by registering the property, if unregistered. - Firefox no initial
transitionstartbug (#1916214): By design,StyleObserverdoes not fire its callback immediately (i.e. works more likeMutationObserverthan likeResizeObserver). In browsers that do fire an initialtransitionstartevent, it is ignored. - In addition, while working on this, we found a couple more bugs.
Additionally, besides browser support, this supports throttling, aggregation, and plays more nicely with existing transitions.
Since this came out of a real need, to (potentially) ship in a real product, it has been exhaustively tested, and comes with a testsuite of > 150 unit tests (thanks to Dmitry’s hard work).
If you want to contribute, one area we could use help with is benchmarking.
That’s all for now! Try it out and let us know what you think!
Gotta end with a call to action, amirite?
- Docs: observe.style
- Repo: leaverou/style-observer
- NPM: style-observer
Refactoring English: Month 2
Highlights
- I’m having doubts about sitting out the AI revolution.
- I should prove to myself that customers are willing to buy my book before investing more time into it.
- I’m probably the last person on the planet to discover that RSS is a great way to read blogs.
Goal grades
At the start of each month, I declare what I’d like to accomplish. Here’s how I did against those goals:
Install NixOS on a Free Oracle Cloud VM
Oracle is not a very popular cloud hosting service, but they have an unusually attractive free tier offering. You can run the following two VMs for free 24/7:
- 4 CPU / 24 GB RAM Ampere A1 ARM VM
- 1 CPU / 1 GB RAM AMD CPU
The AMD one is not that exciting, but a 4-CPU / 24 GB system is more powerful than you’ll find in the free tier of any other cloud vendor.