Take the 2-minute tour ×
Unix & Linux Stack Exchange is a question and answer site for users of Linux, FreeBSD and other Un*x-like operating systems.. It's 100% free, no registration required.

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.

share|improve this question

6 Answers 6

up vote 16 down vote accepted

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 PATH's 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.)

share|improve this answer
    
@hammar OK. I added a case for that. –  John1024 Apr 13 at 6:36
1  
You can save 2 case distinctions - cf. unix.stackexchange.com/a/40973/1131 . –  maxschlepzig Apr 13 at 8:53
2  
If PATH is empty, this will add an empty entry (i.e. the current directory) to the PATH. I think you need another case. –  Charles Bailey Apr 13 at 13:46
1  
@CharlesBailey Not another case. Just do case "${PATH:=$new}". See my own answer for similar fallbacks. –  mikeserv Apr 13 at 14:27
    
@mikeserv Is ${PATH:=$new} portable beyond bash? –  goldilocks Apr 13 at 14:50

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.

share|improve this answer
2  
This won't work if you have /a/b/c/d in your path but then want to add /a/b to your path as well. –  PlasmaPower Apr 13 at 14:03
    
Changing the regex to :$a\$\|:$a:\|^$a:\|^$a\$ should fix it, you will need to quote it. –  PlasmaPower Apr 13 at 14:12
    
@PlasmaPower : I added a separate function using the case method instead, which is less clunky than grep. –  goldilocks Apr 13 at 14:47
    
splitting the $PATH with IFS=: is ultimately more flexible than case. –  mikeserv Apr 13 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 at 16:52

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.

share|improve this answer

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

share|improve this answer
    
+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 at 20:32

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
share|improve this answer
    
@TAFKA'goldilocks' see the update here - you inspired me. –  mikeserv Apr 13 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 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 at 17:04

I have been carrying this little function around with me in various ~/.profile files for years. I think it was written by the sysadmin in a lab I used to work in but I'm not sure. Anyway, it is similar to Goldilock's approach but slightly different:

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
share|improve this answer

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

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