Как мне манипулировать элементами $ PATH в сценариях оболочки? - PullRequest
29 голосов
/ 08 ноября 2008

Есть ли идиоматический способ удаления элементов из PATH-подобных переменных оболочки?

То есть я хочу взять

PATH=/home/joe/bin:/usr/local/bin:/usr/bin:/bin:/path/to/app/bin:.

и удалить или заменить /path/to/app/bin без заусеницы остальной переменной. Дополнительные очки за то, что я могу поставить новые элементы в произвольные позиции. Цель будет распознаваема по четко определенной строке и может появиться в любой точке списка.

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

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


В данном контексте контекст заключается в необходимости удобного переключения между несколькими версиями (одна для анализа, другая для работы на платформе) большого пакета для научного анализа, который производит пару десятков исполняемых файлов, хранит данные вокруг файловой системы и использует переменную окружения, чтобы помочь найти все эти вещи. Я хотел бы написать скрипт, который выбирает версию и должен иметь возможность удалять элементы $PATH, относящиеся к текущей активной версии, и заменять их теми же элементами, которые относятся к новой версии.


Это связано с проблемой предотвращения повторяющихся элементов $PATH при повторном запуске сценариев входа в систему и т. П.


Ответы [ 12 ]

19 голосов
/ 16 ноября 2008

Решение предложенного решения от dmckee:

  1. Хотя некоторые версии Bash могут разрешать дефисы в именах функций, другие (MacOS X) этого не делают.
  2. Я не вижу необходимости использовать return непосредственно перед окончанием функции.
  3. Я не вижу необходимости во всех точках с запятой.
  4. Я не понимаю, почему у вас есть путь-элемент-по-шаблону для экспорта значения. Считайте export эквивалентным установке (или даже созданию) глобальной переменной - чего-то, чего следует избегать при любой возможности.
  5. Я не уверен, что вы ожидаете от 'replace-path PATH $PATH /usr', но он не делает то, что я ожидал.

Рассмотрим значение PATH, которое начинается с:

.
/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

Результат, который я получил (от 'replace-path PATH $PATH /usr'):

.
/Users/jleffler/bin
/local/postgresql/bin
/local/mysql/bin
/Users/jleffler/perl/v5.10.0/bin
/local/bin
/bin
/bin
/sw/bin
/sbin
/sbin

Я ожидал получить свой исходный путь обратно, так как / usr не отображается как (полный) элемент пути, только как часть элемента пути.

Это можно исправить в replace-path, изменив одну из команд sed:

export $path=$(echo -n $list | tr ":" "\n" | sed "s:^$removestr\$:$replacestr:" |
               tr "\n" ":" | sed "s|::|:|g")

Я использовал ':' вместо '|' отделить части замены, так как '|' может (в теории) появляться в компоненте пути, тогда как по определению PATH двоеточие не может. Я заметил, что второй sed может удалить текущий каталог из середины PATH. То есть допустимое (хотя и извращенное) значение PATH может быть:

PATH=/bin::/usr/local/bin

После обработки текущий каталог больше не будет находиться в PATH.

Аналогичное изменение для привязки совпадения уместно в path-element-by-pattern:

export $target=$(echo -n $list | tr ":" "\n" | grep -m 1 "^$pat\$")

Попутно отмечу, что grep -m 1 не является стандартным (это расширение GNU, также доступное в MacOS X). И действительно, опция -n для echo также нестандартна; Вам было бы лучше просто удалить завершающий двоеточие, которое добавлено путем преобразования новой строки из echo в двоеточие. Так как путь элемент-за-шаблоном используется только один раз, имеет нежелательные побочные эффекты (он забивает любую существующую ранее экспортированную переменную, называемую $removestr), его можно разумно заменить своим телом. Это, наряду с более либеральным использованием кавычек во избежание проблем с пробелами или нежелательным расширением имени файла, приводит к:

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

У меня есть сценарий Perl echopath, который я считаю полезным при отладке проблем с переменными, подобными PATH:

#!/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";
    }
}

Когда я запускаю модифицированное решение по тестовому коду ниже:

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

Вывод:

.
/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

Мне это кажется правильным - по крайней мере, для моего определения, в чем проблема.

Замечу, что echopath LD_LIBRARY_PATH оценивает $LD_LIBRARY_PATH. Было бы хорошо, если бы ваши функции могли это сделать, чтобы пользователь мог набрать:

replace_path PATH /usr/bin /work/bin

Это можно сделать с помощью:

list=$(eval echo '$'$path)

Это приводит к пересмотру кода:

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

Теперь работает и следующий исправленный тест:

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

Он выдает тот же вывод, что и раньше.

5 голосов
/ 17 декабря 2008

Повторный ответ на мой вопрос Какой самый элегантный способ удалить путь из переменной $ PATH в 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=$(IFS=':';t=($PATH);unset IFS;t=(${t[@]%%*usr*});IFS=':';echo "${t[*]}");
3 голосов
/ 08 ноября 2008

Для удаления элемента вы можете использовать sed:

#!/bin/bash
NEW_PATH=$(echo -n $PATH | tr ":" "\n" | sed "/foo/d" | tr "\n" ":")
export PATH=$NEW_PATH

удалит пути, содержащие "foo", из пути.

Вы также можете использовать sed для вставки новой строки до или после заданной строки.

