175

Has anybody written a bash function to add a directory to $PATH only if it's not already there?

I typically add to PATH using something like:

export PATH=/usr/local/mysql/bin:$PATH

If I construct my PATH in .bash_profile, then it's not read unless the session I'm in is a login session -- which isn't always true. If I construct my PATH in .bashrc, then it runs with each subshell. So if I launch a Terminal window and then run screen and then run a shell script, I get:

$ echo $PATH
/usr/local/mysql/bin:/usr/local/mysql/bin:/usr/local/mysql/bin:....

I'm going to try building a bash function called add_to_path() which only adds the directory if it's not there. But, if anybody has already written (or found) such a thing, I won't spend the time on it.

3

22 Answers 22

180

From my .bashrc:

pathadd() {
    if [ -d "$1" ] && [[ ":$PATH:" != *":$1:"* ]]; then
        PATH="${PATH:+"$PATH:"}$1"
    fi
}

Note that PATH should already be marked as exported, so reexporting is not needed. This checks whether the directory exists & is a directory before adding it, which you may not care about.

Also, this adds the new directory to the end of the path; to put at the beginning, use PATH="$1${PATH:+":$PATH"}" instead of the above PATH= line.

10
  • 36
    I care. Sep 12, 2009 at 3:30
  • 6
    @Neil: It does work, because it compares with ":$PATH:" instead of just "$PATH" Apr 5, 2013 at 21:43
  • 3
    @GordonDavisson: I apologize, my test was wrong and you are correct.
    – Neil
    Apr 5, 2013 at 23:06
  • 6
    @Mark0978: That's what I did to fix the problem bukzor pointed out. ${variable:+value} means check whether variable is defined and has a non-empty value, and if it does gives the result of evaluating value. Basically, if PATH is nonblank to begin with it sets it to "$PATH:$1"; if it's blank it sets it to just "$1" (note the lack of a colon). Dec 21, 2013 at 1:47
  • 4
    Then you can add a required path by adding a line like this to the end of .bashrc file: pathadd "/usr/local/mysql/bin/" I know it can be obvious for Linux users but it might help those who came from Windows background and are unfamiliar with bash. Feb 21, 2017 at 11:29
34

Expanding on Gordon Davisson's answer, this supports multiple arguments

pathappend() {
  for ARG in "$@"
  do
    if [ -d "$ARG" ] && [[ ":$PATH:" != *":$ARG:"* ]]; then
        PATH="${PATH:+"$PATH:"}$ARG"
    fi
  done
}

So you can do pathappend path1 path2 path3 ...

For prepending,

