Add directory to $PATH if it's not already there

129

62

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

Posted 2009-09-11T16:19:11.790

Reputation: 23 578

http://unix.stackexchange.com/questions/4965/keep-duplicates-out-of-path-on-source – Ciro Santilli 新疆改造中心法轮功六四事件 – 2015-08-10T09:41:32.890

If you frame the problem as "only adding if not already there", you're going to be rudely surprised when the day comes when it's important for the inserted item to be at the beginning but it doesn't wind up there. A better approach would be to insert the element, and then remove duplicates, so if the new entry was already there it will be effectively moved to the beginning. – Don Hatch – 2017-11-24T22:48:16.630

See http://stackoverflow.com/questions/273909/how-do-i-manipulate-path-elements-in-shell-scripts for some infrastructure that can help.

– dmckee --- ex-moderator kitten – 2009-09-11T17:49:46.693

Answers

129

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.

Gordon Davisson

Posted 2009-09-11T16:19:11.790

Reputation: 28 538

2Works on OS X's bash 3.2, but I read that in POSIX sh, [[ ]] is not supported. Am I worrying too much? – Jorge Bucaran – 2015-03-09T07:33:34.523

1@JorgeBucaran It depends on whether you're going to use the same shell init files with multiple shells; if you're putting this in ~/.bashrc it'll only ever be run by bash, so it's not a problem; but if you want to put it in something like ~/.profile that'd be read by a generic POSIX shell then it'd be better to stick to only the basic POSIX-required shell features. – Gordon Davisson – 2015-03-09T17:54:14.687

1Then 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. – Ivan Yurchenko – 2017-02-21T11:29:09.410

27I care. – Paused until further notice. – 2009-09-12T03:30:32.090

1This doesn't work correctly if $1 is the final entry in the path before calling pathadd. Because $1 doesn't exist within $PATH with a : before and a : after it, it's not found and $1 is appended to the path. It will only add the $1 to the path a second time, though. – Neil – 2013-04-05T21:38:19.507

4@Neil: It does work, because it compares with ":$PATH:" instead of just "$PATH" – Gordon Davisson – 2013-04-05T21:43:53.670

3@GordonDavisson: I apologize, my test was wrong and you are correct. – Neil – 2013-04-05T23:06:42.187

2@GordonDavisson What is the point of the stuff in the curly brackets. I can't seem to puzzle it out "${PATH:+"$PATH:"}$1" – boatcoder – 2013-12-21T00:21:41.893

5@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). – Gordon Davisson – 2013-12-21T01:47:18.570

21

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

Guillaume Perrault-Archambault

Posted 2009-09-11T16:19:11.790

Reputation: 308

3This 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 – 2014-08-15T21:07:05.413

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! – Guillaume Perrault-Archambault – 2016-12-15T23:30:00.577

14

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
}

Paused until further notice.

Posted 2009-09-11T16:19:11.790

Reputation: 86 075

This worked for me only using $1 instead of ${1} – Andrei – 2012-09-05T19:25:13.607

@Andrei: Yes, the braces are unnecessary in this instance. I'm not sure why I included them. – Paused until further notice. – 2012-09-05T19:31:22.303

10

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.

Christopher Smith

Posted 2009-09-11T16:19:11.790

Reputation: 209

This puts the addition at the end. It is often desirable to add to the beginning in order to override existing locations. – Paused until further notice. – 2014-11-20T21:02:23.950

@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. – Christopher Smith – 2014-12-30T09:48:05.287

1@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. – Jesse Chisholm – 2015-11-10T21:17:20.767

@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. – Scott – 2017-08-22T18:22:23.143

1Comments support formatting using the backtick only but you don't get any decent paragraph control. – boatcoder – 2013-09-24T21:44:42.113

7

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

Posted 2009-09-11T16:19:11.790

Reputation: 71

This fails in the unusual case that $1 is the only entry (no colons). The entry becomes doubled. – Paused until further notice. – 2014-11-20T21:05:36.263

It also deletes too aggressively as pointed out by PeterS6g.

– Paused until further notice. – 2014-11-20T21:13:39.257

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 – 2013-10-25T11:01:22.963

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... – andybuckley – 2013-10-27T23:21:55.220

6

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

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

The single argument to this function is prepended to the PATH, 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 added as a duplicate.

The function works by prepending a colon to the path to ensure that all entries have a colon at the front, and then prepending the new entry to the existing path with that entry removed. The last part is performed using bash's ${var//pattern/sub} notation; see the bash manual for more details.

Rob Hague

Posted 2009-09-11T16:19:11.790

Reputation: 161

Good thought; flawed implementation.  Consider what happens if you already have /home/robert in your PATH and you pathadd /home/rob. – Scott – 2017-08-22T20:35:05.463

5

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

Posted 2009-09-11T16:19:11.790

Reputation: 45 216

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
}

PeterS6g

