183

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.

Doug Harris
  • 28,397

23 Answers23

193

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.

M.M
  • 572
  • 1
  • 8
  • 19
35

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 ...

18

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
}
13

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.

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.

Russell
  • 111
  • 1
  • 2
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.

Cosmo
  • 113
Rob Hague
  • 211
9

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/
terdon
  • 54,564
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
}
qwertyzw
  • 161
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:+&quot;$$1:&quot;}$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

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.

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
}
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
Akceptor
  • 141
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
}
Doug Harris
  • 28,397
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
1
function __path_add(){  
if [ -d "$1" ] ; then  
    local D=":${PATH}:";   
    [ "${D/:$1:/:}" == "$D" ] && PATH="$PATH:$1";  
    PATH="${PATH/#:/}";  
    export PATH="${PATH/%:/}";  
fi  
}  
Gareth
  • 19,080
GreenFox
  • 161
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
}
Tom Hale
  • 2,708
0

I have these modifications to PATH in my .profile, so it's no good to use the [[ bash-ism. This does the same thing, but should be POSIXly correct:

prepend_to_PATH () {
    local dir
    for dir ; do
        if [ -d "$dir" ] ; then
            case ":$PATH:" in
                (*":$dir:"*) : ;;
                (*) PATH="$dir:$PATH" ;;
            esac
        fi
    done
}

prepend_to_PATH /my/path/bin /another/path

Walf
  • 491
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

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

go2null
  • 209
  • 1
  • 5
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
}