Каков самый элегантный способ удалить путь из переменной $PATH в Bash?

или в более общем плане, как удалить элемент из списка, разделенного двоеточием, в переменной среды Bash?

Я думал, что видел простой способ сделать это много лет назад, используя более продвинутые формы расширения переменной Bash, но если это так, я потерял его из виду. Быстрый поиск Google дал удивительно мало релевантных результатов, и ни один из них я бы не назвал "простым" или "элегантным". Например, два метода с использованием sed и awk, соответственно:

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)

ничего не делает прямолинейное существование? Есть ли что-нибудь аналогичное функции split() в Bash?

обновление:
Похоже, мне нужно извиниться за мой намеренно-неопределенный вопрос; я был менее заинтересован в решении конкретного прецедента, чем в провоцировании хорошей дискуссии. К счастью, у меня получилось!

есть некоторые очень умные методы здесь. В конце концов, я добавил следующие три функции в моем арсенале. Магия происходит в path_remove, который основан в основном на умном использовании Мартина Йорка awkпеременная RS.

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

единственный реальный cruft там является использование sed чтобы удалить конечную двоеточие. Учитывая, насколько прямолинейна остальная часть решения Мартина, я вполне готов жить с этим!


вопрос: Как манипулировать элементами $PATH в сценариях оболочки?

30 ответов


минута с awk:

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

Edit: это ответ на комментарии ниже:

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

редактировать в ответ на проблему безопасности: (это не относится к вопросу)

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

это удаляет любые конечные двоеточия, оставшиеся, удалив последние записи, которые эффективно добавят . в свой путь.


мой грязный хак:

echo ${PATH} > t1
vi t1
export PATH=$(cat t1)

поскольку большая проблема с подстановкой-это конечные случаи, как насчет того, чтобы сделать конечные случаи не отличающимися от других случаев? Если путь уже имел двоеточия в начале и конце, мы могли бы просто искать нужную строку, обернутую двоеточиями. Как бы то ни было, мы можем легко добавить эти двоеточия и удалить их впоследствии.

# 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 :).


вот самое простое решение я могу придумать:

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

в приведенном выше примере будет удален любой элемент в $PATH, содержащий "usr". Вы можете заменить "* usr* " на "/ home/user / bin", чтобы удалить только этот элемент.

обновление per sschuberth

хотя я думаю, что пробелы в $PATH - это ужасно идея, вот решение, которое обрабатывает это:

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[*]}");

или

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

вот однострочный, который, несмотря на текущий принят и высокий рейтинг ответы, не добавляет невидимые символы в путь и может справиться с путями, которые содержат пробелы:

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

лично я также считаю, что это легко читать / понимать, и он включает только общие команды вместо использования awk.


функции __путь_удалить(){
локальный D=":${PATH}:";
["${D/:$1:/:}" != "$D"] & & PATH= " ${D/:$1:/:}";
PATH= " ${PATH/#:/}";
export PATH= " ${PATH/%:/}";
}

выкопал его из моего .файл bashrc. Когда вы играете с PATH, и он теряется, awk/sed / grep становится недоступным :-)


лучший вариант pure bash, который я нашел до сих пор, следующий:

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

это основано на не совсем правильный ответ to добавить каталог в $PATH, если он еще не существует на суперпользователя.


