What is the most elegant way to remove a path from a $ PATH variable in Bash?

Or, as a rule, how to remove an item from a colon separated list in a Bash environment variable?

I thought I saw an easy way to do this many years ago, using more advanced forms of expanding the Bash variable, but if so, I lost it. A quick Google search showed surprisingly few relevant results, and none of them would call "simple" or "elegant." For example, two methods using sed and awk, respectively:

PATH=$(echo $PATH | sed -e 's;:\?/home/user/bin;;' -e 's;/home/user/bin:\?;;') PATH=!(awk -F: '{for(i=1;i<=NF;i++){if(!($i in a)){a[$i];printf s$i;s=":"}}}'<<<$PATH) 

There is nothing simple? Is there anything similar to the split () function in Bash?

Update:
It seems like I need to apologize for my intentionally vague question; I was less interested in solving a particular use case than provoking a good discussion. Fortunately, I got it!

There are very smart methods here. In the end, I added the following three functions to my toolbox. The magic happens in path_remove, which is largely based on Martin Yorke's ability to use the awk RS variable.

 path_append () { path_remove $1; export PATH="$PATH:$1"; } path_prepend () { path_remove $1; export PATH="$1:$PATH"; } path_remove () { export PATH=`echo -n $PATH | awk -v RS=: -v ORS=: '$0 != "'$1'"' | sed 's/:$//'`; } 

The only real breakthrough in this case is using sed to remove the back colon. Given how fair the rest of Martin’s decision is, I completely agree to live with it!




Related Question: How do I manipulate $ PATH elements in shell scripts?

+96
bash shell path variable-expansion list-processing
Dec 15 '08 at 23:19
source share
34 answers
  • one
  • 2

Minute with awk:

 # Strip all paths with SDE in them. # export PATH=`echo ${PATH} | awk -v RS=: -v ORS=: '/SDE/ {next} {print}'` 

Edit: Reply to comments below:

 $ export a="/a/b/c/d/e:/a/b/c/d/g/k/i:/a/b/c/d/f:/a/b/c/g:/a/b/c/d/g/i" $ echo ${a} /a/b/c/d/e:/a/b/c/d/f:/a/b/c/g:/a/b/c/d/g/i ## Remove multiple (any directory with a: all of them) $ echo ${a} | awk -v RS=: -v ORS=: '/a/ {next} {print}' ## Works fine all removed ## Remove multiple including last two: (any directory with g) $ echo ${a} | awk -v RS=: -v ORS=: '/g/ {next} {print}' /a/b/c/d/e:/a/b/c/d/f: ## Works fine: Again! 

Edit in response to a security issue: (this does not apply to the question)

 export PATH=$(echo ${PATH} | awk -v RS=: -v ORS=: '/SDE/ {next} {print}' | sed 's/:*$//') 

This removes any trailing colons left after deleting the last entries, which will effectively add . to your way.

+41
Dec 16 '08 at 0:40
source share

My dirty hack:

 echo ${PATH} > t1 vi t1 export PATH=$(cat t1) 
+51
Dec 16 '08 at 0:05
source share

Since the big problem with the substitution is the end cases, how about the end cases not differing from other cases? If there were already colons at the beginning and end of the path, we could just find the desired line wrapped in colons. Be that as it may, we can easily add these colons and remove them afterwards.

 # PATH => /bin:/opt/a dir/bin:/sbin WORK=:$PATH: # WORK => :/bin:/opt/a dir/bin:/sbin: REMOVE='/opt/a dir/bin' WORK=${WORK/:$REMOVE:/:} # WORK => :/bin:/sbin: WORK=${WORK%:} WORK=${WORK#:} PATH=$WORK # PATH => /bin:/sbin 

Pure bash :).

+38
Jan 21 '10 at 10:48
source share

Here is the simplest solution that I can develop:

 #!/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[*]}" 

