Setting Bash Environment Variables
From Software By Jeff
It happened one time too many that some shell script modified the PATH to insert its bin directory, and then incorrectly exported the environment variable. Two crimes were committed here. First the path is incorrectly exported, persisting its changes longer than nessecary, and the second is blindly adding paths to the environment.
The first and easier to dismiss was the unnecessary export of the environment variable. When your shell script has a block that looks something like the following
#! /bin/bash
PATH=/path/to/app:${PATH}
export PATH
you've done a disservice with the export. I'm sure the intent was to make sure the PATH correctly allowed the "application" to run, but the export isn't necessary. The PATH is altered, and will remain so for the life of the script.
| Table of contents |
Use Export Correctly
At the end of the script, without the export, the PATH will not affect the rest of your session. We want to do this so we don't end up with very long PATH environment variables while we work. This can be corrected by removing the export.
At the end of the script, with the export, the PATH will remain and replace the PATH of the environment that called the script. This might be desired, but probably not. If we call this script several times, our PATH will end up looking like /path/to/app:/path/to/app:/path/to/app:/path/to/app:${PATH} after calling it for the fourth time. Certainly not what anyone wanted.
Prevent PATH Repetition
This brings us to the second disservice. By simply adding your directory to the PATH, exported or not, there is no way to ensure that it's in there only once. Sure the second and even third addition might not make a big deal, and maybe someone will argue that bash is smart enough to figure out that you've done it, but let's come up with a way to keep the PATH clean.
It may be the case that your script exists purely to update an environment variable like this. Consider your .bash_profie that builds on the system's defaults. Innocently you may end up with a PATH like /usr/bin:/usr/bin because both the default and your script update the PATH with the same items.
In the bash shell we have a powerful tool to help us avoid this. We can use string replacement to remove the previous instance or instances of a directory in the PATH, or other similar variable.
Consider PATH=/usr/bin:${PATH//\/usr\/bin/} when writing your new PATH, instead of simply PATH=/usr/bin:${PATH} to put your desired directory in the list. The string replacement will remove all previous instances of /usr/bin from the variable, and then the assignment will insert the directory in the beginning of the path.
In detail, the shell has incredible processing power, and we'll use some of that here. Looking at the ${variable//string/string} form we'll see how we can turn that to our advantage and keep our PATH and other variables neat and tidy.
Normally we just put $PATH or ${PATH} (which helps delimit the variable name, especially useful when in the middle of other text) in our script and let the shell expand that for us.
Adding the slash-style search-and-replace we can better control our output. The format takes our variable name (PATH), and notes we want to do a string replace with the first slash (or two slashes for a global replace, as we have) and second slash noting the string we're searching for, replacing it with the string after the second slash through the end of our string. That is, ${PATH/foo/fee} would look through our PATH variable and give us the string with the first instance of "foo" with "fee" instead, while ${PATH//foo/fee} would replace all "foo" with "fee" throughout the string. Note that this does not change the PATH, but returns what it would be. Let's work through an example.
The old way might have the statement PATH=~/bin:/usr/local/bin:/usr/bin:${PATH} to put our home directory's bin directory before the /usr/local/bin directory before the /usr/bin directory, before whatever the path was before. Very common. If the PATH was empty before, we get the new PATH of /home/username/bin:/usr/local/bin:usr/bin: while if the PATH were /bin before it is now /home/username/bin:/usr/local/bin:usr/bin:/bin. Nothing new here. Running this a second time, however, we get /home/username/bin:/usr/local/bin:usr/bin:/home/username/bin:/usr/local/bin:usr/bin: plus whatever PATH was originally. As noted before, ugly and unnecessary.
If we instead break out our PATH reassignment in a few lines instead of one, we get a neater, better-controlled PATH.
PATH=/usr/bin:${PATH//\/usr\/bin/}
PATH=/usr/local/bin:${PATH//\/usr\/local\/bin/}
PATH=~/bin:${PATH//${HOME}\/bin/}
Let's step through this one bit at a time. Assume that we've done it once, and our PATH when we start is /home/username/bin:/usr/local/bin:usr/bin:/bin when we start.
The first line, PATH=/usr/bin:${PATH//\/usr\/bin/} takes our PATH and removes the /usr/bin from it. Note the goofy \/ in the search part of our search-and-replace string. This is to help the shell know that the slash is part of our string, and not the delimiter of search-and-replace. Note also that after the final / (following \/bin) there is nothing before the ending brace. This tells us we're replacing our string, when found, with nothing. Note also that we're using a global search and replace, so it'll find all instances of /usr/bin in the PATH and remove them. This gives us /home/username/bin:/usr/local/bin::/bin as a result, to which we prepend /usr/bin finally resulting in usr/bin:/home/username/bin:/usr/local/bin::/bin that we assign to the PATH variable.
The next line, PATH=/usr/local/bin:${PATH//\/usr\/local\/bin/} likewise, removes /usr/local/bin from the path, and prepends it. First the string from the previous assigment usr/bin:/home/username/bin:/usr/local/bin::/bin is searched and the string /usr/local/bin is replaced with nothing, resulting in usr/bin:/home/username/bin:::/bin to which we prepend /usr/local/bin again, resulting in /usr/local/bin:usr/bin:/home/username/bin:::/bin that is assigned to PATH.
The final line, PATH=~/bin:${PATH//${HOME}\/bin/} will then remove our home directory's bin from the PATH and prepend it back again. First the string from the previous assignment /usr/local/bin:usr/bin:/home/username/bin:::/bin is searched. Note that the ${HOME} is used in the search portion instead of ~/bin as the ~ replacement doesn't seem to happen in the middle of that evaluation, but we are allowed to use the HOME variable, which should be expanded the same way; inconsistently, however, the tilde is expanded during the assignment, and the PATH will reflect the full path and not the tilde shortcut. Searching and replacing this with nothing results in the string /usr/local/bin:usr/bin::::/bin to which we prepend the path and assign it to our PATH resulting in /home/username/bin:/usr/local/bin:usr/bin::::/bin instead of the repeated paths.
Annoyingly, there are all of the colons in the block where our strings were removed. You can experiment with where to place the colons in your search and replace string, but you might find that you either duplicate searching before and after, like PATH=~/bin:${PATH//:${HOME}\/bin/}; PATH=~/bin:${PATH//${HOME}\/bin:/} or you may miss the string. Or you may end up with more complicated searches than I want to discuss. This might be required for something that would be part of a smaller string, like /bin which could logically require PATH=/bin:${PATH//:\/bin/} but probably should be avoided for other path entries. See the later discussion on /bin.
The method presented leaves a few doubled-up colons that can be removed with the one-liner loop while [ -n "`echo $PATH | grep ::`" ]; do PATH=${PATH//::/:}; done which would give us ultimately /home/username/bin:/usr/local/bin:usr/bin:/bin from the last result above. Nice and neat. The while loop searches the PATH for "::" by enlisting the help of grep. The search and replace then removes all double-colons replacing them with single colons. However, the search and replace isn't terribly intelligent, so after the first pass of our sample string we end up with /home/username/bin:/usr/local/bin:usr/bin::/bin as it replaces first the first pair, then the second pair out of the original string, not seeing that it has created a pair as a result. Looping allows us to keep going until it's all done.
What happens, however, if the search-and-replace removes the last entry, leaving the path ending in a colon? This tidbit, PATH=${PATH%:} will remove the colon by using the string truncating ${PATH%string} where the percent symbol says take off the string if it ends with the character. Note if the string ended with a different string, nothing is done.
Now, combining those things we can end up with this bit instead, which puts only one instance of a directory in the PATH, and makes sure it's neat and tidy.
PATH=/usr/bin:${PATH//\/usr\/bin/}
PATH=/usr/local/bin:${PATH//\/usr\/local\/bin/}
PATH=~/bin:${PATH//${HOME}\/bin/}
while [ -n "`echo $PATH | grep ::`" ]; do PATH=${PATH//::/:}; done
PATH=${PATH%:}
Of course, you can prepend and postpend as you would normally. If you, for example, always wanted /usr/bin at the end of the path, simply change its line to PATH=${PATH//\/usr\/bin/}:/usr/bin
Note that this works for other similarly formatted directory-based paths like the lD_LIBRARY_PATH and MANPATH. On my system I use the PATH as a guide and make sure, by the same method, that my LD_LIBRARY_PATH and MANPATH contain the related directories in the same order; that way if there is an over-riding entry (/usr/local/bin/tar always comes to mind...without care I get the less feature-filled /bin/tar), I'll get the corresponding information.
Replacement Alternative
Instead of rearranging the path every time, which is certainly guaranteed to maintain your order only when all of the items are introduced, we can use the string replacement to conditionally add items to the PATH. It may be the case that we don't care where the desired directory is in the order of the PATH, just that it's there.
Try if [ "${PATH}" = "${PATH//\/usr\/local\/ant\/bin/}" ]; then PATH=/usr/local/ant/bin:${PATH}; fi to make sure that your directory exists, adding it only if necessary.
What to do About /bin
After reviewing this, I thought about how to handle the special case of /bin which is a valid directory, but is a substring of most other valid directories. We can't simply search and replace all /bin entries and remove them, because then /usr/local/bin becomes /usr/local, which is not the same thing. We can't assume that it's in the middle, either and search only for :/bin: nor do we want to assume that it's at the beginning or end.
Sadly, the only solution I could deliver is to do all three.
# Eliminate :/bin from the end of the path
PATH=${PATH%:/bin}
# Remove /bin: from the beginning of the path
PATH=${PATH#/bin:}
# Replace :/bin: from the middle of the path
PATH=${PATH//:\/bin:/:}:/bin
Since, as mentioned, /bin contains the base for programs that are often replaced, I always put this at the end of my path. If the program I'm looking for is no where else, then use the /bin copy.
Special thanks to
The Linux Documentation Project (http://www.tldp.org) on which I found the Reference Card (http://www.tldp.org/LDP/abs/html/refcards.html) that showed the bash string manipulations.