[ACCEPTED]-How do I manipulate $PATH elements in shell scripts?-path-variables
Addressing the proposed solution from dmckee:
- While some versions of Bash may allow hyphens in function names, others (MacOS X) do not.
- I don't see a need to use return immediately before the end of the function.
- I don't see the need for all the semi-colons.
- I don't see why you have path-element-by-pattern export a value. Think of
export
as equivalent to setting (or even creating) a global variable - something to be avoided whenever possible. - I'm not sure what you expect '
replace-path PATH $PATH /usr
' to do, but it does not do what I would expect.
Consider 41 a PATH value that starts off containing:
.
/Users/jleffler/bin
/usr/local/postgresql/bin
/usr/local/mysql/bin
/Users/jleffler/perl/v5.10.0/bin
/usr/local/bin
/usr/bin
/bin
/sw/bin
/usr/sbin
/sbin
The 40 result I got (from 'replace-path PATH $PATH /usr
') is:
.
/Users/jleffler/bin
/local/postgresql/bin
/local/mysql/bin
/Users/jleffler/perl/v5.10.0/bin
/local/bin
/bin
/bin
/sw/bin
/sbin
/sbin
I would have expected 39 to get my original path back since /usr 38 does not appear as a (complete) path element, only 37 as part of a path element.
This can be fixed 36 in replace-path
by modifying one of the sed
commands:
export $path=$(echo -n $list | tr ":" "\n" | sed "s:^$removestr\$:$replacestr:" |
tr "\n" ":" | sed "s|::|:|g")
I 35 used ':' instead of '|' to separate parts 34 of the substitute since '|' could (in theory) appear 33 in a path component, whereas by definition 32 of PATH, a colon cannot. I observe that 31 the second sed
could eliminate the current 30 directory from the middle of a PATH. That 29 is, a legitimate (though perverse) value 28 of PATH could be:
PATH=/bin::/usr/local/bin
After processing, the current 27 directory would no longer be on the PATH.
A 26 similar change to anchor the match is appropriate 25 in path-element-by-pattern
:
export $target=$(echo -n $list | tr ":" "\n" | grep -m 1 "^$pat\$")
I note in passing that grep -m 1
is not standard 24 (it is a GNU extension, also available on 23 MacOS X). And, indeed, the-n
option for echo
is 22 also non-standard; you would be better off 21 simply deleting the trailing colon that 20 is added by virtue of converting the newline 19 from echo into a colon. Since path-element-by-pattern 18 is used just once, has undesirable side-effects 17 (it clobbers any pre-existing exported variable 16 called $removestr
), it can be replaced sensibly by 15 its body. This, along with more liberal 14 use of quotes to avoid problems with spaces 13 or unwanted file name expansion, leads to:
# path_tools.bash
#
# A set of tools for manipulating ":" separated lists like the
# canonical $PATH variable.
#
# /bin/sh compatibility can probably be regained by replacing $( )
# style command expansion with ` ` style
###############################################################################
# Usage:
#
# To remove a path:
# replace_path PATH $PATH /exact/path/to/remove
# replace_path_pattern PATH $PATH <grep pattern for target path>
#
# To replace a path:
# replace_path PATH $PATH /exact/path/to/remove /replacement/path
# replace_path_pattern PATH $PATH <target pattern> /replacement/path
#
###############################################################################
# Remove or replace an element of $1
#
# $1 name of the shell variable to set (e.g. PATH)
# $2 a ":" delimited list to work from (e.g. $PATH)
# $3 the precise string to be removed/replaced
# $4 the replacement string (use "" for removal)
function replace_path () {
path=$1
list=$2
remove=$3
replace=$4 # Allowed to be empty or unset
export $path=$(echo "$list" | tr ":" "\n" | sed "s:^$remove\$:$replace:" |
tr "\n" ":" | sed 's|:$||')
}
# Remove or replace an element of $1
#
# $1 name of the shell variable to set (e.g. PATH)
# $2 a ":" delimited list to work from (e.g. $PATH)
# $3 a grep pattern identifying the element to be removed/replaced
# $4 the replacement string (use "" for removal)
function replace_path_pattern () {
path=$1
list=$2
removepat=$3
replacestr=$4 # Allowed to be empty or unset
removestr=$(echo "$list" | tr ":" "\n" | grep -m 1 "^$removepat\$")
replace_path "$path" "$list" "$removestr" "$replacestr"
}
I 12 have a Perl script called echopath
which I find 11 useful when debugging problems with PATH-like 10 variables:
#!/usr/bin/perl -w
#
# "@(#)$Id: echopath.pl,v 1.7 1998/09/15 03:16:36 jleffler Exp $"
#
# Print the components of a PATH variable one per line.
# If there are no colons in the arguments, assume that they are
# the names of environment variables.
@ARGV = $ENV{PATH} unless @ARGV;
foreach $arg (@ARGV)
{
$var = $arg;
$var = $ENV{$arg} if $arg =~ /^[A-Za-z_][A-Za-z_0-9]*$/;
$var = $arg unless $var;
@lst = split /:/, $var;
foreach $val (@lst)
{
print "$val\n";
}
}
When I run the modified solution 9 on the test code below:
echo
xpath=$PATH
replace_path xpath $xpath /usr
echopath $xpath
echo
xpath=$PATH
replace_path_pattern xpath $xpath /usr/bin /work/bin
echopath xpath
echo
xpath=$PATH
replace_path_pattern xpath $xpath "/usr/.*/bin" /work/bin
echopath xpath
The output is:
.
/Users/jleffler/bin
/usr/local/postgresql/bin
/usr/local/mysql/bin
/Users/jleffler/perl/v5.10.0/bin
/usr/local/bin
/usr/bin
/bin
/sw/bin
/usr/sbin
/sbin
.
/Users/jleffler/bin
/usr/local/postgresql/bin
/usr/local/mysql/bin
/Users/jleffler/perl/v5.10.0/bin
/usr/local/bin
/work/bin
/bin
/sw/bin
/usr/sbin
/sbin
.
/Users/jleffler/bin
/work/bin
/usr/local/mysql/bin
/Users/jleffler/perl/v5.10.0/bin
/usr/local/bin
/usr/bin
/bin
/sw/bin
/usr/sbin
/sbin
This 8 looks correct to me - at least, for my definition 7 of what the problem is.
I note that echopath LD_LIBRARY_PATH
evaluates 6 $LD_LIBRARY_PATH
. It would be nice if your functions were 5 able to do that, so the user could type:
replace_path PATH /usr/bin /work/bin
That 4 can be done by using:
list=$(eval echo '$'$path)
This leads to this 3 revision of the code:
# path_tools.bash
#
# A set of tools for manipulating ":" separated lists like the
# canonical $PATH variable.
#
# /bin/sh compatibility can probably be regained by replacing $( )
# style command expansion with ` ` style
###############################################################################
# Usage:
#
# To remove a path:
# replace_path PATH /exact/path/to/remove
# replace_path_pattern PATH <grep pattern for target path>
#
# To replace a path:
# replace_path PATH /exact/path/to/remove /replacement/path
# replace_path_pattern PATH <target pattern> /replacement/path
#
###############################################################################
# Remove or replace an element of $1
#
# $1 name of the shell variable to set (e.g. PATH)
# $2 the precise string to be removed/replaced
# $3 the replacement string (use "" for removal)
function replace_path () {
path=$1
list=$(eval echo '$'$path)
remove=$2
replace=$3 # Allowed to be empty or unset
export $path=$(echo "$list" | tr ":" "\n" | sed "s:^$remove\$:$replace:" |
tr "\n" ":" | sed 's|:$||')
}
# Remove or replace an element of $1
#
# $1 name of the shell variable to set (e.g. PATH)
# $2 a grep pattern identifying the element to be removed/replaced
# $3 the replacement string (use "" for removal)
function replace_path_pattern () {
path=$1
list=$(eval echo '$'$path)
removepat=$2
replacestr=$3 # Allowed to be empty or unset
removestr=$(echo "$list" | tr ":" "\n" | grep -m 1 "^$removepat\$")
replace_path "$path" "$removestr" "$replacestr"
}
The following revised 2 test now works too:
echo
xpath=$PATH
replace_path xpath /usr
echopath xpath
echo
xpath=$PATH
replace_path_pattern xpath /usr/bin /work/bin
echopath xpath
echo
xpath=$PATH
replace_path_pattern xpath "/usr/.*/bin" /work/bin
echopath xpath
It produces the same 1 output as before.
Reposting my answer to What is the most elegant way to remove a path from the $PATH variable in Bash? :
#!/bin/bash
IFS=:
# convert it to an array
t=($PATH)
unset IFS
# perform any array operations to remove elements from the array
t=(${t[@]%%*usr*})
IFS=:
# output the new array
echo "${t[*]}"
or the one-liner:
PATH=$(IFS=':';t=($PATH);unset IFS;t=(${t[@]%%*usr*});IFS=':';echo "${t[*]}");
0
For deleting an element you can use sed:
#!/bin/bash
NEW_PATH=$(echo -n $PATH | tr ":" "\n" | sed "/foo/d" | tr "\n" ":")
export PATH=$NEW_PATH
will 5 delete the paths that contain "foo" from 4 the path.
You could also use sed to insert 3 a new line before or after a given line.
Edit: you 2 can remove duplicates by piping through 1 sort and uniq:
echo -n $PATH | tr ":" "\n" | sort | uniq -c | sed -n "/ 1 / s/.*1 \(.*\)/\1/p" | sed "/foo/d" | tr "\n" ":"
There are a couple of relevant programs 19 in the answers to "How to keep from duplicating path variable in csh". They concentrate 18 more on ensuring that there are no repeated 17 elements, but the script I provide can be 16 used as:
export PATH=$(clnpath $head_dirs:$PATH:$tail_dirs $remove_dirs)
Assuming you have one or more directories 15 in $head_dirs and one or more directories 14 in $tail_dirs and one or more directories 13 in $remove_dirs, then it uses the shell 12 to concatenate the head, current and tail 11 parts into a massive value, and then removes 10 each of the directories listed in $remove_dirs 9 from the result (not an error if they don't 8 exist), as well as eliminating second and 7 subsequent occurrences of any directory 6 in the path.
This does not address putting 5 path components into a specific position 4 (other than at the beginning or end, and 3 those only indirectly). Notationally, specifying 2 where you want to add the new element, or 1 which element you want to replace, is messy.
Just a note that bash itself can do search 26 and replace. It can do all the normal "once 25 or all", cases [in]sensitive options you 24 would expect.
From the man page:
${parameter/pattern/string}
The pattern 23 is expanded to produce a pattern just as 22 in pathname expansion. Parameter is expanded 21 and the longest match of pattern against 20 its value is replaced with string. If Ipattern 19 begins with /
, all matches of pattern are 18 replaced with string. Normally only the 17 first match is replaced. If pattern begins 16 with #
, it must match at the beginning of 15 the expanded value of parameter. If pattern 14 begins with %
, it must match at the end of 13 the expanded value of parameter. If string 12 is null, matches of pattern are deleted 11 and the /
following pattern may be omitted. If 10 parameter is @
or *
, the substitution operation 9 is applied to each positional parameter 8 in turn, and the expansion is the resultant 7 list. If parameter is an array variable 6 subscripted with @
or
*
, the substitution 5 operation is applied to each member of the 4 array in turn, and the expansion is the 3 resultant list.
You can also do field splitting 2 by setting $IFS
(input field separator) to the 1 desired delimiter.
OK, thanks to all responders. I've prepared 8 an encapsulated version of florin's answer. The 7 first pass looks like this:
# path_tools.bash
#
# A set of tools for manipulating ":" separated lists like the
# canonical $PATH variable.
#
# /bin/sh compatibility can probably be regained by replacing $( )
# style command expansion with ` ` style
###############################################################################
# Usage:
#
# To remove a path:
# replace-path PATH $PATH /exact/path/to/remove
# replace-path-pattern PATH $PATH <grep pattern for target path>
#
# To replace a path:
# replace-path PATH $PATH /exact/path/to/remove /replacement/path
# replace-path-pattern PATH $PATH <target pattern> /replacement/path
#
###############################################################################
# Finds the _first_ list element matching $2
#
# $1 name of a shell variable to be set
# $2 name of a variable with a path-like structure
# $3 a grep pattern to match the desired element of $1
function path-element-by-pattern (){
target=$1;
list=$2;
pat=$3;
export $target=$(echo -n $list | tr ":" "\n" | grep -m 1 $pat);
return
}
# Removes or replaces an element of $1
#
# $1 name of the shell variable to set (i.e. PATH)
# $2 a ":" delimited list to work from (i.e. $PATH)
# $2 the precise string to be removed/replaced
# $3 the replacement string (use "" for removal)
function replace-path () {
path=$1;
list=$2;
removestr=$3;
replacestr=$4; # Allowed to be ""
export $path=$(echo -n $list | tr ":" "\n" | sed "s|$removestr|$replacestr|" | tr "\n" ":" | sed "s|::|:|g");
unset removestr
return
}
# Removes or replaces an element of $1
#
# $1 name of the shell variable to set (i.e. PATH)
# $2 a ":" delimited list to work from (i.e. $PATH)
# $2 a grep pattern identifying the element to be removed/replaced
# $3 the replacement string (use "" for removal)
function replace-path-pattern () {
path=$1;
list=$2;
removepat=$3;
replacestr=$4; # Allowed to be ""
path-element-by-pattern removestr $list $removepat;
replace-path $path $list $removestr $replacestr;
}
Still needs error 6 trapping in all the functions, and I should 5 probably stick in a repeated path solution 4 while I'm at it.
You use it by doing a . /include/path/path_tools.bash
in 3 the working script and calling on of the 2 the replace-path*
functions.
I am still open to new and/or 1 better answers.
This is easy using awk.
Replace
{
for(i=1;i<=NF;i++)
if($i == REM)
if(REP)
print REP;
else
continue;
else
print $i;
}
Start it using
function path_repl {
echo $PATH | awk -F: -f rem.awk REM="$1" REP="$2" | paste -sd:
}
$ echo $PATH
/bin:/usr/bin:/home/js/usr/bin
$ path_repl /bin /baz
/baz:/usr/bin:/home/js/usr/bin
$ path_repl /bin
/usr/bin:/home/js/usr/bin
Append
Inserts 9 at the given position. By default, it appends 8 at the end.
{
if(IDX < 1) IDX = NF + IDX + 1
for(i = 1; i <= NF; i++) {
if(IDX == i)
print REP
print $i
}
if(IDX == NF + 1)
print REP
}
Start it using
function path_app {
echo $PATH | awk -F: -f app.awk REP="$1" IDX="$2" | paste -sd:
}
$ echo $PATH
/bin:/usr/bin:/home/js/usr/bin
$ path_app /baz 0
/bin:/usr/bin:/home/js/usr/bin:/baz
$ path_app /baz -1
/bin:/usr/bin:/baz:/home/js/usr/bin
$ path_app /baz 1
/baz:/bin:/usr/bin:/home/js/usr/bin
Remove duplicates
This one keeps 7 the first occurences.
{
for(i = 1; i <= NF; i++) {
if(!used[$i]) {
print $i
used[$i] = 1
}
}
}
Start it like this:
echo $PATH | awk -F: -f rem_dup.awk | paste -sd:
Validate whether all elements exist
The 6 following will print an error message for 5 all entries that are not existing in the 4 filesystem, and return a nonzero value.
echo -n $PATH | xargs -d: stat -c %n
To 3 simply check whether all elements are paths 2 and get a return code, you can also use 1 test
:
echo -n $PATH | xargs -d: -n1 test -d
suppose
echo $PATH
/usr/lib/jvm/java-1.6.0/bin:lib/jvm/java-1.6.0/bin/:/lib/jvm/java-1.6.0/bin/:/usr/lib/qt-3.3/bin:/usr/lib/ccache:/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin:/sbin:/home/tvnadeesh/bin
If you want to remove /lib/jvm/java-1.6.0/bin/ do 3 like as below
export PATH=$(echo $PATH | sed 's/\/lib\/jvm\/java-1.6.0\/bin\/://g')
sed
will take input from echo $PATH
and 2 replace /lib/jvm/java-1.6.0/bin/: with empty
in 1 this way you can remove
- Order of PATH is not distrubed
- Handles corner cases like empty path, space in path gracefully
- Partial match of dir does not give false positives
- Treats path at head and tail of PATH in proper ways. No : garbage and such.
Say you have /foo:/some/path:/some/path/dir1:/some/path/dir2:/bar and 4 you want to replace /some/path Then 3 it correctly replaces "/some/path" but 2 leaves "/some/path/dir1" or "/some/path/dir2", as 1 what you would expect.
function __path_add(){
if [ -d "$1" ] ; then
local D=":${PATH}:";
[ "${D/:$1:/:}" == "$D" ] && PATH="$PATH:$1";
PATH="${PATH/#:/}";
export PATH="${PATH/%:/}";
fi
}
function __path_remove(){
local D=":${PATH}:";
[ "${D/:$1:/:}" != "$D" ] && PATH="${D/:$1:/:}";
PATH="${PATH/#:/}";
export PATH="${PATH/%:/}";
}
# Just for the shake of completeness
function __path_replace(){
if [ -d "$2" ] ; then
local D=":${PATH}:";
if [ "${D/:$1:/:}" != "$D" ] ; then
PATH="${D/:$1:/:$2:}";
PATH="${PATH/#:/}";
export PATH="${PATH/%:/}";
fi
fi
}
Related post What is the most elegant way to remove a path from the $PATH variable in Bash?
I prefer using ruby to the likes of awk/sed/foo 3 these days, so here's my approach to deal 2 with dupes,
# add it to the path
PATH=~/bin/:$PATH:~/bin
export PATH=$(ruby -e 'puts ENV["PATH"].split(/:/).uniq.join(":")')
create a function for reuse,
mungepath() {
export PATH=$(ruby -e 'puts ENV["PATH"].split(/:/).uniq.join(":")')
}
Hash, arrays 1 and strings in a ruby one liner :)
The first thing to pop into my head to change 31 just part of a string is a sed substitution.
example: if 30 echo $PATH => "/usr/pkg/bin:/usr/bin:/bin:/usr/pkg/games:/usr/pkg/X11R6/bin" then 29 to change "/usr/bin" to "/usr/local/bin" could 28 be done like this:
## produces standard output 27 file
## the "=" character is used instead 26 of slash ("/") since that would be messy, # alternative 25 quoting character should be unlikely in 24 PATH
## the path separater character ":" is 23 both removed and re-added here, # might 22 want an extra colon after the last path
echo 21 $PATH | sed '=/usr/bin:=/usr/local/bin:='
This 20 solution replaces an entire path-element 19 so might be redundant if new-element is 18 similar.
If the new PATH'-s aren't dynamic 17 but always within some constant set you 16 could save those in a variable and assign as 15 needed:
PATH=$TEMP_PATH_1; # commands ... ; \n PATH=$TEMP_PATH_2; # commands 14 etc... ;
Might not be what you were thinking. some 13 of the relevant commands on bash/unix would 12 be:
pushd popd cd ls # maybe l -1A for single 11 column; find grep which # could confirm 10 that file is where you think it came from; env type
..and 9 all that and more have some bearing on PATH 8 or directories in general. The text altering 7 part could be done any number of ways!
Whatever 6 solution chosen would have 4 parts:
1) fetch 5 the path as it is 2) decode the path to 4 find the part needing changes 3) determing 3 what changes are needed/integrating those 2 changes 4) validation/final integration/setting 1 the variable
In line with dj_segfault's answer, I do this in scripts that 6 append/prepend environment variables that 5 might be executed multiple times:
ld_library_path=${ORACLE_HOME}/lib
LD_LIBRARY_PATH=${LD_LIBRARY_PATH//${ld_library_path}?(:)/}
export LD_LIBRARY_PATH=${ld_library_path}${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}
Using this 4 same technique to remove, replace or manipulate 3 entries in PATH is trivial given the filename-expansion-like 2 pattern matching and pattern-list support 1 of shell parameter expansion.
More Related questions
We use cookies to improve the performance of the site. By staying on our site, you agree to the terms of use of cookies.