46

I'd like a way to add things to $PATH, system-wide or for an individual user, without potentially adding the same path multiple times.

One reason to want to do this is so that additions can be made in .bashrc, which does not require a login, and is also more useful on systems which use (e.g.) lightdm, which never calls .profile.

I am aware of questions dealing with how to clean duplicates from $PATH, but I do not want to remove duplicates. I would like a way to add paths only if they are not already present.

4

10 Answers 10

44

Suppose that the new path that we want to add is:

new=/opt/bin

Then, using any POSIX shell, we can test to see if new is already in the path and add it if it isn't:

case ":${PATH:=$new}:" in
    *:"$new":*)  ;;
    *) PATH="$new:$PATH"  ;;
esac

Note the use of colons. Without the colons, we might think that, say, new=/bin was already in the path because it pattern matched on /usr/bin. While PATHs normally have many elements, the special cases of zero and one elements in the PATH is also handled. The case of the PATH initially having no elements (being empty) is handled by the use of ${PATH:=$new} which assigns PATH to $new if it is empty. Setting default values for parameters in this way is a feature of all POSIX shells: see section 2.6.2 of the POSIX docs.)

A callable function

For convenience, the above code can be put into a function. This function can be defined at the command line or, to have it available permanently, put into your shell's initialization script (For bash users, that would be ~/.bashrc):

pupdate() { case ":${PATH:=$1}:" in *:"$1":*) ;; *) PATH="$1:$PATH" ;; esac; }

To use this path update function to add a directory to the current PATH:

pupdate /new/path
14
  • 1
    You can save 2 case distinctions - cf. unix.stackexchange.com/a/40973/1131 . Apr 13, 2014 at 8:53
  • 3
    If PATH is empty, this will add an empty entry (i.e. the current directory) to the PATH. I think you need another case.
    – CB Bailey
    Apr 13, 2014 at 13:46
  • 2
    @CharlesBailey Not another case. Just do case "${PATH:=$new}". See my own answer for similar fallbacks.
    – mikeserv
    Apr 13, 2014 at 14:27
  • 1
    @mc0e I added an example of how to use a shell function to hide the "line-noise."
    – John1024
    Mar 1, 2016 at 3:52
  • 1
    @Doogle: uniq only detects duplicates if they are adjacent to each other, so I do not think this will remove duplicates if a path appears at the beginning and end of $PATH.
    – Ralph
    Dec 2, 2019 at 14:37
10

Create a file in /etc/profile.d called, e.g., mypath.sh (or whatever you want). If you are using lightdm, make sure that's viable or else use /etc/bashrc or a file sourced from same. Add to that the following functions:

checkPath () {
        case ":$PATH:" in
                *":$1:"*) return 1
                        ;;
        esac
        return 0;
}

# Prepend to $PATH
prependToPath () {
        for a; do
                checkPath $a
                if [ $? -eq 0 ]; then
                        PATH=$a:$PATH
                fi
        done
        export PATH
}

# Append to $PATH
appendToPath () {
        for a; do
                checkPath $a
                if [ $? -eq 0 ]; then
                        PATH=$PATH:$a
                fi
        done
        export PATH
}

Things at the beginning of (prepended to) $PATH take precedence over what follows, and conversely, things at the end (appended) will be superseded by what comes before. This means if your $PATH is /usr/local/bin:/usr/bin and there is an executable gotcha in both directories, the one in /usr/local/bin will be used by default.

You can now -- in this same file, in another shell config file, or from the commandline -- use:

appendToPath /some/path /another/path
prependToPath /some/path /yet/another/path

If this is in a .bashrc, it will prevent the value from appearing more than once when you start a new shell. There is a limitation in that if you want to append something that was prepended (i.e. move a path within $PATH) or vice versa, you'll have to do it yourself.