вот это решение:

  • это чистый Баш,
  • не вызывает другие процессы (например, " sed " или "awk"),
  • не меняет IFS,
  • не вилка суб-оболочки,
  • обрабатывает пути с пробелами, и
  • удаляет все вхождения аргумента в PATH.

    removeFromPath() {
       local p d
       p="::"
       d=":$PATH:"
       d=${d//$p/:}
       d=${d/#:/}
       PATH=${d/%:/}
    }

Я только что использовал функции в распределении bash, которые были там, по-видимому, с 1991 года. Они все еще находятся в пакете bash-docs на Fedora и используются в /etc/profile, но не более...

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

Ну, в bash, поскольку он поддерживает регулярное выражение, я бы просто сделал:

PATH=${PATH/:\/home\/user\/bin/}

Я написал ответ на это здесь (используя awk тоже). Но я не уверен, что это то, что вы ищете? По крайней мере, мне кажется ясным, что он делает, а не пытается вписаться в одну линию. Для простого лайнера, который удаляет только вещи, я рекомендую

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

замена

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

или (короче, но менее читаемый)

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

В любом случае, для того же вопроса и много полезных ответов см. здесь.


да, помещая двоеточие в конце пути, например, делает удаление пути немного менее неуклюжим и подверженным ошибкам.

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 

Если вы обеспокоены удалением дубликаты в $PATH, самым элегантным способом, ИМХО, было бы не добавлять их в первую очередь. В 1 строку:

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

$папка может быть заменена чем угодно и может содержать пробелы ("/home/user/my documents")


самое элегантное чистое решение bash, которое я нашел на сегодняшний день:

pathrm () {                                                                      
  local IFS=':'                                                                  
  local newpath                                                                  
  local dir                                                                      
  local pathvar=${2:-PATH}                                                       
  for dir in ${!pathvar} ; do                                                    
    if [ "$dir" != "" ] ; then                                                 
      newpath=${newpath:+$newpath:}$dir                                          
    fi                                                                           
  done                                                                           
  export $pathvar="$newpath"                                                        
}

pathprepend () {                                                                 
  pathrm                                                                     
  local pathvar=${2:-PATH}                                                       
  export $pathvar="${!pathvar:+:${!pathvar}}"                                  
}

pathappend () {                                                                    
  pathrm                                                                     
  local pathvar=${2:-PATH}                                                       
  export $pathvar="${!pathvar:+${!pathvar}:}"                                  
} 

большинство других предлагаемых решений полагаются только на соответствие строк и не учитывают сегменты пути, содержащие специальные имена, такие как ., .. или ~. Функция bash ниже разрешает строки каталога в своем аргументе и в сегментах пути, чтобы найти логические совпадения каталогов, а также совпадения строк.

rm_from_path() {
  pattern=""
  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}
}

мне нравятся три функции, показанные в обновлении @BenBlank к его исходному вопросу. Чтобы обобщить их, я использую форму 2-аргумента, которая позволяет мне установить PATH или любую другую переменную среды, которую я хочу:

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

пример использования:

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

обратите внимание, что я также добавил некоторые кавычки, чтобы обеспечить правильную обработку имен путей, содержащих пробелы.


каков самый элегантный способ удалить путь из переменной $PATH в Bash?

что более элегантно, чем awk?

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

питон! Это более читаемое и поддерживаемое решение, и его легко проверить, чтобы увидеть, что он действительно делает то, что вы хотите.

Допустим, вы хотите удалить первый элемент пути?

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

(вместо трубопроводов из echo, os.getenv['PATH'] было бы немного короче, и при условии того же результата, что и выше, но я беспокоюсь, что Python может что-то сделать с этой переменной среды, поэтому, вероятно, лучше передать ее непосредственно из среды, о которой вы заботитесь.)

аналогично удалить с конца:

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

чтобы сделать эти многоразовые функции оболочки, которые вы можете, например, вставить в свой .файл bashrc:

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))")"
}

поскольку это, как правило, довольно проблематично, так как нет элегантного способа, я рекомендую избегать проблемы, переставляя решение: создайте свой путь, а не пытаться сорвать его.

Я мог бы быть более конкретным, если бы знал ваш реальный контекст проблемы. Тем временем я буду использовать сборку программного обеспечения в качестве контекста.

общая проблема со сборками программного обеспечения заключается в том, что он ломается на некоторых машинах, в конечном счете из-за того, как кто-то настроил свою оболочку по умолчанию (Путь и другие переменные среды). Элегантное решение состоит в том, чтобы сделать ваши сценарии сборки иммунными, полностью указав среду оболочки. Кодируйте сценарии сборки, чтобы задать путь и другие переменные среды на основе элементов сборки, которыми вы управляете, таких как расположение компилятора, библиотеки, инструменты, компоненты и т. д. Сделайте каждый настраиваемый элемент чем-то, что вы можете индивидуально установить, проверить и затем использовать в своем скрипте.

например, у меня есть Maven на основе WebLogic-целевой Java-сборки, которую я унаследовал от своего нового работодателя. Сценарий сборки печально известен своей хрупкостью, и еще один новый сотрудник и я потратили три недели (не полный рабочий день, только здесь и там, но все еще много часов), чтобы заставить его работать на наших машинах. Важным шагом было то, что я взял под контроль путь, чтобы точно знать, какая Java, какая Maven и какая WebLogic вызывается. Я создал переменные среды, чтобы указать на каждый из этих инструментов, затем я рассчитал путь, основанный на них плюс несколько других. Подобные методы приручили другие настраиваемые настройки, пока мы, наконец, не создали воспроизводимую сборку.

