A more comfortable way to edit a long $PATH?

35

14

I want to add, in ~/.bashrc, a few directories to my $PATH.

My $PATH is quite long so it's a bit hard to see what directories it contains and in what order.

I know I can modify my ~/.bashrc to be:

PATH=$PATH:/some/dir
PATH=$PATH:/another/dir:/yet/another
PATH=$PATH:/and/another
...

it'd make it easier to read. But I was wondering if during the last years Bash has acquired some syntax that makes specifying a long PATH easier. E.g., I'm fantasizing about a syntax similar to:

PATH=:((
  /some/dir
  /another/dir
  /yet/another
  /and/another
  ...
))

I know such syntax is invalid. I was wondering if there's something as easy. Is there?

Niccolo M.

Posted 2015-11-15T13:37:27.603

Reputation: 724

The traditional tutorial to set the path via PATH=foo:$PATH seems wrong because it keep growth every time source ~/.bashrc and even exec bash can't help since the $PATH is export. – 林果皞 – 2019-08-15T05:44:37.687

Answers

25

I use a set of convenience functions for prepending or appending a path to a variable. The functions come in the distribution tarball for Bash in a contrib file called "pathfuncs".

  • add_path will add the entry to the end of the PATH variable
  • pre_path will add the entry to the beginning of the PATH variable
  • del_path will remove the entry from the PATH variable, wherever it is

If you specify a variable as the second argument, it will use that instead of PATH.

For convenience, here they are:

# is $1 missing from $2 (or PATH) ?
no_path() {
    eval "case :\$${2-PATH}: in *:$1:*) return 1;; *) return 0;; esac"
}
# if $1 exists and is not in path, append it
add_path () {
  [ -d ${1:-.} ] && no_path $* && eval ${2:-PATH}="\$${2:-PATH}:$1"
}
# if $1 exists and is not in path, prepend it
pre_path () {
  [ -d ${1:-.} ] && no_path $* && eval ${2:-PATH}="$1:\$${2:-PATH}"
}
# if $1 is in path, remove it
del_path () {
  no_path $* || eval ${2:-PATH}=`eval echo :'$'${2:-PATH}: |
    sed -e "s;:$1:;:;g" -e "s;^:;;" -e "s;:\$;;"`
}

If you add those to your bash startup file, you can add to your PATH like this:

pre_path $HOME/bin
add_path /sbin
add_path /usr/sbin

Or specify a different variable:

pre_path $HOME/man MANPATH
pre_path $HOME/share/man MANPATH
add_path /usr/local/man MANPATH
add_path /usr/share/man MANPATH

I use this method in my rc files putting the pre_paths first and the add_paths second. It makes all of my path changes easy to understand at a glance. Another benefit is that the lines are short enough that I can add a trailing comment on a line if necessary.

And since these are functions, you can use them interactively from the command line, such as by saying add_path $(pwd) to add the current directory to the path.

Starfish

Posted 2015-11-15T13:37:27.603

Reputation: 838

Thanks. I've checked your code and it works. Surprisingly, I also found a use for del_path (A "." creeps into my PATH in some situations, devil knows from where, so I did del_path .). – Niccolo M. – 2015-11-15T22:17:58.750

Hi. it is possible to source (include) this pathfuncs from bashrc script or should I copy/paste them to there ? – Cristiano – 2016-02-01T20:36:38.443

@Cristiano Either will work. It's really up to you. – Starfish – 2016-02-09T02:26:52.873

11

OK, I figured out the following solution, which I think is elegant (as far as shell syntax go). It uses Bash's array syntax and also a neat way to join elements:

paths=(
  /some/dir
  /another/dir
  '/another/dir with spaces in it'
  /yet/another
  /and/another
  /end
)
paths_joined=$( IFS=: ; echo "${paths[*]}" )

PATH=$paths_joined:$PATH

ALERT!

It turns out that this solution has a problem: Unlike the solutions of @terdon and @Starfish, it doesn't first check whether the paths are already in PATH. So, since I want to put this code in ~/.bashrc (and not in ~/.profile), duplicate paths will creep into PATH. So don't use this solution (unless you put it in ~/.profile (or, better, ~/.bash_profile as it has Bash specific syntax)).

Niccolo M.

Posted 2015-11-15T13:37:27.603

Reputation: 724

1Very nice. Can you please accept an answer so others don't come here to offer a solution when you've already found one? Thanks – Basic – 2015-11-15T17:08:07.290

Duplicate paths aren't really a problem. It's highly unlikely that you would have add enough directories to PATH to actually cause performance issues (especially since the shell caches successful lookups). – chepner – 2015-11-16T20:52:53.133

5

I use the function below in my ~/.bashrc. It is something I got from a sysadmin in my old lab but I don't think he wrote them. Just add these lines to your ~/.profile or ~/.bashrc:

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