pathprepend() {
  for ((i=$#; i>0; i--)); 
  do
    ARG=${!i}
    if [ -d "$ARG" ] && [[ ":$PATH:" != *":$ARG:"* ]]; then
        PATH="$ARG${PATH:+":$PATH"}"
    fi
  done
}

Similar to pathappend, you can do

pathprepend path1 path2 path3 ...

4
  • 3
    This is great! I made one small change. For the 'pathprepend' function, it's convenient to read the arguments in reverse, so you can say, for example, pathprepend P1 P2 P3 and end up with PATH=P1:P2:P3. To get this behavior, change for ARG in "$@" do to for ((i=$#; i>0; i--)); do ARG=${!i}
    – ishmael
    Aug 15, 2014 at 21:07
  • Thanks @ishmael, good suggestion, I edited the answer. I realize your comment is more than two years old, but I haven't been back since. I have to figure out how to get stack exchange e-mails to land in my inbox! Dec 15, 2016 at 23:30
  • pathprepend gave me a bad substitution error with the following code: github.com/ajr-dev/dotfiles/blob/…
    – user428047
    Dec 15, 2020 at 17:27
  • @ajr-dev My answer is in bash, I can't say whether it will work in zsh Jan 15, 2021 at 2:09
16

Here's something from my answer to this question combined with the structure of Doug Harris' function. It uses Bash regular expressions:

add_to_path ()
{
    if [[ "$PATH" =~ (^|:)"${1}"(:|$) ]]
    then
        return 0
    fi
    export PATH=${1}:$PATH
}
2
  • This worked for me only using $1 instead of ${1}
    – Andy
    Sep 5, 2012 at 19:25
  • @Andrei: Yes, the braces are unnecessary in this instance. I'm not sure why I included them. Sep 5, 2012 at 19:31
12

Put this in the comments to the selected answer, but comments don't seem to support PRE formatting, so adding the answer here:

@gordon-davisson I'm not a huge fan of unnecessary quoting & concatenation. Assuming you are using a bash version >= 3, you can instead use bash's built in regexs and do:

pathadd() {
    if [ -d "$1" ] && [[ ! $PATH =~ (^|:)$1(:|$) ]]; then
        PATH+=:$1
    fi
}

This does correctly handle cases where there are spaces in the directory or the PATH. There is some question as to whether bash's built in regex engine is slow enough that this might net be less efficient than the string concatenation and interpolation that your version does, but somehow it just feels more aesthetically clean to me.

5
  • 1
    Comments support formatting using the backtick only but you don't get any decent paragraph control.
    – boatcoder
    Sep 24, 2013 at 21:44
  • This puts the addition at the end. It is often desirable to add to the beginning in order to override existing locations. Nov 20, 2014 at 21:02
  • @DennisWilliamson That's a fair point, though I'd not recommend that as the default behaviour. It's not hard to figure out how to change for a prepend. Dec 30, 2014 at 9:48
  • 3
    @ChristopherSmith - re: unnecessary quoting implies you know ahead of time that $PATH is not null. "$PATH" makes it OK whether PATH is null or not. Similarly if $1 contains characters that might confuse the command parser. Putting the regex in quotes "(^|:)$1(:|$)" prevents that. Nov 10, 2015 at 21:17
  • @JesseChisholm: Actually, I believe Christopher’s point is that the rules are different between [[ and ]].  I prefer to quote everything that can possibly need to be quoted, unless that causes it to fail, but I believe he’s right, and that quotes really aren’t needed around $PATH. On the other hand, it seems to me that you are right about $1. Aug 22, 2017 at 18:22
11
idempotent_path_prepend ()
{
    PATH=${PATH//":$1"/} #delete any instances in the middle or at the end
    PATH=${PATH//"$1:"/} #delete any instances at the beginning
    export PATH="$1:$PATH" #prepend to beginning
}

When you need $HOME/bin to appear exactly once at the beginning of your $PATH and nowhere else, accept no substitutes.

4
  • Thanks, that's a nice elegant solution, but I found that I had to do PATH=${PATH/"... rather than PATH=${PATH//"... in order to get it to work.
    – Mark Booth
    Oct 25, 2013 at 11:01
  • The double-slash form should match any number of matches; the single slash only matches the first (search for "Pattern substitution" in the bash man page). Not sure why it didn't work... Oct 27, 2013 at 23:21
  • This fails in the unusual case that $1 is the only entry (no colons). The entry becomes doubled. Nov 20, 2014 at 21:05
  • It also deletes too aggressively as pointed out by PeterS6g. Nov 20, 2014 at 21:13
11

Here's an alternative solution that has the additional advantage of removing redundant entries:

function pathadd {
    PATH=:$PATH
    PATH=$1${PATH//:$1:/:}
}

The single argument to this function is prepended to the $PATH variable, and the first instance of the same string is removed from the existing path. In other words, if the directory already exists in the path, it is promoted to the front rather than being duplicated.

The function works by first prepending a colon to the existing path and then simultaneously prepending and removing the new entry from the existing path. This last part is performed using Bash's ${var//pattern/sub} notation; see the Bash Reference Manual for more details.

2
  • 1
    Good thought; flawed implementation.  Consider what happens if you already have /home/robert in your PATH and you pathadd /home/rob. Aug 22, 2017 at 20:35
  • 3
    flaw easily fixed as follows: the second line needs to replace whole paths by : as follows: PATH=$1${PATH//:$1:/:}
    – Eric
    May 23, 2021 at 16:22
8

Here's mine (I believe it was written years ago by Oscar, the sysadmin of my old lab, all credit to him), its been around in my bashrc for ages. It has the added benefit of allowing you to prepend or append the new directory as desired:

pathmunge () {
        if ! echo $PATH | /bin/egrep -q "(^|:)$1($|:)" ; then
           if [ "$2" = "after" ] ; then
              PATH=$PATH:$1
           else
              PATH=$1:$PATH
           fi
        fi
}

Usage:

$ echo $PATH
/bin/:/usr/local/bin/:/usr/bin
$ pathmunge /bin/
$ echo $PATH
/bin/:/usr/local/bin/:/usr/bin
$ pathmunge /sbin/ after
$ echo $PATH
/bin/:/usr/local/bin/:/usr/bin:/sbin/
6

I'm a bit surprised that no one has mentioned this yet, but you can use readlink -f to convert relative paths to absolute paths, and add them to the PATH as such.

For example, to improve on Guillaume Perrault-Archambault's answer,

pathappend() {
  for ARG in "$@"
  do
    if [ -d "$ARG" ] && [[ ":$PATH:" != *":$ARG:"* ]]; then
        PATH="${PATH:+"$PATH:"}$ARG"
    fi
  done
}

becomes

pathappend() {
    for ARG in "$@"
    do
        if [ -d "$ARG" ] && [[ ":$PATH:" != *":$ARG:"* ]]
        then
            if ARGA=$(readlink -f "$ARG")               #notice me
            then
                if [ -d "$ARGA" ] && [[ ":$PATH:" != *":$ARGA:"* ]]
                then
                    PATH="${PATH:+"$PATH:"}$ARGA"
                fi
            else
                PATH="${PATH:+"$PATH:"}$ARG"
            fi
        fi
    done
}

1. The Basics — What Good Does This Do?

The readlink -f command will (among other things) convert a relative path to an absolute path.  This allows you to do something like

$ cd /path/to/my/bin/dir
$ pathappend .
$ echo "$PATH"
<your_old_path>:/path/to/my/bin/dir

2. Why Do We Test For Being In PATH Twice?

Well, consider the above example.  If the user says pathappend . from the /path/to/my/bin/dir directory a second time, ARG will be ..  Of course, . will not be present in PATH.  But then ARGA will be set to /path/to/my/bin/dir (the absolute path equivalent of .), which is already in PATH.  So we need to avoid adding /path/to/my/bin/dir to PATH a second time.

Perhaps more importantly, the primary purpose of readlink is, as its name implies, to look at a symbolic link and read out the pathname that it contains (i.e., points to).  For example:

$ ls -ld /usr/lib/perl/5.14
-rwxrwxrwx  1   root   root    Sep  3  2015 /usr/lib/perl/5.14 -> 5.14.2
$ readlink /usr/lib/perl/5.14
5.14.2
$ readlink -f /usr/lib/perl/5.14
/usr/lib/perl/5.14.2

Now, if you say pathappend /usr/lib/perl/5.14, and you already have /usr/lib/perl/5.14 in your PATH, well, that’s fine; we can just leave it as it is.  But, if /usr/lib/perl/5.14 isn’t already in your PATH, we call readlink and get ARGA = /usr/lib/perl/5.14.2, and then we add that to PATH.  But wait a minute — if you already said pathappend /usr/lib/perl/5.14, then you already have /usr/lib/perl/5.14.2 in your PATH, and, again, we need to check for that to avoid adding it to PATH a second time.

3. What’s the Deal With if ARGA=$(readlink -f "$ARG")?

In case it’s unclear, this line tests whether the readlink succeeds.  This is just good, defensive programming practice.  If we’re going to use the output from command m as part of command n (where m < n), it’s prudent to check whether command m failed and handle that in some way.  I don’t think it’s likely that readlink will fail — but, as discussed in How to retrieve the absolute path of an arbitrary file from OS X and elsewhere, readlink is a GNU invention.  It’s not specified in POSIX, so its availability in Mac OS, Solaris, and other non-Linux Unixes is questionable.  (In fact, I just read a comment that says “readlink -f does not seem to work on Mac OS X 10.11.6, but realpath works out of the box.”  So, if you’re on a system that doesn’t have readlink, or where readlink -f doesn’t work, you may be able to modify this script to use realpath.)  By installing a safety net, we make our code somewhat more portable.

Of course, if you’re on a system that doesn’t have readlink (or realpath), you’re not going to want to do pathappend ..

The second -d test ([ -d "$ARGA" ]) is really probably unnecessary.  I can’t think of any scenario where $ARG is a directory and readlink succeeds, but $ARGA is not a directory.  I just copy-and-pasted the first if statement to create the third one, and I left the -d test there out of laziness.

4. Any Other Comments?

Yeah.  Like many of the other answers here, this one tests whether each argument is a directory, processes it if it is, and ignores it if it is not.  This may (or may not) be adequate if you’re using pathappend only in “.” files (like .bash_profile and .bashrc) and other scripts.  But, as this answer showed (above), it’s perfectly feasible to use it interactively.  You will be very puzzled if you do

$ pathappend /usr/local/nysql/bin
$ mysql
-bash: mysql: command not found

Did you notice that I said nysql in the pathappend command, rather than mysql?  And that pathappend didn’t say anything; it just silently ignored the incorrect argument?

As I said above, it’s good practice to handle errors.  Here’s an example:

pathappend() {
    for ARG in "$@"
    do
        if [ -d "$ARG" ]
        then
            if [[ ":$PATH:" != *":$ARG:"* ]]
            then
                if ARGA=$(readlink -f "$ARG")           #notice me
                then
                    if [[ ":$PATH:" != *":$ARGA:"* ]]
                    then
                        PATH="${PATH:+"$PATH:"}$ARGA"
                    fi
                else
                    PATH="${PATH:+"$PATH:"}$ARG"
                fi
            fi
        else
            printf "Error: %s is not a directory.\n" "$ARG" >&2
        fi
    done
}
13
  • (1) You should add quotes: readlink -f "$ARG". (2) I don’t know why it would happen (especially after the -d "$ARG" test succeeded), but you might want to test whether readlink failed. (3) You seem to be overlooking readlink’s primary function — to map a symbolic link name to a “real filename”.  If (for example) /bin is a symbolic link to /bin64, then repeated calls to pathappend /bin could result in PATH saying …:/bin64:/bin64:/bin64:/bin64:….  You should probably (also) check whether the new value of $ARG is already in PATH. Aug 22, 2017 at 16:58
  • (1) Good observation, I fixed it. (2) in what case would readlink fail? Assuming that a path is correct and it refers to a valid location. (3) I'm not sure what dictates readlink's primary function, I believe most (if not all?) paths in a unix filesystem can be divided into 2 types of links, symbolic links and hard links. As for the duplicate path entry you're right, but the purpose of my answer was not to add that (as I've noticed that other answers have already mentioned it). The only reason why I'm adding yet another answer is to contribute something I thought wasn't already contributed
    – qwertyzw
    Aug 22, 2017 at 17:16
  • (2) I mean, if at the very least the name of the command implied/hinted at its purpose, then 'link' in readlink can refer to either hard or soft links. You're correct however: man readlink says 'readlink - print resolved symbolic links or canonical file names', the . in my example I believe can be regarded as a canonical file name. Correct?
    – qwertyzw
    Aug 22, 2017 at 17:22
  • (1) To people who understand symbolic links, readlink’s name clearly implies its purpose — it looks at a symbolic link and reads out the pathname that it contains (i.e., points to).  (2) Well, I said that I didn’t know why readlink would fail.  I was making the general point that if a script or function contains multiple commands, and command n is guaranteed to fail catastrophically (or doesn’t make sense at all) if command m failed (where m < n), it’s prudent good practice to check whether command m failed and handle that in some way — at the very least,   … (Cont’d) Aug 26, 2017 at 20:36
  • (Cont’d) …  abort the script/function with a diagnostic. Hypothetically, the readlink could fail if (a) the directory was renamed or deleted (by another process) between your calls to test and readlink, or (b) if /usr/bin/readlink was deleted (or corrupted). (3) You seem to be missing my point.  I’m not encouraging you to duplicate other answer(s); I’m saying that, by checking whether the original ARG (from the command line) is already in PATH, but not repeating the check for the new ARG (the output from readlink), your answer is incomplete and therefore incorrect. … (Cont’d) Aug 26, 2017 at 20:36
5

A simple alias like this one below should do the trick:

alias checkInPath="echo $PATH | tr ':' '\n' | grep -x -c "

All it does is split the path on the : character and compare each component against the argument you pass in. grep checks for a complete line match, and prints out the count.

Sample usage:

$ checkInPath "/usr/local"
1
$ checkInPath "/usr/local/sbin"
1
$ checkInPath "/usr/local/sbin2"
0
$ checkInPath "/usr/local/" > /dev/null && echo "Yes" || echo "No"
No
$ checkInPath "/usr/local/bin" > /dev/null && echo "Yes" || echo "No"
Yes
$ checkInPath "/usr/local/sbin" > /dev/null && echo "Yes" || echo "No"
Yes
$ checkInPath "/usr/local/sbin2" > /dev/null && echo "Yes" || echo "No"
No

Replace the echo command with addToPath or some similar alias/function.

1
  • Using "grep -x" is probably faster than the loop I put in my bash function. Sep 11, 2009 at 18:04
5

For prepending, I like @Russell's solution, but there's a small bug: if you try to prepend something like "/bin" to a path of "/sbin:/usr/bin:/var/usr/bin:/usr/local/bin:/usr/sbin" it replaces "/bin:" 3 times (when it didn't really match at all). Combining a fix for that with the appending solution from @gordon-davisson, I get this:

path_prepend() {
    if [ -d "$1" ]; then
        PATH=${PATH//":$1:"/:} #delete all instances in the middle
        PATH=${PATH/%":$1"/} #delete any instance at the end
        PATH=${PATH/#"$1:"/} #delete any instance at the beginning
        PATH="$1${PATH:+":$PATH"}" #prepend $1 or if $PATH is empty set to $1
    fi
}
5

This question is quite saturated, but what I haven't seen so far is a generalized approach that works with any PATH-like variable

For example:

# prepend to PATH
_path_prepend "$XDG_DATA_HOME/phpenv/bin"

# prepend to MANPATH
_path_prepend MANPATH "$XDG_DATA_HOME/shell-installer/man"

# prepend to PERL5LIB
_path_prepend PERL5LIB "$PERL_LOCAL_LIB_ROOT/lib/perl5"

Features

  • POSIX compliant
  • Doesn't use Bash's declare -n
_path_prepend() {
    if [ -n "$2" ]; then
        case ":$(eval "echo \$$1"):" in
            *":$2:"*) :;;
            *) eval "export $1=$2\${$1:+\":\$$1\"}" ;;
        esac
    else
        case ":$PATH:" in
            *":$1:"*) :;;
            *) export PATH="$1${PATH:+":$PATH"}" ;;
        esac
    fi
}

_path_append() {
    if [ -n "$2" ]; then
        case ":$(eval "echo \$$1"):" in
            *":$2:"*) :;;
            *) eval "export $1=\${$1:+\"\$$1:\"}$2" ;;
        esac
    else
        case ":$PATH:" in
            *":$1:"*) :;;
            *) export PATH="${PATH:+"$PATH:"}$1" ;;
        esac
    fi
}

