Adding a directory to the PATH in Unix/Linux is a very common task. However, what if the directory is already in the path? My goal is to write a shell function (preferably portable) that will add a directory to the front or back of the PATH variable, but only if it's not already there.
Here's what I have (syntax is in zsh / bash):
#-------
# DESC: Adds a directory to the PATH if it's not already in the PATH
# ARGS:
# 1 - The directory to add
# 2 - Which end of PATH to add to. Use "front" to prepend.
#-------
add2path() {
if ! echo $PATH | egrep "(^|:)1ドル(:|\$)" > /dev/null ; then
if [[ 2ドル = "front" ]]; then
PATH="1ドル:$PATH"
else
PATH="$PATH:1ドル"
fi
export PATH
fi
}
This function works. I have tested it on Ubuntu, Solaris, and FreeBSD. And I have tested it in bash, zsh, and ksh. But I want to make sure that it is as portable (first and foremost), readable, and as efficient as possible.
Remarks
- I know that the
=~operator would be more readable, but I had trouble with it not working on certain OSes, particularly Solaris. - I know I could (and arguably should) use
grep -Fqinstead ofegreprouting to/dev/null, but again this didn't work on certain OSes (Solaris)- Ironically, the way to fix this on Solaris is to add
/usr/xpg4/binto the PATH. :)
- Ironically, the way to fix this on Solaris is to add
-
1\$\begingroup\$ If you want to write a portable shell function, instead of targeting certain shell interpreters and OSes, you should just write it with pure POSIX syntax and make sure it is executed in a POSIX context (which is the trickiest part). \$\endgroup\$jlliagre– jlliagre2015年04月28日 16:31:21 +00:00Commented Apr 28, 2015 at 16:31
5 Answers 5
grep is most likely an overkill. After setting IFS=":", the $PATH is conveniently split into words. Then the presence of the directory can be determined in a simple loop
IFS=":"
for pathdir in $PATH; do
if [ $pathdir == 1ドル]; then return; fi
done
# Now restore IFS and modify path as needed.
-
\$\begingroup\$ Won't this be slower, though? In this solution, we're using a script to perform looping whereas the original solution has
grep, which runs at the machine level, doing the looping for us. \$\endgroup\$Sildoreth– Sildoreth2015年04月28日 18:32:11 +00:00Commented Apr 28, 2015 at 18:32 -
3\$\begingroup\$ @Sildoreth No. Looping via builtins is fast (at least comparing to a
fork/execofgrepinvocation). \$\endgroup\$vnp– vnp2015年04月28日 18:53:27 +00:00Commented Apr 28, 2015 at 18:53 -
3\$\begingroup\$ I like this. But I'd go with the shorter
$pathdir == 1ドル && return\$\endgroup\$janos– janos2015年04月29日 19:08:07 +00:00Commented Apr 29, 2015 at 19:08 -
1\$\begingroup\$ You probably don't want to just
returnthere without also restoringIFS. \$\endgroup\$Toby Speight– Toby Speight2018年11月08日 14:04:38 +00:00Commented Nov 8, 2018 at 14:04 -
\$\begingroup\$ Here is a nice way to restore IFS \$\endgroup\$raratiru– raratiru2022年11月10日 14:56:10 +00:00Commented Nov 10, 2022 at 14:56
vnp's solution is pretty good because it uses only builtin. Here's another way, which doesn't modify the IFS:
add2path() {
case :$PATH: in
*:1ドル:*) ;;
*)
if [ "2ドル" = 'front' ]; then
PATH="1ドル:$PATH"
else
PATH="$PATH:1ドル"
fi
export PATH
;;
esac
}
-
4\$\begingroup\$ This is terrible. There is no way to uniquely match pathnames in
:-delimited strings in raw bash without either manually iterating the${IFS}or matching via the regex-based=~operator. Without profiling, it's unclear which is more performant. What is clear, however, is that this answer fails in fairly obviously common edge cases (e.g., when the passed dirname is itself a substring of an existing dirname on the current${PATH}). Notably,PATH=/usr/bin && add2path /bin/ front && echo $PATHprints/usr/binrather than the expected/bin:/usr/bin. (Woops.) \$\endgroup\$Cecil Curry– Cecil Curry2018年11月08日 05:41:50 +00:00Commented Nov 8, 2018 at 5:41 -
\$\begingroup\$ @CecilCurry Thanks for spotting the bug. I've fixed it and now the function works just fine. \$\endgroup\$Mateusz Piotrowski– Mateusz Piotrowski2018年11月08日 13:52:18 +00:00Commented Nov 8, 2018 at 13:52
-
\$\begingroup\$ Simpler method:
case ":$PATH:" in *:1ドル:*)- if we add:onto each end, we no longer have to special-case the first and last members. I'm not 100% sure, but I think1ドルmight need quotes there, too. (Oh, and we can use[instead of[[, to regain portability). \$\endgroup\$Toby Speight– Toby Speight2018年11月08日 14:07:31 +00:00Commented Nov 8, 2018 at 14:07
Here is one way to make it more portable and robust:
add2path() {
if ! echo "$PATH" | PATH=$(getconf PATH) grep -Eq '(^|:)'"${1:?missing argument}"'(:|\$)' ; then # Use the POSIX grep implementation
if [ -d "1ドル" ]; then # Don't add a non existing directory to the PATH
if [ "2ドル" = front ]; then # Use a standard shell test
PATH="1ドル:$PATH"
else
PATH="$PATH:1ドル"
fi
export PATH
fi
fi
}
I have added a test to quit the function with an error should no argument be passed.
PATH=$(getconf PATH) is the portable way to have the POSIX commands first in the PATH. This is solving the issue you had with Solaris where, not to break existing Solaris scripts portability, the first grep command found in the PATH is not POSIX compliant.
Using [[ is not specified by POSIX so it is better to stick with [ to do tests.
2ドル should then be quoted to avoid clashes with [ operands.
front, being a constant string, need not be quoted.
Excluding non directories to be added looks to me a reasonable approach but is of course an optional extension which you might remove.
-
1\$\begingroup\$ It's not necessarily bad to add a non-existent directory to the PATH. For instance, say I always want to be able to run from
~/bin, and I have the exact same rc files on many machines (which is the case for me). There might be cases where the~/bindirectory doesn't exist yet. Then if I add it on one machine, I'd have to re-run my rc file to place that directory in the PATH. \$\endgroup\$Sildoreth– Sildoreth2015年04月28日 16:51:55 +00:00Commented Apr 28, 2015 at 16:51 -
\$\begingroup\$ @Sildoreth Indeed, I have added comments about it in my reply. \$\endgroup\$jlliagre– jlliagre2015年04月28日 17:02:41 +00:00Commented Apr 28, 2015 at 17:02
-
\$\begingroup\$ @TobySpeight Not just a matter of style. Quotes are definitely required here to allow adding a path with an embedded space, or not to fail when passed an empty argument. \$\endgroup\$jlliagre– jlliagre2018年11月08日 20:53:43 +00:00Commented Nov 8, 2018 at 20:53
-
\$\begingroup\$ @TobySpeight Okay, I misunderstood your comment. I just meant that the quotes were superfluous around
frontbut were required around2ドル. You might indeed keep them around the former without breaking the script. \$\endgroup\$jlliagre– jlliagre2018年11月08日 21:05:15 +00:00Commented Nov 8, 2018 at 21:05 -
1\$\begingroup\$ Yes, that's what I meant. But it looked like you were saying that it shouldn't be quoted. I think that's actually just a typo, and I've made an edit accordingly. (Sometimes in English, a single letter makes all the difference - e.g. "a few people" is very much more than "few people"!) \$\endgroup\$Toby Speight– Toby Speight2018年11月08日 21:38:39 +00:00Commented Nov 8, 2018 at 21:38
I don't think you need to export PATH.
By the time this script of yours is sourced,
PATH should be exported already by startup scripts of the OS.
See also this post on Unix SE.
If you don't mind dropping ksh support,
then you can replace echo with a here-string:
egrep "(^|:)1ドル(:|\$)" <<< "$PATH" > /dev/null
I liked the idea presented by @vpn but it's only a snippet so I coded a working function using just bash builtins and separating the functionality into two different calls (append/prepend) which IMHO makes it more robust.
@vpns's answer doesn't include the whole function and also has the IFS problem. This working code doesn't use grep and IFS is untouched.
not_in_path(){
IFS=":" read -r -a path_array <<< "$PATH"
for pathdir in ${path_array[@]}; do
if [ "$pathdir" == "1ドル" ]; then
return 1
fi
done
return 0
}
append2path(){
not_in_path "1ドル" && export PATH="$PATH:1ドル"
}
prepend2path(){
not_in_path "1ドル" && export PATH="1ドル:$PATH"
}
Usage is pretty simple, just:
append2path /my/pathto appendprepend2path /my/pathto prepend
From what I understand I can rely on PATH being already present so it is not necessary to handle the case of PATH being empty or not set and colon should be omitted.
Also there's no need to export the variable. It doesn't harm, though and for me it is clearer what the function intends to achieve. Maybe there are other issues. Happy to see some feedback.
-
1\$\begingroup\$ If you have a new question, please ask it by clicking the Ask Question button. Include a link to this question if it helps provide context. - From Review \$\endgroup\$rolfl– rolfl2022年02月11日 19:24:52 +00:00Commented Feb 11, 2022 at 19:24
-
\$\begingroup\$ You have presented an alternative solution, but haven't reviewed the code. Please edit to show what aspects of the question code prompted you to write this version, and in what ways it's an improvement over the original. It may be worth (re-)reading How to Answer. \$\endgroup\$Toby Speight– Toby Speight2022年02月12日 15:13:34 +00:00Commented Feb 12, 2022 at 15:13
-
1\$\begingroup\$ @TobySpeight did you find any specific issues in the code which made you suggest that the code is not reviewed? I added a reasoning. It is more a continuation of an idea than an alternative solution. \$\endgroup\$badc0de– badc0de2022年02月12日 23:14:54 +00:00Commented Feb 12, 2022 at 23:14
-
1\$\begingroup\$ @TobySpeight Sorry, should I link the answer in question. Didn't know how to do that. The sentence would be: "@vpns's answer doesn't include the whole function and also has the IFS problem. This working code doesn't use grep and IFS is untouched." \$\endgroup\$badc0de– badc0de2022年02月12日 23:45:12 +00:00Commented Feb 12, 2022 at 23:45
-
1\$\begingroup\$ It looks like you have some pretty good suggestions in there - separate functions to append and prepend is certainly more user-friendly and robust than relying on a second argument to switch behaviours, for example. That's something that could be a worthwhile review and good intro to your alternative solution! \$\endgroup\$Toby Speight– Toby Speight2022年02月12日 23:53:51 +00:00Commented Feb 12, 2022 at 23:53