This has various advantages:

  • Adding new directories to the $PATH is trivial: pathmunge /foo/bar;
  • It avoids duplicated entries;
  • You can choose whether to add a new entry to the beginning (pathmunge /foo/bar or the end (pathmunge /foo/bar after) of the $PATH.

Your shell's initialization file would then contain something like:

pathmunge /some/dir
pathmunge /another/dir
pathmunge '/another/dir with spaces in it'
pathmunge /yet/another
pathmunge /and/another
pathmunge /end

terdon

Posted 2015-11-15T13:37:27.603

Reputation: 45 216

Thanks. But I'll pick @Starfish's solution because his doesn't spawn grep. – Niccolo M. – 2015-11-15T22:13:23.530

2@NiccoloM. no problem, accept which ever you prefer. Be careful with starfish's approach though, it will execute arbitrary code through the eval so you could cause some serious damage if you run it with the wrong argument. – terdon – 2015-11-15T22:31:12.300

Note that faster function exists in redhat to do that without external command grep, see https://bugzilla.redhat.com/show_bug.cgi?id=544652#c7

– 林果皞 – 2019-08-18T00:19:42.333

4

I want to add, in ~/.bashrc, a few directories to my $PATH.

I use the following in Cygwin. It should work in other versions of bash. You can remove the unset PATH to build on your current PATH (if you do this you may have to figure out how to add the : separators correctly).

Note:

  • I once had this functionality in a bash function but lost it after a disk crash.

In my .bash_profile:

# Build up the path using the directories in ~/.path_elements
unset PATH
while read line; do 
  PATH="${PATH}$line"; 
done < ~/.path_elements

...

# Add current directory to path
export PATH=".:${PATH}"

In ~/.path_elements:

/home/DavidPostill/bin:
/usr/local/bin:
/usr/bin:
/c/Windows/system32:
/c/Windows

DavidPostill

Posted 2015-11-15T13:37:27.603

Reputation: 118 938

Thanks. Your answer inspired me to work on akin solution. (To my taste storing the paths in a separate file is a bother.) – Niccolo M. – 2015-11-15T15:55:52.520

1

I use this in my .bashrc (and also my .zshrc, since I typically use zsh where available instead of bash). Granted, it requires me to manually add directories, but an advantage is that as I update it, I can keep copying it to new servers and not worry about the PATH on a new server being created with directories that don't exist there.

##
## PATH
##
## Instead of just clobbering our PATH with directories that may
## not be appropriate for this server, try to be intelligent about what we add
##
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
[ -d /cs/sbin ]                  && PATH=/cs/sbin:$PATH
[ -d /cs/bin ]                   && PATH=/cs/bin:$PATH
[ -d /usr/ucb ]                  && PATH=$PATH:/usr/ucb
[ -d /usr/ccs/bin ]              && PATH=$PATH:/usr/ccs/bin
[ -d /usr/local/ssl/bin ]        && PATH=$PATH:/usr/local/ssl/bin
[ -d /usr/krb5/bin ]             && PATH=$PATH:/usr/krb5/bin
[ -d /usr/krb5/sbin ]            && PATH=$PATH:/usr/krb5/sbin
[ -d /usr/kerberos/sbin ]        && PATH=$PATH:/usr/kerberos/sbin
[ -d /usr/kerberos/bin ]         && PATH=$PATH:/usr/kerberos/bin
[ -d /cs/local/jdk1.5.0/bin ]    && PATH=$PATH:/cs/local/jdk1.5.0/bin
[ -d /usr/java/jre1.5.0_02/bin ] && PATH=$PATH:/usr/java/jre1.5.0_02/man
[ -d /usr/java1.2/bin ]          && PATH=$PATH:/usr/java1.2/bin
[ -d /cs/local/perl5.8.0/bin ]   && PATH=$PATH:/cs/local/perl5.8.0/bin
[ -d /usr/perl5/bin ]            && PATH=$PATH:/usr/perl5/bin
[ -d /usr/X11R6/bin ]            && PATH=$PATH:/usr/X11R6/bin
[ -d /etc/X11 ]                  && PATH=$PATH:/etc/X11
[ -d /opt/sfw/bin ]              && PATH=$PATH:/opt/sfw/bin
[ -d /usr/local/apache/bin ]     && PATH=$PATH:/usr/local/apache/bin
[ -d /usr/apache/bin ]           && PATH=$PATH:/usr/apache/bin
[ -d /cs/admin/bin ]             && PATH=$PATH:/cs/admin/bin
[ -d /usr/openwin/bin ]          && PATH=$PATH:/usr/openwin/bin
[ -d /usr/xpg4/bin ]             && PATH=$PATH:/usr/xpg4/bin
[ -d /usr/dt/bin ]               && PATH=$PATH:/usr/dt/bin

I do the same thing for my MANPATH:

##
## MANPATH
##
## Instead of just clobbering our MANPATH with directories that may
## not be appropriate for this server, try to be intelligent about what we add
##
MANPATH=/usr/local/man
[ -d /usr/share/man ]            && MANPATH=$MANPATH:/usr/share/man
[ -d /usr/local/share/man ]      && MANPATH=$MANPATH:/usr/local/share/man
[ -d /usr/man ]                  && MANPATH=$MANPATH:/usr/man
[ -d /cs/man ]                   && MANPATH=$MANPATH:/cs/man
[ -d /usr/krb5/man ]             && MANPATH=$MANPATH:/usr/krb5/man
[ -d /usr/kerberos/man ]         && MANPATH=$MANPATH:/usr/kerberos/man
[ -d /usr/local/ssl/man ]        && MANPATH=$MANPATH:/usr/local/ssl/man
[ -d /cs/local/jdk1.5.0/man ]    && MANPATH=$MANPATH:/cs/local/jdk1.5.0/man
[ -d /usr/java/jre1.5.0_02/man ] && MANPATH=$MANPATH:/usr/java/jre1.5.0_02/man
[ -d /usr/java1.2/man ]          && MANPATH=$MANPATH:/usr/java1.2/man
[ -d /usr/X11R6/man ]            && MANPATH=$MANPATH:/usr/X11R6/man
[ -d /usr/local/apache/man ]     && MANPATH=$MANPATH:/usr/local/apache/man
[ -d /usr/local/mysql/man ]      && MANPATH=$MANPATH:/usr/local/mysql/man
[ -d /cs/local/perl5.8.0/man ]   && MANPATH=$MANPATH:/cs/local/perl5.8.0/man
[ -d /usr/perl5/man ]            && MANPATH=$MANPATH:/usr/perl5/man
[ -d /usr/local/perl/man ]       && MANPATH=$MANPATH:/usr/local/perl/man
[ -d /usr/local/perl5.8.0/man ]  && MANPATH=$MANPATH:/usr/local/perl5.8.0/man
[ -d /usr/openwin/man ]          && MANPATH=$MANPATH:/usr/openwin/man

In addition to having a single file that I can copy to systems in disparate environments without fear of adding non-existant directories to the PATH, this approach also has the advantage of allowing me to specify the order I want directories to appear in the PATH. Since the first line of each definition completely redefines the PATH variable, I can update my .bashrc and source it after the edit to have my shell updated without adding duplicate entries (which I used to experience a long time ago when I simply started with "$PATH=$PATH:/new/dir". This ensures I get a clean copy in the order I desire.

Brian Snook

Posted 2015-11-15T13:37:27.603

Reputation: 11

1suggesting an alternative: d="/usr/share/man" ; [ -d "$d" ] && MANPATH="$MANPATH:${d}" It will be shorter, and easier to add a new dir (just copy a line and edit the first "d=...." part). However, for the PATH one, I think you'll end up with far too many dirs in your PATH, which is not always good (what if some "foo" command exist in one of the less known paths, and do something entirely different that what a regular user would expect?) – Olivier Dulac – 2015-11-16T18:00:05.510

The OP asked for a more terse way to add paths. This is far more verbose and more prone to error than what they were already trying to avoid. – underscore_d – 2015-11-17T12:12:21.887

-1

There is an easy way! Read Shell Functions and Path Variables at Linux Journal, Mar 01, 2000 By Stephen Collyer

The functions let me use a new data type in my bash environment - the colon separated list. In addition to PATH, I use them to adjust my LOCATE_PATH, MANPATH, and others, and as a general datatype in bash programming. Here's how I set up my PATH (using the functions):

# Add my bin directory to $PATH at the beginning, so it overrides 
addpath -f -p PATH $HOME/bin

# For Raspberry Pi development (add at end)
addpath -b -p PATH ${HOME}/rpi/tools/arm-bcm2708/gcc-linaro-arm-linux-gnueabihf-raspbian-x64/bin

# remove nonexistent directories from PATH
delpath -n -p PATH

# ensure PATH contains unique entries
uniqpath -p PATH

Since the Linux Journal link is referred to as "broken", I've put the Bash Path Functions in a .shar file at http://pastebin.ubuntu.com/13299528/

waltinator

Posted 2015-11-15T13:37:27.603

Reputation: 159

3What functions are these? How does the OP use them? Presumably they are described in the broken link in your answer which is precisely why we always want answers to be self contained. Please edit and include the actual functions in your answer. – terdon – 2015-11-15T19:21:39.267

@terdon: link works for me, I put a .shar file on pastebin, I won't post 1K lines here. – waltinator – 2015-11-25T00:31:57.730

Whoops, works for me too now. It didn't when I left the comment. Anyway, the gist of my comment was that we try to avoid linking to external resources in answers. We want the information to be here where it can be updated, edited and is immune to link rot. – terdon – 2015-11-25T00:45:46.747