In both functions, if there isn't a second argument, it falls back to adding with the hard-coded PATH variable

The :+ part in ${PATH:+"$PATH:"} behaves as stated by Gordon Davisson in the replies of the top answer

... ${variable:+value} means check whether variable is defined and has a non-empty value, and if it does gives the result of evaluating value. Basically, if PATH is nonblank to begin with it sets it to "$PATH:$1"; if it's blank it sets it to just "$1" (note the lack of a colon)

Usage

➤ alfa=
➤ _path_prepend alfa one; echo "$alfa"
one
➤ _path_prepend alfa two; echo "$alfa"
two:one
➤ _path_prepend alfa three; echo "$alfa"
three:two:one

➤ bravo=
➤ _path_append bravo one; echo "$bravo"
one
➤ _path_append bravo two; echo "$bravo"
one:two
➤ _path_append bravo three; echo "$bravo"
one:two:three
3

See How to keep from duplicating path variable in csh? on StackOverflow for one set of answers to this question.

3

This way works fine:

if [[ ":$PATH:" != *":/new-directory:"* ]]; then PATH=${PATH}:/new-directory; fi
0
2

Here's what I whipped up:

add_to_path ()
{
    path_list=`echo $PATH | tr ':' ' '`
    new_dir=$1
    for d in $path_list
    do
        if [ $d == $new_dir ]
        then
            return 0
        fi
    done
    export PATH=$new_dir:$PATH
}