Редактировать: вы можете удалить дубликаты, отправив через sort и uniq:

echo -n $PATH | tr ":" "\n" | sort | uniq -c | sed -n "/ 1 / s/.*1 \(.*\)/\1/p" | sed "/foo/d" | tr "\n" ":"
2 голосов
/ 16 ноября 2008

Просто обратите внимание, что сам bash может выполнять поиск и замену. Он может выполнять все обычные «один или все» варианты, чувствительные к регистру [in], которые вы ожидаете.

со страницы руководства:

$ {параметр / шаблон / строка}

Шаблон расширяется, чтобы создать шаблон так же, как в расширении пути. Параметр раскрывается, и самое длинное совпадение шаблона с его значением заменяется строкой. Если Ipattern начинается с /, все совпадения шаблона заменяются на строку. Обычно заменяется только первый матч. Если шаблон начинается с #, он должен совпадать в начале расширенного значения параметра. Если шаблон начинается с%, он должен совпадать в конце расширенного значения параметра. Если строка пуста, совпадения шаблона удаляются, и / следующий шаблон может быть опущен. Если параметр @ или *, операция замещения применяется к каждому позиционному параметру по очереди, и расширение является результирующим списком. Если параметр является переменной массива, подписанной с @ или * операция замещения применяется к каждому члену массива по очереди, а расширение является результирующим списком.

Вы также можете выполнить разбиение поля, установив $ IFS (разделитель входного поля) на желаемый разделитель.

2 голосов
/ 08 ноября 2008

В ответах на " Как избежать дублирования переменной пути в csh " есть пара соответствующих программ. Они больше концентрируются на обеспечении отсутствия повторяющихся элементов, но предоставленный мною сценарий можно использовать как:

export PATH=$(clnpath $head_dirs:$PATH:$tail_dirs $remove_dirs)

Если у вас есть один или несколько каталогов в $ head_dirs и один или несколько каталогов в $ tail_dirs и один или несколько каталогов в $ remove_dirs, то он использует оболочку для объединения головной, текущей и хвостовой частей в огромное значение, затем удаляет каждый из каталогов, перечисленных в $ remove_dirs, из результата (не является ошибкой, если они не существуют), а также удаляет второе и последующие вхождения любого каталога в пути.

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

1 голос
/ 15 мая 2019

В настоящее время я предпочитаю использовать ruby, а не awk / sed / foo, так что вот мой подход к работе с обманщиками,

# add it to the path
PATH=~/bin/:$PATH:~/bin
export PATH=$(ruby -e 'puts ENV["PATH"].split(/:/).uniq.join(":")')

создать функцию для повторного использования,

mungepath() {
   export PATH=$(ruby -e 'puts ENV["PATH"].split(/:/).uniq.join(":")')
}

Хеш, массивы и строки в одном рубиновом лайнере:)

1 голос
/ 13 августа 2012
  • Орден ПУТИ не нарушен
  • Изящно обрабатывает угловые случаи, например, пустой путь, пространство в пути
  • Частичное совпадение dir не дает ложных срабатываний
  • Правильно относится к пути в голове и хвосте ПУТИ. Нет : мусор и прочее.

Скажи, что у тебя есть / Foo: / некоторый / путь: / некоторые / путь / dir1: / некоторые / путь / dir2: / бар и вы хотите заменить / Некоторые / путь Тогда он правильно заменяет "/ some / path", но оставляет "/ some / path / dir1" или "/ some / path / dir2", как и следовало ожидать.

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  
}  

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

1 голос
/ 29 сентября 2011

предположим

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

Если вы хотите удалить /lib/jvm/java-1.6.0/bin/, сделайте так, как показано ниже

export PATH=$(echo $PATH | sed  's/\/lib\/jvm\/java-1.6.0\/bin\/://g')

sed примет ввод от echo $PATH и заменит /lib/jvm/java-1.6.0/bin/: пустым

таким образом вы можете удалить

1 голос
/ 07 декабря 2008

Это легко с помощью awk.

Заменить

{
  for(i=1;i<=NF;i++) 
      if($i == REM) 
          if(REP)
              print REP; 
          else
              continue;
      else 
          print $i; 
}

Запустите его, используя

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

Вставка в заданную позицию. По умолчанию добавляется в конце.

{ 
    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
}

Запустите его, используя

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

Удалить дубликаты

Этот хранит первые вхождения.

{ 
    for(i = 1; i <= NF; i++) {
        if(!used[$i]) {
            print $i
            used[$i] = 1
        }
    }
}

Запустите это так:

echo $PATH | awk -F: -f rem_dup.awk | paste -sd:

Проверка наличия всех элементов

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

echo -n $PATH | xargs -d: stat -c %n

Чтобы просто проверить, являются ли все элементы путями и получить код возврата, вы также можете использовать test:

echo -n $PATH | xargs -d: -n1 test -d
1 голос
/ 08 ноября 2008

ОК, спасибо всем респондентам. Я подготовил инкапсулированную версию ответа Флорина. Первый проход выглядит так:

# 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;
}

По-прежнему требуется перехват ошибок во всех функциях, и мне, вероятно, следует придерживаться решения с повторным путем, пока я на нем.

Вы используете его, выполняя . /include/path/path_tools.bash в рабочем скрипте и вызывая функции replace-path*.


Я все еще открыт для новых и / или лучших ответов.

...