Posted 2009-09-11T16:19:11.790

Reputation: 51

4

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.

user4358

Posted 2009-09-11T16:19:11.790

Reputation:

Using "grep -x" is probably faster than the loop I put in my bash function. – Doug Harris – 2009-09-11T18:04:00.863

2

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

Posted 2009-09-11T16:19:11.790

Reputation: 121

(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. – Scott – 2017-08-22T16:58:09.710

(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 – 2017-08-22T17:16:39.793

(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 – 2017-08-22T17:22:45.127

(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) – Scott – 2017-08-26T20:36:55.620

(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) – Scott – 2017-08-26T20:36:58.587

(Cont’d) …  (4) No, . is most certainly *not* a canonical name. If you look at the GNU Coreutils documentation for readlink, you’ll see that ‘Canonicalize mode’ means that readlink outputs the absolute name of the given files which contain no ‘.’ or ‘..’ components nor any repeated separators (‘/’) or symbolic links — so canonical names are absolute pathnames.

– Scott – 2017-08-26T20:37:01.630

(Cont’d) …  But, BTW, I don’t even understand what point you’re trying to make by asking whether . is a canonical name. What does that have to do with what we’re talking about? – Scott – 2017-08-26T20:38:12.360

I used the numbering ((1), (2)...) to refer to the corresponding points you made, I wasn't trying to make new points.

I think you got a little bit defesnive there, I was legitimately asking for the sake of learning, nothing else.

Can you refer me to the part of readlink's documentation that says I'm not supposed to be using it like this? Top answers in other questions suggest that it's the way to go. https://stackoverflow.com/questions/4045253/converting-relative-path-into-absolute-path#answer-4045442

As for whether my answer is 'complete' or not, feel free to 'complete' it if need be

– qwertyzw – 2017-08-29T20:31:38.070

(I) I’m sorry if I was defensive, but when you said “I mean, if at the very least the name of the command implied/hinted at its purpose, ...”, I interpreted it as “The author(s) of readlink gave it a stupid name that doesn’t imply or even hint at its purpose. It’s their fault that my answer doesn’t handle all cases well.” which seems combative, and I was reacting to that. If I misread your attitude, I’m sorry; feel free to post a revision. … (Cont’d) – Scott – 2017-09-03T20:37:29.040

(Cont’d) …  (II) And I didn’t say that you were using readlink correctly, per se; I said (or I meant to say) that your overall solution was flawed, with a flawed design that looked like it could be traced to an imperfect understanding of what readlink does. Note that the Stack Overflow thread that you linked to says “This also has the side-effect of resolving all symlinks. This may or may not be desirable, ...”, which is just the point I was making. (III) In accordance with your invitation, I have edited your answer. – Scott – 2017-09-03T20:37:32.613

Of course that last comment should say, “I didn’t say that you were using readlink *in* correctly...”. – Scott – 2017-09-03T21:08:20.770

@Scott Wow! thank you so much! You're a great teacher :) – qwertyzw – 2017-09-11T19:56:38.093

You’re welcome / thank you.  I hope somebody learns something from this.    :-)    ⁠ – Scott – 2017-09-12T03:22:24.027

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

Posted 2009-09-11T16:19:11.790

Reputation: 23 578

1This may fail if any directory name already in PATH contains whitespace, *, ?, or []. – Scott – 2017-08-22T18:06:30.690

Good point... but I'm an old-school linux guy and wouldn't ever use a path with whitespace in it :-) – Doug Harris – 2017-08-22T19:09:24.350

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. – Scott – 2017-08-22T20:28:54.140

2

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

Jonathan Leffler

Posted 2009-09-11T16:19:11.790

Reputation: 4 526

1

function __path_add(){  
if [ -d "$1" ] ; then  
    local D=":${PATH}:";   
    [ "${D/:$1:/:}" == "$D" ] && PATH="$PATH:$1";  
    PATH="${PATH/#:/}";  
    export PATH="${PATH/%:/}";  
fi  
}  

GreenFox

Posted 2009-09-11T16:19:11.790

Reputation: 151

1

This way works fine:

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

Akceptor

Posted 2009-09-11T16:19:11.790

Reputation: 121

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.

Big McLargeHuge

Posted 2009-09-11T16:19:11.790

Reputation: 449

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

Posted 2009-09-11T16:19:11.790

Reputation: 1 348

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!)

andybuckley

Posted 2009-09-11T16:19:11.790

Reputation: 141

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
}

Daniel Patru

Posted 2009-09-11T16:19:11.790

Reputation: 226

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

Posted 2009-09-11T16:19:11.790

Reputation: 189

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) – Scott – 2017-08-23T21:49:04.817

(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) – Scott – 2017-08-23T21:49:08.037

(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. – Scott – 2017-08-23T21:49:11.007

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
}

Thorsten Lorenz

Posted 2009-09-11T16:19:11.790

Reputation: 161