Now in .bashrc I have:

add_to_path /usr/local/mysql/bin

Updated version following comment about how my original will not handle directories with spaces (thanks to this question for pointing me to using IFS):

add_to_path ()
{
    new_dir=$1
    local IFS=:
    for d in $PATH
    do
        if [[ "$d" == "$new_dir" ]]
        then
            return 0
        fi
    done
    export PATH=$new_dir:$PATH
}
3
  • 1
    This may fail if any directory name already in PATH contains whitespace, *, ?, or []. Aug 22, 2017 at 18:06
  • Good point... but I'm an old-school linux guy and wouldn't ever use a path with whitespace in it :-) Aug 22, 2017 at 19:09
  • 1
    Good for you, for not creating things with whitespace in their names.  Shame on you for writing code that can’t handle them when they exist.  And what does being “an old-school Linux guy” have to do with it?  Windoze may have popularized the idea (thank you, Documents and Settings and Program Files), but Unix has supported pathnames containing whitespace since before Windoze existed. Aug 22, 2017 at 20:28
1
function __path_add(){  
if [ -d "$1" ] ; then  
    local D=":${PATH}:";   
    [ "${D/:$1:/:}" == "$D" ] && PATH="$PATH:$1";  
    PATH="${PATH/#:/}";  
    export PATH="${PATH/%:/}";  
fi  
}  
1