3
  • splitting the $PATH with IFS=: is ultimately more flexible than case.
    – mikeserv
    Apr 13, 2014 at 16:40
  • @mikeserv No doubt. This is a kind of hack use for case, IMO. I imagine awk could be put to good use here too.
    – goldilocks
    Apr 13, 2014 at 16:52
  • That's a good point. And, as I think, gawk could directly assign $PATH.
    – mikeserv
    Apr 13, 2014 at 16:54
6

You can do it this way:

echo $PATH | grep /my/bin >/dev/null || PATH=$PATH:/my/bin

Note: if you build PATH from other variables, do check that they're not empty, for many shells interpret "" like "." .

2
  • +1 According to the man page -q is required by POSIX for grep, but I don't know if that means there are still some (non POSIX) greps that don't have it.
    – goldilocks
    Apr 12, 2014 at 20:32
  • 1
    note that the grep pattern is overly broad. Consider using egrep -q "(^|:)/my/bin(:|\$)" instead of grep /my/bin >/dev/null. With that modification your solution is correct, and I think this is a more readable solution than the currently preferred answer from @john1024. Note that I've used double quotes so you use variable substitution in place of /my/bin
    – mc0e
    Feb 29, 2016 at 2:38
6

The important part of the code is to check whether PATH contains a specific path:

printf '%s' ":${PATH}:" | grep -Fq ":${my_path}:"

That is, ensure that each path in PATH is delimited on both sides by the PATH separator (:), then check (-q) whether the literal string (-F) consisting of a PATH separator, your path, and another PATH separator exists in there. If it does not you can safely add the path:

if ! printf '%s' ":${PATH-}:" | grep -Fq ":${my_path-}:"
then
    PATH="${PATH-}:${my_path-}"
fi

This should be POSIX compatible, and should work with any path not containing a newline character. It's more complex if you want it to work with paths containing newline while being POSIX compatible, but if you have a grep which supports -z you can use that.

6

I have been carrying this little function around with me in various ~/.profile files for years. It comes in the stock /etc/profile in Red Hat Linux and derivatives (CentOS, Fedora).

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

So, to add a new directory to the beginning of the PATH:

pathmunge /new/path

and to the end:

pathmunge /new/path after
4
  • This works for me! But I swapped to logic to put it after by default, and override with "before". :) Aug 25, 2015 at 17:33
  • 1
    pathmunge is part of linux centos distribution /etc/profile, it has a parameter before and after. I don't see it in my latest ubuntu 16.
    – Kemin Zhou
    Jan 2, 2017 at 2:04
  • Seems to work OK on macOS 10.12 after /bin/grep -> grep
    – Ben Creasy
    Feb 23, 2018 at 22:40
  • @KeminZhou indeed, I see it in Fedora 33 as well.
    – Nemo
    Mar 23, 2021 at 14:30
4

UPDATE:

I noticed your own answer had a separate function each for appending or prepending to the $PATH. I liked the idea. So I added a little argument handling. I also properly _namespaced it:

_path_assign() { oFS=$IFS ; IFS=: ; add=$* ; unset P A ; A=
    set -- ${PATH:=$1} ; for p in $add ; do {
        [ -z "${p%-[AP]}" ] && { unset P A
                eval ${p#-}= ; continue ; }
        for d ; do [ -z "${d%"$p"}" ] && break
        done ; } || set -- ${P+$p} $* ${A+$p}
        done ; export PATH="$*" ; IFS=$oFS
}

% PATH=/usr/bin:/usr/yes/bin
% _path_assign \
    /usr/bin \
    /usr/yes/bin \
    /usr/bin/nope \
    -P \
    /usr/nope/bin \
    /usr/bin \
    -A \
    /nope/usr/bin \
    /usr/nope/bin

% echo $PATH

OUTPUT:

/usr/nope/bin:/usr/bin:/usr/yes/bin:/usr/bin/nope:/nope/usr/bin

By default it will -Append to $PATH, but you can alter this behavior to -Prepend by adding a -P anywhere in your list of arguments. You can switch it back to -Appending by handing it a -A again.

SAFE EVAL

In most cases I recommend that people avoid any use of eval. But this, I think, stands out as an example of its use for good. In this case the only statement eval can ever see is P= or A=. The values of its arguments are strictly tested before ever it is called. This is what eval is for.

assign() { oFS=$IFS ; IFS=: ; add=$* 
    set -- ${PATH:=$1} ; for p in $add ; do { 
        for d ; do [ -z "${d%"$p"}" ] && break 
        done ; } || set -- $* $p ; done
    PATH="$*" ; IFS=$oFS
}

This will accept as many arguments as you give it and add each to $PATH only once and only if it is not already in $PATH. It makes use of only fully portable POSIX shell-script, relies only on shell built-ins, and is very fast.

% PATH=/usr/bin:/usr/yes/bin
% assign \
    /usr/bin \
    /usr/yes/bin \
    /usr/nope/bin \
    /usr/bin \
    /nope/usr/bin \
    /usr/nope/bin

% echo "$PATH"
> /usr/bin:/usr/yes/bin:/usr/nope/bin:/nope/usr/bin
4
  • @TAFKA'goldilocks' see the update here - you inspired me.
    – mikeserv
    Apr 13, 2014 at 12:53
  • +1 Out of curiousity (maybe this would be a good separate Q&A), where does the idea that _ prefixing shell functions make them "properly namespaced" come from? In other languages, it would usually indicate an internal global function (that is, one that needs to be global, but is not intended to be used externally as part of an API). My names are certainly not great choices, but it seems to me just using _ does not solve collision issues at all -- it would be better to tack on an actual namespace, eg. mikeserv_path_assign().
    – goldilocks
    Apr 13, 2014 at 16:56
  • @TAFKA'goldilocks' - it would be better to get even more specific with it, but the longer the name gets the less convenient its use is. But if you have any proper executable binaries prefixed with _ then you need to switch package managers. In any case, this, essentially, is just a a "global, internal, function" - it's global to every shell invoked from the shell in which it is declared, and it is only a bit of interpreted language script hanging out in the interpreter's memory. unix.stackexchange.com/questions/120528/…
    – mikeserv
    Apr 13, 2014 at 17:04
  • Can you not unset a (or equivalent) at the end of the profile?
    – sourcejedi
    Apr 6, 2019 at 18:48
2

To expand upon Cecil's excellent answer (kudos for the epicness of the response as well as the level of detail included, that was so helpful!), I wanted to make this work with other variables in addition to ${PATH}, and be portable across Bash and Zsh, so I came up with the below (note that my experience writing and especially documenting shell scripts is limited, so any further suggestions are welcome).

# string +variable.value(str variable)
#
# Print the value of the variable whose name is passed to this function.
# Basically a more portable way of resolving indirect references.
#
# Works in Bash and Zsh.
+variable.value() {
    # Extract the variable from the function argument.
    local variable="${1}"

    # If the variable doesn't exist, just return immediately.
    if [[ ! -v ${variable} ]]; then
        return 0;
    fi

    # Expand the variable based on whether the current shell is Bash or Zsh.
    if [[ "$BASH_VERSION" ]]; then
        echo "${!variable}"
    elif [[ "$ZSH_VERSION" ]]; then
        echo "${(P)pathvariable}"
    fi
}

# void +path.append(str pathvariable, str dirname, ...)
#
# Append each passed existing directory to the current user's $$pathvariable in
# a safe manner silently ignoring:
#
# * Relative directories (*NOT* prefixed by the directory separator).
# * Duplicate directories (already listed in the current $$pathvariable).
# * Non-existent directories.
+path.append() {
    # Extract the path variable into which we're injecting the other paths.
    local pathvariable="${1}"

    # If the variable exists in the environment, start building the new value
    # with its current value, otherwise start with an empty string.
    if [[ -v ${pathvariable} ]]; then
        local currentpath="$(+variable.value ${pathvariable})"
    else
        local currentpath=""
    fi;

    # Shift the path variable from the argument list, leaving only the paths
    # which need to be appended.
    shift

    # For each passed dirname:
    local dirname
    for dirname in "${@}"; 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 non-existent, then
        # silently ignore this dirname and continue to the next. Note that the
        # existency test is the least performant test and hence deferred.
        [[ "${dirname:0:1}" == '/' &&
           ":${currentpath}:" != *":${dirname}:"* &&
           -d "${dirname}" ]] || continue

        # Else, this is an existing absolute unique dirname. In this case,
        # append this dirname to the current ${PATH}.
        currentpath="${currentpath}:${dirname}"
    done

    # Strip an erroneously leading delimiter from the final value, if any, a
    # common edge case when the initial value is the empty string, and export
    # the new variable to subprocesses.
    export $pathvariable="${currentpath#:}"
}

# void +path.prepend(str pathvariable, str dirname, ...)
#
# Prepend each passed existing directory to the current user's ${!pathvariable}
# in a safe manner silently ignoring:
#
# * Relative directories (*NOT* prefixed by the directory separator).
# * Duplicate directories (already listed in the current ${!pathvariable}).
# * Non-existent directories.
+path.prepend() {
    # Extract the path variable into which we're injecting the other paths.
    local pathvariable="${1}"

    # If the variable exists in the environment, start building the new value
    # with its current value, otherwise start with an empty string.
    if [[ -v ${pathvariable} ]]; then
        local currentpath="$(+variable.value ${pathvariable})"
    else
        local currentpath=""
    fi;

    # Shift the path variable from the argument list, leaving only the paths
    # which need to be appended.
    shift

    # For each passed dirname:
    local dirname
    for dirname in "${@}"; 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 non-existent, then
        # silently ignore this dirname and continue to the next. Note that the
        # existency test is the least performant test and hence deferred.
        [[ "${dirname:0:1}" == '/' &&
           ":${currentpath}:" != *":${dirname}:"* &&
           -d "${dirname}" ]] || continue

        # Else, this is an existing absolute unique dirname. In this case,
        # append this dirname to the current ${PATH}.
        currentpath="${dirname}:${currentpath}"
    done

    # Strip an erroneously trailing delimiter from the final value, if any, a
    # common edge case when the initial value is the empty string, and export
    # the new variable to subprocesses.
    export $pathvariable="${currentpath%:}"
}

Unlike Cecil's original functions, you would use these by first passing the name of the variable to which you want to append the rest of the paths, like this:

+path.append "PATH" "/usr/bin" "/some/other/path"
+path.append "MANPATH" "/usr/share/man"

In order to be a little nitpicky (because the original answer deserves no less!), I also changed the "nonextant" comments from the original code to "non-existent", as "nonextant" implies that something existed previously (whereas that's not the context it's being used in).

1

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.

0

Here's my functional-programming-style version.

  • Works for any colon-delimited *PATH variable, not only PATH.
  • Does not access global state
  • Only works with/on its given immutable inputs
  • Produces a single output
  • No side effects
  • Memoizable (in principle)

Also noteworthy:

  • Agnostic regarding exporting; that's left to the caller (see examples)
  • Pure bash; no forking
path_add () {
  # $1: Element to ensure is in the given path string exactly once
  # $2: Existing path string value ("$PATH", not "PATH")
  # $3 (optional, anything): If given, append $1; otherwise, prepend
  #
  # Examples:
  #   $ export PATH=$(path_add '/opt/bin' "$PATH")
  #   $ CDPATH=$(path_add '/Music' "$CDPATH" at_end)

  local -r already_present="(^|:)${1}($|:)"
  if [[ "$2" =~ $already_present ]] ; then
    echo "$2"
  elif [[ $# == 3 ]] ; then
    echo "${2}:${1}"
  else
    echo "${1}:${2}"
  fi
}
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" ] && [ "$prefix" != "" ] && PATH=$prefix:$PATH
}

You must log in to answer this question.

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