Behold! The industrial-strength 12-line ...technically bash- and zsh-portable shell function that devotedly loves your ~/.bashrc
or ~/.zshrc
startup script of choice:
# void +path.append(str dirname, ...)
#
# Append each passed existing directory to the current user's ${PATH} in a
# safe manner silently ignoring:
#
# * Relative directories (i.e., *NOT* prefixed by the directory separator).
# * Duplicate directories (i.e., already listed in the current ${PATH}).
# * Nonextant directories.
+path.append() {
# For each passed dirname...
local dirname
for dirname; do
# Strip the trailing directory separator if any from this dirname,
# reducing this dirname to the canonical form expected by the
# test for uniqueness performed below.
dirname="${dirname%/}"
# If this dirname is either relative, duplicate, or nonextant, then
# silently ignore this dirname and continue to the next. Note that the
# extancy test is the least performant test and hence deferred.
[[ "${dirname:0:1}" == '/' &&
":${PATH}:" != *":${dirname}:"* &&
-d "${dirname}" ]] || continue
# Else, this is an existing absolute unique dirname. In this case,
# append this dirname to the current ${PATH}.
PATH="${PATH}:${dirname}"
done
# Strip an erroneously leading delimiter from the current ${PATH} if any,
# a common edge case when the initial ${PATH} is the empty string.
PATH="${PATH#:}"
# Export the current ${PATH} to subprocesses. Although system-wide scripts
# already export the ${PATH} by default on most systems, "Bother free is
# the way to be."
export PATH
}
Prepare thyself for instantaneous glory. Then, rather than doing this and wishfully hoping for the best:
export PATH=$PATH:~/opt/bin:~/the/black/goat/of/the/woods/with/a/thousand/young
Do this instead and be guaranteed of getting the best, whether you really even wanted that or not:
+path.append ~/opt/bin ~/the/black/goat/of/the/woods/with/a/thousand/young
Very Well, Define "Best."
Safely appending and prepending onto the current ${PATH}
isn't the trivial affair it's commonly made out to be. While convenient and seemingly sensible, one-liners of the form export PATH=$PATH:~/opt/bin
invite devilish complications with:
Accidentally relative dirnames (e.g., export PATH=$PATH:opt/bin
). While bash
and zsh
silently accept and mostly ignore relative dirnames in most cases, relative dirnames prefixed by either h
or t
(and possibly other nefarious characters) cause both to shamefully mutilate themselves ala Masaki Kobayashi's seminal 1962 masterpiece Harakiri:
# Don't try this at home. You will feel great pain.
$ PATH='/usr/local/bin:/usr/bin:/bin' && export PATH=$PATH:harakiri && echo $PATH
/usr/local/bin:/usr/bin:arakiri
$ PATH='/usr/local/bin:/usr/bin:/bin' && export PATH=$PATH:tanuki/yokai && echo $PATH
binanuki/yokai # Congratulations. Your system is now face-up in the gutter.
Accidentally duplicate dirnames. While duplicate ${PATH}
dirnames are largely innocuous, they're also unwanted, cumbersome, mildly inefficient, impede debuggability, and promote drive wear – sorta like this answer. While NAND-style SSDs are (of course) immune to read wear, HDDs are not. Unnecessary filesystem access on every attempted command implies unnecessary read head wear at the same tempo. Duplicates are particularly unctuous when invoking nested shells in nested subprocesses, at which point seemingly innocuous one-liners like export PATH=$PATH:~/wat
rapidly explode into the Seventh Circle of ${PATH}
Hell like PATH=/usr/local/bin:/usr/bin:/bin:/home/leycec/wat:/home/leycec/wat:/home/leycec/wat:/home/leycec/wat
. Only Beelzebubba can help you if you then append additional dirnames onto that. (Don't let this happen to your precious children.)
- Accidentally missing dirnames. Again, while missing
${PATH}
dirnames are largely innocuous, they're also typically unwanted, cumbersome, mildly inefficient, impede debuggability, and promote drive wear.
Ergo, friendly automation like the shell function defined above. We must save ourselves from ourselves.
But... Why "+path.append()"? Why Not Simply append_path()?
For disambiguity (e.g., with external commands in the current ${PATH}
or system-wide shell functions defined elsewhere), user-defined shell functions are ideally prefixed or suffixed with unique substrings supported by bash
and zsh
but otherwise prohibited for standard command basenames – like, say, +
.
Hey. It works. Don't judge me.
But... Why "+path.append()"? Why Not "+path.prepend()"?
Because appending to the current ${PATH}
is safer than prepending to the current ${PATH}
, all things being equal, which they never are. Overriding system-wide commands with user-specific commands can be unsanitary at best and crazy-making at worst. Under Linux, for example, downstream applications commonly expect the GNU coreutils variants of commands rather than custom non-standard derivatives or alternatives.
That said, there absolutely are valid use cases for doing so. Defining the equivalent +path.prepend()
function is trivial. Sans prolix nebulosity, for his and her shared sanity:
+path.prepend() {
local dirname
for dirname in "${@}"; do
dirname="${dirname%/}"
[[ "${dirname:0:1}" == '/' &&
":${PATH}:" != *":${dirname}:"* &&
-d "${dirname}" ]] || continue
PATH="${dirname}:${PATH}"
done
PATH="${PATH%:}"
export PATH
}
But... Why Not Gilles?
Gilles' accepted answer elsewhere is impressively optimal in the general case as a "shell agnostic idempotent append". In the common case of bash
and zsh
with no undesirable symlinks, however, the performance penalty required to do so saddens the Gentoo ricer in me. Even in the presence of undesirable symlinks, it's debatable whether forking one subshell per add_to_PATH()
argument is worth the potential insertion of symlink duplicates.
For strict use cases demanding that even symlink duplicates be eliminated, this zsh
-specific variant does so via efficient builtins rather than inefficient forks:
+path.append() {
local dirname
for dirname in "${@}"; do
dirname="${dirname%/}"
[[ "${dirname:0:1}" == '/' &&
":${PATH}:" != *":${dirname:A}:"* &&
-d "${dirname}" ]] || continue
PATH="${PATH}:${dirname}"
done
PATH="${PATH#:}"
export PATH
}
Note the *":${dirname:A}:"*
rather than *":${dirname}:"*
of the original. :A
is a wondrous zsh
-ism sadly absent under most other shells – including bash
. To quote man zshexpn
:
A: Turn a file name into an absolute path as the a
modifier does, and then pass the result through the realpath(3)
library function to resolve symbolic links. Note: on systems that do not have a realpath(3)
library function, symbolic links are not resolved, so on those systems a
and A
are equivalent.
No Further Questions.
You're welcome. Enjoy safe shelling. You now deserve it.