One-line solution.

if [[ ":$PATH:" != *":/your/path/:"* ]]; then PATH=${PATH}:/your/path/; echo "export PATH=\$PATH:/your/path/" >> ~/.bashrc; fi

We check env variable PATH for existing and

  • add for current session
  • add to .bashrc for all future session.

Or separated lines for readability (with \):

if [[ ":$PATH:" != *":/your/path/:"* ]]; then \
PATH=${PATH}:/your/path/; \
echo "export PATH=\$PATH:/your/path/" >> ~/.bashrc; \
fi
0

My versions are less careful about empty paths and insisting on paths being valid and directories than some posted here, but I do find a large-ish collection of prepend/append/clean/unique-ify/etc. shell functions to be useful for path manipulation. The whole lot, in their current state, are here: http://pastebin.com/xS9sgQsX (feedback and improvements very welcome!)

0

You can use a perl one liner:

appendPaths() { # append a group of paths together, leaving out redundancies
    # use as: export PATH="$(appendPaths "$PATH" "dir1" "dir2")
    # start at the end:
    #  - join all arguments with :,
    #  - split the result on :,
    #  - pick out non-empty elements which haven't been seen and which are directories,
    #  - join with :,
    #  - print
    perl -le 'print join ":", grep /\w/ && !$seen{$_}++ && -d $_, split ":", join ":", @ARGV;' "$@"
}