кстати, не используйте Maven, Java в порядке, и покупайте только WebLogic, если вам абсолютно нужна его кластеризация (но в противном случае нет, и особенно не его собственные функции).

наилучшие пожелания.


как и в случае с @litb, я дал ответ на вопрос"Как манипулировать элементами $PATH в сценариях оболочки", так что мой главный ответ там.

функциональность "Сплит" в bash и другие производные оболочки Борна наиболее аккуратно достигаются с $IFS "Интер" -разделитель полей. Например, чтобы задать позиционные аргументы (, , ...) для элементов пути используйте:

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

он будет работать нормально, пока нет пробелов в $ПУТЬ. Заставить его работать для элементов пути, содержащих пробелы, - нетривиальное упражнение, оставленное для заинтересованного читателя. Вероятно, проще справиться с этим, используя язык сценариев, такой как Perl.

у меня тоже есть скрипт clnpath, который я широко использую для установки моего пути. Я задокументировал это в ответе на "Как избежать дублирования переменной пути в csh".


что делает эту проблему раздражающей, это случаи fencepost среди первых и последних элементов. Проблема может быть элегантно решена путем изменения IFS и использования массива, но я не знаю, как повторно ввести двоеточие после преобразования пути в форму массива.

вот немного менее элегантная версия, которая удаляет один каталог из $PATH использование только строковых манипуляций. Я проверил его.

#!/bin/bash
#
#   remove_from_path dirname
#
#   removes  from user's $PATH

if [ $# -ne 1 ]; then
  echo "Usage:  pathname" 1>&2; exit 1;
fi

delendum=""
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"

вот Perl One-liner:

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

на $a переменная получает путь для удаления. The s (заменить) и print команды неявно работают на $_ переменной.


хороший материал здесь. Я использую это, чтобы не добавлять дураков в первую очередь.

#!/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

с расширенной поддержкой подстановка можно сделать следующее:

# 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

расширенные подстановка один-лайнер (ну, вроде):

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

Кажется, нет необходимости избегать косых черт в $1.

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

добавление двоеточий в путь мы также можем сделать что-то вроде:

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 

в path_remove_all (по proxxy):

-newPATH="${newPATH//:+(${@:${i}:1})*([^:])/}" 
+newPATH="${newPATH//:${@:${i}:1}*([^:])/}"        # s/:\/path[^:]*//g 

хотя это очень старый поток, я думал, что это решение может представлять интерес:

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

нашел его на этом блоге. Я думаю, что мне нравится больше всего :)


Я взял немного другой подход, чем большинство людей и сосредоточены конкретно на обработку строк, вот так:

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

выше приведен упрощенный пример конечных функций, которые я использую. Я также создал path_add_before и path_add_after позволяет вставить путь до / после указанного пути уже в PATH.

полный набор функций доступен в path_helpers.sh в моем dotfiles. Они полностью поддерживают удаление/добавление/добавление/вставка в начале/середине / конце строки пути.


трейлинг': 'вызван тем, что вы устанавливаете окончание строки, а не разделитель. Я использую ограниченные ресурсы и люблю упаковывать все в один скрипт, без этих странностей:

path_remove () {
    PATH="$(echo -n $PATH | awk -v RS=: -v ORS= ' != "''"{print s _ ;s=":"}')"
}

Это, конечно, элегантно, но он использует внешний sed. Кроме того, он удаляет все пути, содержащие строку поиска $1. Он также не оставляет болтаться: в конце в случае, если удаленный путь является последним на пути.

PATH=`echo $PATH | sed 's/:[^:]*[^:]*//g'`

эта альтернатива оставляет болтающийся финал: однако.

PATH=`echo $PATH | tr ":" "\n" | grep -v  | tr "\n" ":"`

альтернативы без backticks:

PATH=$(echo $PATH | sed 's/:[^:]*[^:]*//g')

PATH=$(echo $PATH | tr ":" "\n" | grep -v  | tr "\n" ":")