The author's last post was about terminal frustrations[0]
This is easily one of the most annoying ones, especially when shells will have several different locations to read config from depending on how you start the shell, or what a desktop app will do to try and pull the config in (`exec-path-from-shell` in emacs, for example).
And you can't really, say, put it in both your `.zprofile` and `.zshrc` or `.bash_profile`, `.profile`, and `.bashrc`, because then it'll get executed more than once and you'd need to maintain some kind of state to prevent that.
> And you can't really, say, put it in both your `.zprofile` and `.zshrc` or `.bash_profile`, `.profile`, and `.bashrc`, because then it'll get executed more than once and you'd need to maintain some kind of state to prevent that.
FWIW, profiles are loaded on 'from scratch' logins; RC files are loaded all the time.
Somewhat related there are other confusing things around how environment variables are set (especially on Linux). There are environment variables I set for QHD and UHD monitors e.g. QT_SCALE_FACTOR
* If you are using Xorg, you put environment variables in ~/.xprofile
* If you are using Xorg on Debian, you put the environment variables in ~/.xsessionrc
* If you are using Wayland, you make a file in ~/.config/environment.d/ and set you configuration variables there.
At what point do you consider it an "environment variable" vs "application setting" (even when called an env var by the app)? Windows has it's explicit env vars and GUI or CLI methods to set them, but if I was changing something about Explorer or some other graphical portion of the OS, I'd be going to the registry, for example. Setting the display resolution or scaling factor would be another registry entry, though I'd be using a purpose-built GUI for that.
Well with QT_SCALE_FACTOR is goes and tells all the QT to behave in a particular way. It isn't a per application setting.
There is a similar environment variable for Steam on Linux and I would argue that it should be an application setting but for whatever reason it isn't.
Both of these are hacks around how Xorg (doesn't) handle fractional scaling.
> Well with QT_SCALE_FACTOR is goes and tells all the QT to behave in a particular way. It isn't a per application setting.
I understand that, but your window server is just another application. On Windows I wouldn't consider scale factor being an env var, and indeed it isn't set as such -- rather that value is set in the registry.
Maybe that's the answer -- Windows has a defacto method of setting system-wide env vars that every application inherits within a given personality but Linux/BSD does not.
> If you are using Wayland, you make a file in ~/.config/environment.d/ and set you configuration variables there.
This is not in any way Wayland specific, and arguably should be the default choice for setting env vars, as it should apply to all user sessions regardless of what type they are or what shell is used.
I've worked on maintaining internal dev tooling for some small companies for a while now, and it's a real PITA to write a robust installation script for bootstrapping a new laptop running an arbitrary shell on linux or macOS into a working environment. Way more work than it feels like it should be.
At this point I've pretty much given in and decided that a containerized dev environment is probably the better solution, but on principle it feels so unsatisfying to have to resort to this :(
(I know someone is going to mention Nix/Guix ;) but that feels like a giant rabbit hole)
The compact one-liners below are similar but avoid adding duplicate items to PATH, so are fine to call in various init scripts.
In Bash:
path_append() { local p; for p; do [[ :"$PATH": =~ :"$p": ]] || PATH+=:$p; done; }
path_prepend() { local p; for p; do [[ :"$PATH": =~ :"$p": ]] || PATH=$p:$PATH; done; }
In portable POSIX shell:
path_append() { for p; do case :"$PATH": in *:"$p":*) ;; *) export PATH="$PATH:$p" ;; esac; done; }
path_prepend() { for p; do case :"$PATH": in *:"$p":*) ;; *) export PATH="$p:$PATH" ;; esac; done; }
Do any of these guard against an empty value on either side ?
"export PATH=$DIR:$PATH -
That particular pattern is way too common, and is very dangerous if
you consider the case when [$DIR or] $PATH (or whatever your variable is, like
$LD_LIBRARY_PATH) isn’t set. Then, the value will be :/path/to/dir,
which usually means both /path/to/dir and the current directory,
which is usually both unexpected behaviour and a security concern."
I'm surprised both the blog post and all the other comments don't mention how it should have logic to check if the item exists in the path before adding it.
Otherwise you get duplicates added everytime you source your config.
Another option is to set the full $PATH value explicitly instead of doing an add thing for each directory. This avoids duplicates and the extra logic, but maybe isn't as convenient.
> 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)
But, that meant every shell was sharing the same history. Pressing up would go to the last command run anywhere, not just my current shell.
Sadly, I wasn't even trying to solve the problem Julia is talking about here. I just wanted to make sure my history was saved when I shutdown. Still haven't found a great solution to that. My attempts as using traps and signals caused weird issues.
In my (fever) dreams, there's a directory at `/var/share/env` with the rule that regular files are <file name>=<file contents> and all other file types are ignored. The `env` program can slurp all the files to create the default environment before applying whatever customization are in dotfiles for a shell.
Want to add to path? `echo ':<directory>' >> /var/share/env/PATH` (or `env --add PATH :<directory>`, or something like that).
Why not make your dreams real then? Here's a barebones zsh function. (Bash fails at export part. If remember right, in past, had wanted to set variables from subshell results and used some eval shenanigans.)
function my_env() {
ENVDIR=${ENVDIR:-/var/share/env}
arg="$1"
case "$arg" in
--add)
echo "$3" >> $ENVDIR/"$2"
;;
*)
(
for f in $ENVDIR/*; do
export $(basename $f)="$(< $f | sed -z 's/\n:/:/g')"
done
[[ -z "$arg" ]] && env || env "$arg"
)
;;
esac
}
(Of course, that's a bad approach but your idea is certainly doable in a good way.)
I'm not sure I'd stick a path setting in .bashrc - just because it would make it very difficult to ever override that PATH setting elsewhere.
You might want that but it might also mess up some other program's attempt to set the PATH and then e.g. run a shell command.
I usually use .bash_profile or .profile more for this sort of thing and then I have to tell my terminal program to run bash as a login shell and that gives me what I mostly expect.
It seems to be for interactive shells. So I probably hit a problem while shelling out from my editor but potentially any IDE that has a command prompt will have trouble if it's trying to set environment variables.
Its consistent on windows across shells but its also a hard to find feature if you haven't been using Windows for decades and its not easily scripted. Its still kind of hard, could be a lot easier than it is.
It's in the registry, iirc. All Linux needs is a standard persistent key/value store(*) that by convention shells check for environment variables before running their rc scripts.
It is. You go to Env vars, and modify the PATH setting. It just works.
I'm confused on the Linux point, because I thought I was the only one who had problems. Until I saw this article! Describes it well. I take responsibility; I am bad at setting env vars in Linux.
Why do you feel it is harder in Windows? In Windows 11, go into the Settings app, search for 'Path', then choose "Edit environment variables <in system/in account>". You then get a graphical method to add/edit/delete entries, and the paths apply to any application you run. No need to find a dotfile to edit. No need to know how to format a particular entry, or chain it with the existing $PATH.
On macOS it is arguably harder as you need to use Cmd-Shift-. to reveal dotfiles should you be uncomfortable editing them with vim/nano/etc. When using Omz with Terminal, a path entry could be in any number of files supporting Omz along with my zshrc. Windows doesn't have this issue.
Windows has it's issues, but env vars are significantly easier to manipulate without much knowledge.
The problem is that Linux does not have solid way of setting environment variables for an user (or user session). .bashrc is relevant for only bash (obviously), but bash is not the only thing that needs PATH and other env vars.
At least systemd brought some sanity with environment.d, but as this thread (and jvns article) shows it's not very well known. And of course there is still all sorts of weird inherited complexity like pam_env.
The Windows has a system-wide variables (incl. PATH) applicable everywhere. In contrast, the Linux situation is touched in the submitted article.
>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.
>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.
The ~/.pam_environment was the equivalent to Windows' environment variables but has been deprecated and supposedly systemd's environment.d took over its functionality.
I don't know what she means about setting the PATH for a GUI. If you want to run a program from the GUI you add a .desktop file to ~/.local/share/applications and provide the full path to the binary as part of the Exec key[0]
You can also override the actual PATH variable for an application in the .desktop file as well, or any environment variable, with the Environment key, if that's what she means.
Tbh the moment you need to use a terminal is the moment you're googling the magic spell unless you've previously googled the magic spell.
On Windows you just go Win->start typing "environment variable" and you get "Edit the system environment variables - Control Panel". Kinda stupid that you then need to press "Environment Variables..." but I probably wouldn't have to google it.
The difference on Linux/BSD/macOS is that the Window Server is a userland application -- you can't depend on it existing (except macOS), nor can you depend on the underlying shell being consistent on a given OS/distribution (macOS ships with a deprecated bash shell and current zsh shell).
On Windows, Microsoft has it easy. The window server is part of the executive; Win32/conhost always exist, even if you choose to run another personality (OS/2, POSIX, et. al.).
I would bet that there is a form of graphical editor somewhere out there on the Internet that someone developed for fun for a given shell.
It's improved since then though. When you edit the PATH variable it presents a list of items you can edit and reorder rather than one long delimited string
All this path stuff would go away if you have a proper vfs where you bind you other bins over /bin allowing $path to simply read "/bin ." and be done with it.
How does this work in a multi-user environment? Or if you want to override PATH only in certain contexts (eg. python virtual environment, which overrides python and pip)?
Per-process namespaces which gives each process its own table of mounts and binds. This is how Plan 9 works where you setup the environment with a script or via calls to mount(2) and bind(2). When you log in your profile sets up all your mounts/binds in your root namespace and every process you create thereafter inherits them. Then you change these mounts/binds as needed.
The author's last post was about terminal frustrations[0]
This is easily one of the most annoying ones, especially when shells will have several different locations to read config from depending on how you start the shell, or what a desktop app will do to try and pull the config in (`exec-path-from-shell` in emacs, for example).
And you can't really, say, put it in both your `.zprofile` and `.zshrc` or `.bash_profile`, `.profile`, and `.bashrc`, because then it'll get executed more than once and you'd need to maintain some kind of state to prevent that.
[0]https://jvns.ca/blog/2025/02/05/some-terminal-frustrations/
> And you can't really, say, put it in both your `.zprofile` and `.zshrc` or `.bash_profile`, `.profile`, and `.bashrc`, because then it'll get executed more than once and you'd need to maintain some kind of state to prevent that.
FWIW, profiles are loaded on 'from scratch' logins; RC files are loaded all the time.
* https://superuser.com/questions/657848/why-do-we-have-login-...
* https://askubuntu.com/questions/879364/differentiate-interac...
> RC files are loaded all the time
Not necessarily. Bash won't do it on its own for a login shell, but there's probably a bit in the /etc/profile your distro provides that does it.
Somewhat related there are other confusing things around how environment variables are set (especially on Linux). There are environment variables I set for QHD and UHD monitors e.g. QT_SCALE_FACTOR
* If you are using Xorg, you put environment variables in ~/.xprofile
* If you are using Xorg on Debian, you put the environment variables in ~/.xsessionrc
* If you are using Wayland, you make a file in ~/.config/environment.d/ and set you configuration variables there.
At what point do you consider it an "environment variable" vs "application setting" (even when called an env var by the app)? Windows has it's explicit env vars and GUI or CLI methods to set them, but if I was changing something about Explorer or some other graphical portion of the OS, I'd be going to the registry, for example. Setting the display resolution or scaling factor would be another registry entry, though I'd be using a purpose-built GUI for that.
Well with QT_SCALE_FACTOR is goes and tells all the QT to behave in a particular way. It isn't a per application setting.
There is a similar environment variable for Steam on Linux and I would argue that it should be an application setting but for whatever reason it isn't.
Both of these are hacks around how Xorg (doesn't) handle fractional scaling.
> Well with QT_SCALE_FACTOR is goes and tells all the QT to behave in a particular way. It isn't a per application setting.
I understand that, but your window server is just another application. On Windows I wouldn't consider scale factor being an env var, and indeed it isn't set as such -- rather that value is set in the registry.
Maybe that's the answer -- Windows has a defacto method of setting system-wide env vars that every application inherits within a given personality but Linux/BSD does not.
I can live with that answer :-)
No, it's talking about QT: https://nl.wikipedia.org/wiki/Qt-toolkit
Not the window server.
QT is a library that applications can use to render their GUIs, so there are many instances of QT in all of the apps that use it.
> If you are using Wayland, you make a file in ~/.config/environment.d/ and set you configuration variables there.
This is not in any way Wayland specific, and arguably should be the default choice for setting env vars, as it should apply to all user sessions regardless of what type they are or what shell is used.
I didn’t know that. I have to check if it works with Xorg + Gnome on Debian because I don’t think it does.
I've worked on maintaining internal dev tooling for some small companies for a while now, and it's a real PITA to write a robust installation script for bootstrapping a new laptop running an arbitrary shell on linux or macOS into a working environment. Way more work than it feels like it should be.
At this point I've pretty much given in and decided that a containerized dev environment is probably the better solution, but on principle it feels so unsatisfying to have to resort to this :(
(I know someone is going to mention Nix/Guix ;) but that feels like a giant rabbit hole)
FWIW in bash I have 2 functions:
These can join an arbitrary list of paths to PATH. e.g. They depend on another one: I also have ones for adding and prepending to LD_LIBRARY_PATHThe compact one-liners below are similar but avoid adding duplicate items to PATH, so are fine to call in various init scripts.
In Bash:
In portable POSIX shell:I cribbed these from someplace - slightly different approach:
Do any of these guard against an empty value on either side ?
"export PATH=$DIR:$PATH - That particular pattern is way too common, and is very dangerous if you consider the case when [$DIR or] $PATH (or whatever your variable is, like $LD_LIBRARY_PATH) isn’t set. Then, the value will be :/path/to/dir, which usually means both /path/to/dir and the current directory, which is usually both unexpected behaviour and a security concern."
I'm surprised both the blog post and all the other comments don't mention how it should have logic to check if the item exists in the path before adding it. Otherwise you get duplicates added everytime you source your config.
Your function does that so +1. Though I'd use
over grep -q but it functions the same.Another option is to set the full $PATH value explicitly instead of doing an add thing for each directory. This avoids duplicates and the extra logic, but maybe isn't as convenient.
> 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)
I attempted this in bash like so:
``` PROMPT_COMMAND="history -a; $PROMPT_COMMAND" ```
But, that meant every shell was sharing the same history. Pressing up would go to the last command run anywhere, not just my current shell.
Sadly, I wasn't even trying to solve the problem Julia is talking about here. I just wanted to make sure my history was saved when I shutdown. Still haven't found a great solution to that. My attempts as using traps and signals caused weird issues.
http://atuin.sh
In my (fever) dreams, there's a directory at `/var/share/env` with the rule that regular files are <file name>=<file contents> and all other file types are ignored. The `env` program can slurp all the files to create the default environment before applying whatever customization are in dotfiles for a shell.
Want to add to path? `echo ':<directory>' >> /var/share/env/PATH` (or `env --add PATH :<directory>`, or something like that).
Why not make your dreams real then? Here's a barebones zsh function. (Bash fails at export part. If remember right, in past, had wanted to set variables from subshell results and used some eval shenanigans.)
(Of course, that's a bad approach but your idea is certainly doable in a good way.)you could call it a "Registry"... ;)
.bashrc gets run every time a shell starts
I'm not sure I'd stick a path setting in .bashrc - just because it would make it very difficult to ever override that PATH setting elsewhere.
You might want that but it might also mess up some other program's attempt to set the PATH and then e.g. run a shell command.
I usually use .bash_profile or .profile more for this sort of thing and then I have to tell my terminal program to run bash as a login shell and that gives me what I mostly expect.
> .bashrc gets run every time a shell starts
False things people believe.
https://stackoverflow.com/a/820533
https://unix.stackexchange.com/a/129144
https://www.gnu.org/software/bash/manual/bash.html
So e.g. when I get an interactive shell from vim, for example, or an IDE it will execute .bashrc which can blow away settings I want for building.
> .bashrc gets run every time a shell starts
Probably because it's sourced from your distro provided /etc/profile. Bash will only source .bashrc on its own in some cases.
It seems to be for interactive shells. So I probably hit a problem while shelling out from my editor but potentially any IDE that has a command prompt will have trouble if it's trying to set environment variables.
I love this blog even though I usually get nothing from the articles that I didn't know before. Still read it and still love it.
As for path duplication, I personally have
in my version of pathadd().But not if the shell is running as an inferior process in an emacs buffer, in which case you have to do yet another thing.
Could you elaborate on the method ?
Here's a possible idea for an open-source project if someone is qualified.
Make it as easy as it is in Windows.
Its consistent on windows across shells but its also a hard to find feature if you haven't been using Windows for decades and its not easily scripted. Its still kind of hard, could be a lot easier than it is.
It's in the registry, iirc. All Linux needs is a standard persistent key/value store(*) that by convention shells check for environment variables before running their rc scripts.
So it will never happen.
* the file system doesn't count
SYSTEM: HKLM\SOFTWARE\CurrentControlSet\Control\Session Manager\Environment
User: HKCU\Environment
> a standard persistent key/value store()*
systemd-kvdb
It's not any easier on windows tho
It is. You go to Env vars, and modify the PATH setting. It just works.
I'm confused on the Linux point, because I thought I was the only one who had problems. Until I saw this article! Describes it well. I take responsibility; I am bad at setting env vars in Linux.
I found Rapid Environment Editor for that stuff year ago, never looked back.
I'd second this - REE is a great utility.
I feel like it's harder in Windows. You mean, there should be a GUI to do it instead of .bashrc?
Why do you feel it is harder in Windows? In Windows 11, go into the Settings app, search for 'Path', then choose "Edit environment variables <in system/in account>". You then get a graphical method to add/edit/delete entries, and the paths apply to any application you run. No need to find a dotfile to edit. No need to know how to format a particular entry, or chain it with the existing $PATH.
On macOS it is arguably harder as you need to use Cmd-Shift-. to reveal dotfiles should you be uncomfortable editing them with vim/nano/etc. When using Omz with Terminal, a path entry could be in any number of files supporting Omz along with my zshrc. Windows doesn't have this issue.
Windows has it's issues, but env vars are significantly easier to manipulate without much knowledge.
Don't even have to use GUI. Can do:
Sadly this has no error checking so it may mess things up. But it's an option.Right, but that's not easier, it's quicker. You need to know about setx, the path argument, and how to properly set a new path.
The problem is that Linux does not have solid way of setting environment variables for an user (or user session). .bashrc is relevant for only bash (obviously), but bash is not the only thing that needs PATH and other env vars.
At least systemd brought some sanity with environment.d, but as this thread (and jvns article) shows it's not very well known. And of course there is still all sorts of weird inherited complexity like pam_env.
The Windows has a system-wide variables (incl. PATH) applicable everywhere. In contrast, the Linux situation is touched in the submitted article.
>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.
>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.
The ~/.pam_environment was the equivalent to Windows' environment variables but has been deprecated and supposedly systemd's environment.d took over its functionality.
I don't know what she means about setting the PATH for a GUI. If you want to run a program from the GUI you add a .desktop file to ~/.local/share/applications and provide the full path to the binary as part of the Exec key[0]
You can also override the actual PATH variable for an application in the .desktop file as well, or any environment variable, with the Environment key, if that's what she means.
0 https://specifications.freedesktop.org/desktop-entry-spec/la...
you can just stick it in /etc/environment and reboot if you want
Tbh the moment you need to use a terminal is the moment you're googling the magic spell unless you've previously googled the magic spell.
On Windows you just go Win->start typing "environment variable" and you get "Edit the system environment variables - Control Panel". Kinda stupid that you then need to press "Environment Variables..." but I probably wouldn't have to google it.
There should certainly be a GUI to do it.
The difference on Linux/BSD/macOS is that the Window Server is a userland application -- you can't depend on it existing (except macOS), nor can you depend on the underlying shell being consistent on a given OS/distribution (macOS ships with a deprecated bash shell and current zsh shell).
On Windows, Microsoft has it easy. The window server is part of the executive; Win32/conhost always exist, even if you choose to run another personality (OS/2, POSIX, et. al.).
I would bet that there is a form of graphical editor somewhere out there on the Internet that someone developed for fun for a given shell.
>You mean, there should be a GUI
That must have been what Microsoft was figuring back in the 20th century.
Even in the latest W11, it's the same basic everyday GUI as in Windows XP :)
Just a little more intuitive now.
In XP from the Control Panel click System > Advanced > Environment Variables.
In Win11 from Settings click System > About > Advanced System Settings > Environment Variables.
Pay attention to your choice of User PATH, in addition to System PATH.
Make sure that things like %HOMEDRIVE% that you edit in, actually appear as "C:" once you are done editing, or you're not doing it ideally.
You haven't edited env vars in the path since win 8 then. At some point they made a GUI for env vars and the path variable.
That GUI has been there since at least XP.
It's improved since then though. When you edit the PATH variable it presents a list of items you can edit and reorder rather than one long delimited string
NT4
[flagged]
> fish instructions:
> set PATH $PATH ~/.npm-global/bin
Fish has some nice utilities for these type of set calls
set --append PATH ~/.npm-global/bin
All this path stuff would go away if you have a proper vfs where you bind you other bins over /bin allowing $path to simply read "/bin ." and be done with it.
How does this work in a multi-user environment? Or if you want to override PATH only in certain contexts (eg. python virtual environment, which overrides python and pip)?
Per-process namespaces which gives each process its own table of mounts and binds. This is how Plan 9 works where you setup the environment with a script or via calls to mount(2) and bind(2). When you log in your profile sets up all your mounts/binds in your root namespace and every process you create thereafter inherits them. Then you change these mounts/binds as needed.
Fwiw, this can be imitated on Linux with `unshare` or/and `nsenter`. Another brick to making a poor Plan 9 clone.
It's why I prefer to use Plan 9 where I can and treat Linux as a poor mans Windows.