In the above example, any element in $ PATH containing "usr" will be deleted. You can replace "* usr *" with "/ home / user / bin" to remove only this item.

sschuberth update

Even if I think that spaces in $PATH are a terrible idea, here is a solution that handles it:

 PATH=$(IFS=':';t=($PATH);n=${#t[*]};a=();for ((i=0;i<n;i++)); do p="${t[i]%%*usr*}"; [ "${p}" ] && a[i]="${p}"; done;echo "${a[*]}"); 

or

 IFS=':' t=($PATH) n=${#t[*]} a=() for ((i=0;i<n;i++)); do p="${t[i]%%*usr*}" [ "${p}" ] && a[i]="${p}" done echo "${a[*]}" 
+25
Dec 16 '08 at 1:27
source share

Here is a one-line one, which, despite the current one and the highest rating , does not add invisible characters for PATH and can handle paths that contain spaces:

 export PATH=$(p=$(echo $PATH | tr ":" "\n" | grep -v "/cygwin/" | tr "\n" ":"); echo ${p%:}) 

Personally, I also find it easy to read / understand, and it uses only general commands instead of using awk.

+10
Oct 30
source share

Here is a solution that:

  • is a pure bash,
  • does not call other processes (for example, "sed" or "awk"),
  • does not change IFS ,
  • Doesn't call a subcommand
  • processes paths with spaces and
  • removes all occurrences of the argument in PATH .

     removeFromPath () {
        local pd
        p = ": $ 1:"
        d = ": $ PATH:"
        d = $ {d // $ p /:}
        d = $ {d / #: /}
        PATH = $ {d /%: /}
     } 
+8
Mar 20 '15 at 4:22
source share

function __path_remove () {
local D = ": $ {PATH}:";
["$ {D /: $ 1: /:}"! = "$ D"] && & & PATH = "$ {D /: $ 1: /:}";
PATH = "$ {PATH / #: /}";
export PATH = "$ {PATH /%: /}"

}

Dig it out of my .bashrc file. When you play with PATH and get lost, awk / sed / grep becomes inaccessible :-)

+6
Aug 13 2018-12-12T00: 00Z
source share

The best clean bash option I've found so far is this:

 function path_remove { PATH=${PATH/":$1"/} # delete any instances in the middle or at the end PATH=${PATH/"$1:"/} # delete any instances at the beginning } 

This is based on the not entirely correct answer to Add a directory to $ PATH if it does not already exist over the superuser.

+6
Oct 25 '13 at 10:59
source share

I just used the functions in the bash distribution that have been there, apparently, since 1991. They are still in the bash-docs package on Fedora and are used for use in /etc/profile , but no more ...

 $ rpm -ql bash-doc |grep pathfunc /usr/share/doc/bash-4.2.20/examples/functions/pathfuncs $ cat $(!!) cat $(rpm -ql bash-doc |grep pathfunc) #From: "Simon J. Gerraty" <sjg@zen.void.oz.au> #Message-Id: <199510091130.VAA01188@zen.void.oz.au> #Subject: Re: a shell idea? #Date: Mon, 09 Oct 1995 21:30:20 +1000 # NAME: # add_path.sh - add dir to path # # DESCRIPTION: # These functions originated in /etc/profile and ksh.kshrc, but # are more useful in a separate file. # # SEE ALSO: # /etc/profile # # AUTHOR: # Simon J. Gerraty <sjg@zen.void.oz.au> # @(#)Copyright (c) 1991 Simon J. Gerraty # # This file is provided in the hope that it will # be of use. There is absolutely NO WARRANTY. # Permission to copy, redistribute or otherwise # use this file is hereby granted provided that # the above copyright notice and this notice are # left intact. # 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;:\$;;"` } 
+5
Mar 08 2018-12-12T00:
source share

I wrote an answer to this here (using awk too). But I'm not sure what you are looking for? At least it seems to me what he is doing, instead of trying to fit into one line. However, for a simple one ruler that only removes things, I recommend

 echo $PATH | tr ':' '\n' | awk '$0 != "/bin"' | paste -sd: 

Replacement

 echo $PATH | tr ':' '\n' | awk '$0 != "/bin"; $0 == "/bin" { print "/bar" }' | paste -sd: 

or (shorter but less readable)

 echo $PATH | tr ':' '\n' | awk '$0 == "/bin" { print "/bar"; next } 1' | paste -sd: 

In any case, on the same issue and a number of useful answers, see here .

+4
Dec 16 '08 at 0:13
source share

Well, in bash, since it supports regex, I would just do:

 PATH=${PATH/:\/home\/user\/bin/} 
+3
Dec 15 '08 at 23:23
source share

Yes, adding a colon at the end of PATH, for example, makes deleting a path a little less awkward and error prone.

 path_remove () { declare i newPATH newPATH="${PATH}:" for ((i=1; i<=${#@}; i++ )); do #echo ${@:${i}:1} newPATH="${newPATH//${@:${i}:1}:/}" done export PATH="${newPATH%:}" return 0; } path_remove_all () { declare i newPATH shopt -s extglob newPATH="${PATH}:" for ((i=1; i<=${#@}; i++ )); do newPATH="${newPATH//+(${@:${i}:1})*([^:]):/}" #newPATH="${newPATH//+(${@:${i}:1})*([^:])+(:)/}" done shopt -u extglob export PATH="${newPATH%:}" return 0 } path_remove /opt/local/bin /usr/local/bin path_remove_all /opt/local /usr/local 
+1
Jan 21 '10 at 19:58
source share

If you are concerned about removing duplicates in $ PATH, the most elegant way, IMHO, will not add them in the first place. In 1 line:

 if ! $( echo "$PATH" | tr ":" "\n" | grep -qx "$folder" ) ; then PATH=$PATH:$folder ; fi 
Folder

$ can be replaced with anything and may contain spaces ("/ home / user / my documents")

+1
Feb 19 '11 at 5:11
source share

The most elegant clean bash solution I have found so far:

 pathrm () { local IFS=':' local newpath local dir local pathvar=${2:-PATH} for dir in ${!pathvar} ; do if [ "$dir" != "$1" ] ; then newpath=${newpath:+$newpath:}$dir fi done export $pathvar="$newpath" } pathprepend () { pathrm $1 $2 local pathvar=${2:-PATH} export $pathvar="$1${!pathvar:+:${!pathvar}}" } pathappend () { pathrm $1 $2 local pathvar=${2:-PATH} export $pathvar="${!pathvar:+${!pathvar}:}$1" } 
+1
Apr 07 2018-12-12T00:
source share

Most of the other proposed solutions depend only on line matching and do not take into account route segments containing special names, such as . , .. or ~ . The bash function below resolves directory lines in its argument and in path segments to search for logical directory matches as well as string matches.

 rm_from_path() { pattern="${1}" dir='' [ -d "${pattern}" ] && dir="$(cd ${pattern} && pwd)" # resolve to absolute path new_path='' IFS0=${IFS} IFS=':' for segment in ${PATH}; do if [[ ${segment} == ${pattern} ]]; then # string match continue elif [[ -n ${dir} && -d ${segment} ]]; then segment="$(cd ${segment} && pwd)" # resolve to absolute path if [[ ${segment} == ${dir} ]]; then # logical directory match continue fi fi new_path="${new_path}${IFS}${segment}" done new_path="${new_path/#${IFS}/}" # remove leading colon, if any IFS=${IFS0} export PATH=${new_path} } 

Test:

 $ mkdir -p ~/foo/bar/baz ~/foo/bar/bif ~/foo/boo/bang $ PATH0=${PATH} $ PATH=~/foo/bar/baz/.././../boo/././../bar:${PATH} # add dir with special names $ rm_from_path ~/foo/boo/../bar/. # remove same dir with different special names $ [ ${PATH} == ${PATH0} ] && echo 'PASS' || echo 'FAIL' 
+1
Sep 22 '14 at 23:16
source share

Scratch Linux defines three Bash functions in /etc/profile :

 # Functions to help us manage paths. Second argument is the name of the # path variable to be modified (default: PATH) pathremove () { local IFS=':' local NEWPATH local DIR local PATHVARIABLE=${2:-PATH} for DIR in ${!PATHVARIABLE} ; do if [ "$DIR" != "$1" ] ; then NEWPATH=${NEWPATH:+$NEWPATH:}$DIR fi done export $PATHVARIABLE="$NEWPATH" } pathprepend () { pathremove $1 $2 local PATHVARIABLE=${2:-PATH} export $PATHVARIABLE="$1${!PATHVARIABLE:+:${!PATHVARIABLE}}" } pathappend () { pathremove $1 $2 local PATHVARIABLE=${2:-PATH} export $PATHVARIABLE="${!PATHVARIABLE:+${!PATHVARIABLE}:}$1" } export -f pathremove pathprepend pathappend 

Link: http://www.linuxfromscratch.org/blfs/view/svn/postlfs/profile.html

+1
Nov 22 '15 at 10:00
source share

I like the three features shown in the @BenBlank update to his original question. To summarize them, I use a two-argument form that allows me to set PATH or any other environment variable that I want:

 path_append () { path_remove $1 $2; export $1="${!1}:$2"; } path_prepend () { path_remove $1 $2; export $1="$2:${!1}"; } path_remove () { export $1="`echo -n ${!1} | awk -v RS=: -v ORS=: '$1 != "'$2'"' | sed 's/:$//'`"; } 

Examples of using:

 path_prepend PATH /usr/local/bin path_append PERL5LIB "$DEVELOPMENT_HOME/p5/src/perlmods" 

Note that I also added some quotation marks to allow the proper handling of path names containing spaces.

+1
Jan 05 '16 at 20:54 on
source share

What is the most elegant way to remove a path from a $ PATH variable in Bash?

What is more elegant than awk?

 path_remove () { export PATH=`echo -n $PATH | awk -v RS=: -v ORS=: '$0 != "'$1'"' | sed 's/:$//'`; 

Python! This is a more readable and supported solution, and it is easy to check to make sure that it really does what you want.

Suppose you want to delete the first element of a path?

 PATH="$(echo "$PATH" | python -c "import sys; path = sys.stdin.read().split(':'); del path[0]; print(':'.join(path))")" 

(Instead of moving from echo , os.getenv['PATH'] will be a little shorter and will have the same result as above, but I'm worried that Python can do something with this environment variable, so probably , best to skip it directly from the environment you care about.)

Similar to deleting from the end:

 PATH="$(echo "$PATH" | python -c "import sys; path = sys.stdin.read().split(':'); del path[-1]; print(':'.join(path))")" 

To make these reusable shell functions that you can, for example, paste into your .bashrc file:

 strip_path_first () { PATH="$(echo "$PATH" | python -c "import sys; path = sys.stdin.read().split(':'); del path[0]; print(':'.join(path))")" } strip_path_last () { PATH="$(echo "$PATH" | python -c "import sys; path = sys.stdin.read().split(':'); del path[-1]; print(':'.join(path))")" } 
+1
Jul 10 '16 at 13:08
source share

Since this tends to be quite problematic, since there is no elegant way in it, I recommend avoiding the problem by rearranging the solution: create your PATH rather than trying to tear it down.

I could be more specific if I knew your real problem context. In the meantime, I will use the software assembly as a context.

A common problem with software crashes is that it breaks down on some machines, ultimately due to how someone set up their default shell (PATH and other environment variables). An elegant solution is to ensure that your build scripts are protected by fully defining the shell environment. Build build scripts to set PATH and other environment variables based on the elements you compile, such as the location of the compiler, libraries, tools, components, etc. Make each custom element so that you can individually install, verify, and then use it appropriately in the script.

For example, I have a Maven-based Java assembly Java, which is inherited from my new employer. The script design is known for being fragile, and another new employee, and I spent three weeks (not full time, just here and there, but still many hours) making it work on our machines. The essential step was that I took control of PATH so that I knew exactly which Java, to which Maven, and to which WebLogic was called. I created environment variables to point to each of these tools, then I calculated PATH based on those that were added by several others. Similar methods tamed other customizable parameters until we finally created a reproducible assembly.

By the way, do not use Maven, Java is in order and buy WebLogic if you absolutely need to cluster it (but otherwise not, and especially not its proprietary functions).

Regards.

0
Dec 16 '08 at 0:21
source share

As with @litb, I introduced the answer to the question " How do I manipulate $ PATH elements in shell scripts , so there is my main answer.

The split functionality in bash and other Bourne shell derivatives is most efficiently achieved with $IFS , a field separator. For example, to set positional arguments ( $1 , $2 , ...) to PATH elements, use:

 set -- $(IFS=":"; echo "$PATH") 

It will work fine if there are no spaces in $ PATH. To make it work for path elements containing spaces is a non-trivial exercise - for the interested reader. It is probably easier to handle using a scripting language such as Perl.

I also have a script, clnpath , which I widely use to set my PATH. I registered it in response to Avoiding Duplication of the PATH Variable in csh ".

0
Dec 16 '08 at 0:43
source share

What makes this problem annoying are the cases of fencepost among the first and last elements. The problem can be elegantly solved by changing the IFS and using an array, but I don't know how to re-enter the colon as soon as the path is converted to an array form.

Here is a slightly less elegant version that removes a single directory from $PATH using string manipulation only. I tested it.

 #!/bin/bash # # remove_from_path dirname # # removes $1 from user $PATH if [ $# -ne 1 ]; then echo "Usage: $0 pathname" 1>&2; exit 1; fi delendum="$1" NEWPATH= xxx="$IFS" IFS=":" for i in $PATH ; do IFS="$xxx" case "$i" in "$delendum") ;; # do nothing *) [ -z "$NEWPATH" ] && NEWPATH="$i" || NEWPATH="$NEWPATH:$i" ;; esac done PATH="$NEWPATH" echo "$PATH" 
0
Dec 16 '08 at 2:53
source share

Here's a single-line Perl:

 PATH=`perl -e '$a=shift;$_=$ENV{PATH};s#:$a(:)|^$a:|:$a$#$1#;print' /home/usr/bin` 

The $a variable gets the path to delete. The s (substitute) and print commands implicitly work with the $_ variable.

0
Dec 16 '08 at 19:54
source share

Good stuff here. I use this option so as not to add deceivers to it.

 #!/bin/bash # ###################################################################################### # # Allows a list of additions to PATH with no dupes # # Patch code below into your $HOME/.bashrc file or where it # will be seen at login. # # Can also be made executable and run as-is. # ###################################################################################### # add2path=($HOME/bin .) ## uncomment space separated list if [ $add2path ]; then ## skip if list empty or commented out for nodup in ${add2path[*]} do case $PATH in ## case block thanks to MIKE511 $nodup:* | *:$nodup:* | *:$nodup ) ;; ## if found, do nothing *) PATH=$PATH:$nodup ## else, add it to end of PATH or esac ## *) PATH=$nodup:$PATH prepend to front done export PATH fi ## debug add2path echo echo " PATH == $PATH" echo 
0
Jan 13 '10 at 1:35
source share

With the expanded submenu, you can do the following:

 # delete all /opt/local paths in PATH shopt -s extglob printf "%s\n" "${PATH}" | tr ':' '\n' | nl printf "%s\n" "${PATH//+(\/opt\/local\/)+([^:])?(:)/}" | tr ':' '\n' | nl man bash | less -p extglob 
0
Jan 20 '10 at 18:28
source share

Extended pull-up single-line (well, sort of):

 path_remove () { shopt -s extglob; PATH="${PATH//+(${1})+([^:])?(:)/}"; export PATH="${PATH%:}"; shopt -u extglob; return 0; } 

There seems to be no need to hide slashes at $ 1.

 path_remove () { shopt -s extglob; declare escArg="${1//\//\\/}"; PATH="${PATH//+(${escArg})+([^:])?(:)/}"; export PATH="${PATH%:}"; shopt -u extglob; return 0; } 
0
Jan 21 '10 at 10:14
source share

Adding colons to PATH we could also do something like:

 path_remove () { declare i newPATH # put a colon at the beginning & end AND double each colon in-between newPATH=":${PATH//:/::}:" for ((i=1; i<=${#@}; i++)); do #echo ${@:${i}:1} newPATH="${newPATH//:${@:${i}:1}:/}" # s/:\/fullpath://g done newPATH="${newPATH//::/:}" newPATH="${newPATH#:}" # remove leading colon newPATH="${newPATH%:}" # remove trailing colon unset PATH PATH="${newPATH}" export PATH return 0 } path_remove_all () { declare i newPATH extglobVar extglobVar=0 # enable extended globbing if necessary [[ ! $(shopt -q extglob) ]] && { shopt -s extglob; extglobVar=1; } newPATH=":${PATH}:" for ((i=1; i<=${#@}; i++ )); do newPATH="${newPATH//:+(${@:${i}:1})*([^:])/}" # s/:\/path[^:]*//g done newPATH="${newPATH#:}" # remove leading colon newPATH="${newPATH%:}" # remove trailing colon # disable extended globbing if it was enabled in this function [[ $extglobVar -eq 1 ]] && shopt -u extglob unset PATH PATH="${newPATH}" export PATH return 0 } path_remove /opt/local/bin /usr/local/bin path_remove_all /opt/local /usr/local 
0
Jan 22 '10 at 17:45
source share

In path_remove_all (via proxxy):

 -newPATH="${newPATH//:+(${@:${i}:1})*([^:])/}" +newPATH="${newPATH//:${@:${i}:1}*([^:])/}" # s/:\/path[^:]*//g 
0
Jan 22 '10 at 18:34
source share

Although this is a very old thread, I thought this solution might be of interest:

 PATH="/usr/lib/ccache:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games" REMOVE="ccache" # whole or part of a path :) export PATH=$(IFS=':';p=($PATH);unset IFS;p=(${p[@]%%$REMOVE});IFS=':';echo "${p[*]}";unset IFS) echo $PATH # outputs /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games 

found it in this blog post . I think I like it the most :)

0
Jun 22 2018-11-21T00:
source share

I took a slightly different approach than most people here, and focused only on string manipulations, for example:

 path_remove () { if [[ ":$PATH:" == *":$1:"* ]]; then local dirs=":$PATH:" dirs=${dirs/:$1:/:} export PATH="$(__path_clean $dirs)" fi } __path_clean () { local dirs=${1%?} echo ${dirs#?} } 

The above example is a simplified example of the final functions that I use. I also created path_add_before and path_add_after , allowing you to insert the path before / after the specified path already in PATH.

A full feature set is available in path_helpers.sh in dotfiles . They fully support deleting / adding / adding / inserting at the beginning / middle / end of the PATH line.

0
Feb 06 '12 at 1:16
source share

The final ':' is caused by the fact that you are setting the end of the line, not the delimiter. I use resource-limited units and like to pack everything into one script, without these oddities:

 path_remove () { PATH="$(echo -n $PATH | awk -v RS=: -v ORS= '$0 != "'$1'"{print s _ $0;s=":"}')" } 
0
May 16 '14 at 19:36
source share
  • one
  • 2



All Articles