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 -Fq instead of egrep routing 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/bin to the PATH. :)
  • 1
    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). – jlliagre Apr 28 '15 at 16:31

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.
  • 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. – Sildoreth Apr 28 '15 at 18:32
  • 2
    @Sildoreth No. Looping via builtins is fast (at least comparing to a fork/exec of grep invocation). – vnp Apr 28 '15 at 18:53
  • 3
    I like this. But I'd go with the shorter $pathdir == $1 && return – janos Apr 29 '15 at 19:08
  • You probably don't want to just return there without also restoring IFS. – Toby Speight Nov 8 at 14:04

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
    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 ~/bin directory 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. – Sildoreth Apr 28 '15 at 16:51
  • @Sildoreth Indeed, I have added comments about it in my reply. – jlliagre Apr 28 '15 at 17:02
  • @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. – jlliagre Nov 8 at 20:53
  • @TobySpeight Okay, I misunderstood your comment. I just meant that the quotes were superfluous around front but were required around $2. You might indeed keep them around the former without breaking the script. – jlliagre Nov 8 at 21:05
  • 1
    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"!) – Toby Speight Nov 8 at 21:38

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
}
  • 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 $PATH prints /usr/bin rather than the expected /bin:/usr/bin. (Woops.) – Cecil Curry Nov 8 at 5:41
  • @CecilCurry Thanks for spotting the bug. I've fixed it and now the function works just fine. – Mateusz Piotrowski Nov 8 at 13:52
  • 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 think $1 might need quotes there, too. (Oh, and we can use [ instead of [[, to regain portability). – Toby Speight Nov 8 at 14:07
  • @TobySpeight Good idea. It does in fact simplifies this example a little bit. – Mateusz Piotrowski Nov 8 at 14:20

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

Your Answer

 

By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

Not the answer you're looking for? Browse other questions tagged or ask your own question.