Here it is in bash:

addToPath() { 
    # inspired by Gordon Davisson, http://superuser.com/a/39995/208059
    # call as: addToPath dir1 dir2
    while (( "$#" > 0 )); do
    echo "Adding $1 to PATH."
    if [[ ! -d "$1" ]]; then
        echo "$1 is not a directory.";
    elif [[ ":$PATH:" == *":$1:"* ]]; then
        echo "$1 is already in the path."
    else
            export PATH="${PATH:+"$PATH:"}$1" # ${x:-defaultIfEmpty} ${x:+valueIfNotEmpty}
    fi
    shift
    done
}
0

I slightly modified Gordon Davisson's answer to use the current dir if none is supplied. So you can just do padd from the directory you want do add to your PATH.

padd() {
  current=`pwd`
  p=${1:-$current}
  if [ -d "$p" ] && [[ ":$PATH:" != *":$p:"* ]]; then
      PATH="$p:$PATH"
  fi
}
0

You can check if a custom variable has been set, otherwise set it and then add the new entries:

if [ "$MYPATHS" != "true" ]; then
    export MYPATHS="true"
    export PATH="$PATH:$HOME/bin:"

    # java stuff
    export JAVA_HOME="$(/usr/libexec/java_home)"
    export M2_HOME="$HOME/Applications/apache-maven-3.3.9"
    export PATH="$JAVA_HOME/bin:$M2_HOME/bin:$PATH"

    # etc...
fi

Of course, these entries could still be duplicated if added by another script, such as /etc/profile.

0

This script allows you to add at the end of $PATH:

PATH=path2; add_to_PATH after path1 path2:path3
echo $PATH
path2:path1:path3

Or add at the beginning of $PATH:

PATH=path2; add_to_PATH before path1 path2:path3
echo $PATH
path1:path3:path2

# Add directories to $PATH iff they're not already there
# Append directories to $PATH by default
# Based on https://unix.stackexchange.com/a/4973/143394
# and https://unix.stackexchange.com/a/217629/143394
add_to_PATH () {
  local prepend  # Prepend to path if set
  local prefix   # Temporary prepended path
  local IFS      # Avoid restoring for added laziness

  case $1 in
    after)  shift;; # Default is to append
    before) prepend=true; shift;;
  esac

  for arg; do
    IFS=: # Split argument by path separator
    for dir in $arg; do
      # Canonicalise symbolic links
      dir=$({ cd -- "$dir" && { pwd -P || pwd; } } 2>/dev/null)
      if [ -z "$dir" ]; then continue; fi  # Skip non-existent directory
      case ":$PATH:" in
        *":$dir:"*) :;; # skip - already present
        *) if [ "$prepend" ]; then
           # ${prefix:+$prefix:} will expand to "" if $prefix is empty to avoid
           # starting with a ":".  Expansion is "$prefix:" if non-empty.
            prefix=${prefix+$prefix:}$dir
          else
            PATH=$PATH:$dir  # Append by default
          fi;;
      esac
    done
  done
  [ "$prepend" ] && PATH=$prefix:$PATH
}
0

Here is a POSIX-compliant way.

# USAGE: path_add [include|prepend|append] "dir1" "dir2" ...
#   prepend: add/move to beginning
#   append:  add/move to end
#   include: add to end of PATH if not already included [default]
#          that is, don't change position if already in PATH
# RETURNS:
# prepend:  dir2:dir1:OLD_PATH
# append:   OLD_PATH:dir1:dir2
# If called with no paramters, returns PATH with duplicate directories removed
path_add() {
    # use subshell to create "local" variables
    PATH="$(path_unique)"
    export PATH="$(path_add_do "$@")"
}

path_add_do() {
    case "$1" in
    'include'|'prepend'|'append') action="$1"; shift ;;
    *)                            action='include'   ;;
    esac

    path=":$PATH:" # pad to ensure full path is matched later

    for dir in "$@"; do
        #       [ -d "$dir" ] || continue # skip non-directory params

        left="${path%:$dir:*}" # remove last occurrence to end

        if [ "$path" = "$left" ]; then
            # PATH doesn't contain $dir
            [ "$action" = 'include' ] && action='append'
            right=''
        else
            right=":${path#$left:$dir:}" # remove start to last occurrence
        fi

        # construct path with $dir added
        case "$action" in
            'prepend') path=":$dir$left$right" ;;
            'append')  path="$left$right$dir:" ;;
        esac
    done

    # strip ':' pads
    path="${path#:}"
    path="${path%:}"

    # return
    printf '%s' "$path"
}

# USAGE: path_unique [path]
# path - a colon delimited list. Defaults to $PATH is not specified.
# RETURNS: `path` with duplicated directories removed
path_unique() {
    in_path=${1:-$PATH}
    path=':'

    # Wrap the while loop in '{}' to be able to access the updated `path variable
    # as the `while` loop is run in a subshell due to the piping to it.
    # https://stackoverflow.com/questions/4667509/shell-variables-set-inside-while-loop-not-visible-outside-of-it
    printf '%s\n' "$in_path" \
    | /bin/tr -s ':' '\n'    \
    | {
            while read -r dir; do
                left="${path%:$dir:*}" # remove last occurrence to end
                if [ "$path" = "$left" ]; then
                    # PATH doesn't contain $dir
                    path="$path$dir:"
                fi
            done
            # strip ':' pads
            path="${path#:}"
            path="${path%:}"
            # return
            printf '%s\n' "$path"
        }
}

It is cribbed from Guillaume Perrault-Archambault's answer to this question and mike511's answer here.

UPDATE 2017-11-23: Fixed bug per @Scott

3
  • I was going to upvote this for providing a command-line option to choose between prepending and postpending (appending), with a default.  But then I thought: this is an awful lot of somewhat cryptic code with no explanation. (And the fact that you have two functions, where one changes PATH and echoes its new value, and the other captures that output and assigns it to PATH again, is just an unnecessary complexity.) … (Cont’d) Aug 23, 2017 at 21:49
  • (Cont’d) …  And then I noticed that the links were wrong. (And I’m not sure why you're blaming those guys; you don’t seem to have copied much from their answers.) And then I noticed that the code was wrong. I guess it does an OK job of maintaining a well-formed PATH, but there’s no guarantee that it’s already well-formed, especially if you have an unenlightened /etc/profile.  The directory that you’re trying to add to PATH might already be there more than once, and your code chokes on that. … (Cont’d) Aug 23, 2017 at 21:49
  • (Cont’d) …  For example, if you try to prepend /a/ck to /b:/a/ck:/tr:/a/ck,  you get /a/ck:/b:/a/ck:/tr:/tr:/a/ck. Aug 23, 2017 at 21:49

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .