120

Кто-нибудь написал функцию bash для добавления каталога в $ PATH, только если его там еще нет?

Я обычно добавляю в PATH что-то вроде:

export PATH=/usr/local/mysql/bin:$PATH

Если я создаю свой PATH в .bash_profile, он не будет прочитан, если только сеанс, в котором я нахожусь, не является сеансом входа в систему - что не всегда верно. Если я создаю свой PATH в .bashrc, он запускается с каждой подоболочкой. Поэтому, если я запускаю окно терминала, затем запускаю screen и затем запускаю скрипт оболочки, я получаю:

$ echo $PATH
/usr/local/mysql/bin:/usr/local/mysql/bin:/usr/local/mysql/bin:....

Я собираюсь попробовать создать функцию bash с именем add_to_path() которая добавляет каталог, только если его там нет. Но если кто-то уже написал (или нашел) такую вещь, я не буду тратить на это время.

20 ответов20

121

Из моего .bashrc:

pathadd() {
    if [ -d "$1" ] && [[ ":$PATH:" != *":$1:"* ]]; then
        PATH="${PATH:+"$PATH:"}$1"
    fi
}

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

Кроме того, это добавляет новый каталог в конец пути; чтобы поместить в начало, используйте PATH="$1${PATH:+":$PATH"}" вместо указанной выше строки PATH= .

19

Продолжая ответ Гордона Дэвиссона, он поддерживает несколько аргументов

pathappend() {
  for ARG in "$@"
  do
    if [ -d "$ARG" ] && [[ ":$PATH:" != *":$ARG:"* ]]; then
        PATH="${PATH:+"$PATH:"}$ARG"
    fi
  done
}

Таким образом, вы можете сделать pathappend path1 path2 path3 ...

За предлог,

pathprepend() {
  for ((i=$#; i>0; i--)); 
  do
    ARG=${!i}
    if [ -d "$ARG" ] && [[ ":$PATH:" != *":$ARG:"* ]]; then
        PATH="$ARG${PATH:+":$PATH"}"
    fi
  done
}

Подобно pathappend, вы можете сделать

pathprepend path1 path2 path3 ...

12

Вот кое-что из моего ответа на этот вопрос в сочетании со структурой функции Дуга Харриса. Он использует регулярные выражения Bash:

add_to_path ()
{
    if [[ "$PATH" =~ (^|:)"${1}"(:|$) ]]
    then
        return 0
    fi
    export PATH=${1}:$PATH
}
10

Поместите это в комментарии к выбранному ответу, но комментарии, кажется, не поддерживают форматирование PRE, поэтому добавьте ответ здесь:

@ gordon-davisson Я не большой поклонник ненужных цитат и конкатенации. Предполагая, что вы используете версию bash> = 3, вы можете вместо этого использовать встроенные в bash регулярные выражения и сделать:

pathadd() {
    if [ -d "$1" ] && [[ ! $PATH =~ (^|:)$1(:|$) ]]; then
        PATH+=:$1
    fi
}

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

6
idempotent_path_prepend ()
{
    PATH=${PATH//":$1"/} #delete any instances in the middle or at the end
    PATH=${PATH//"$1:"/} #delete any instances at the beginning
    export PATH="$1:$PATH" #prepend to beginning
}

Когда вам нужно, чтобы $ HOME/bin появлялся ровно один раз в начале вашего $ PATH и нигде больше, не принимайте никаких заменителей.

6

Вот альтернативное решение, которое имеет дополнительное преимущество удаления избыточных объектов:

function pathadd {
    PATH=:$PATH
    PATH=$1${PATH//:$1/}
}

Один аргумент этой функции добавляется к PATH, и первый экземпляр той же строки удаляется из существующего пути. Другими словами, если каталог уже существует в пути, он продвигается вперед, а не добавляется как дубликат.

Функция работает, добавляя двоеточие к пути, чтобы гарантировать, что все записи имеют двоеточие спереди, а затем добавляя новую запись к существующему пути, удаляя эту запись. Последняя часть выполняется с использованием нотации ${var//pattern/sub} bash; см. руководство по bash для более подробной информации.

5

Для предваряющих мне нравится решение @ Russell, но есть небольшая ошибка: если вы попытаетесь добавить что-то вроде "/bin" к пути "/sbin:/usr/bin:/var/usr/bin:/usr/local/bin:/usr/sbin "он заменяет"/bin: «3 раза (когда он вообще не совпадал). Объединяя исправление для этого с добавлением решения от @ gordon-davisson, я получаю это:

path_prepend() {
    if [ -d "$1" ]; then
        PATH=${PATH//":$1:"/:} #delete all instances in the middle
        PATH=${PATH/%":$1"/} #delete any instance at the end
        PATH=${PATH/#"$1:"/} #delete any instance at the beginning
        PATH="$1${PATH:+":$PATH"}" #prepend $1 or if $PATH is empty set to $1
    fi
}
5

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

pathmunge () {
        if ! echo $PATH | /bin/egrep -q "(^|:)$1($|:)" ; then
           if [ "$2" = "after" ] ; then
              PATH=$PATH:$1
           else
              PATH=$1:$PATH
           fi
        fi
}

Использование:

$ echo $PATH
/bin/:/usr/local/bin/:/usr/bin
$ pathmunge /bin/
$ echo $PATH
/bin/:/usr/local/bin/:/usr/bin
$ pathmunge /sbin/ after
$ echo $PATH
/bin/:/usr/local/bin/:/usr/bin:/sbin/
4

Простой псевдоним как этот ниже должен сделать трюк:

alias checkInPath="echo $PATH | tr ':' '\n' | grep -x -c "

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

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

$ checkInPath "/usr/local"
1
$ checkInPath "/usr/local/sbin"
1
$ checkInPath "/usr/local/sbin2"
0
$ checkInPath "/usr/local/" > /dev/null && echo "Yes" || echo "No"
No
$ checkInPath "/usr/local/bin" > /dev/null && echo "Yes" || echo "No"
Yes
$ checkInPath "/usr/local/sbin" > /dev/null && echo "Yes" || echo "No"
Yes
$ checkInPath "/usr/local/sbin2" > /dev/null && echo "Yes" || echo "No"
No

Замените команду echo на addToPath или похожий псевдоним / функцию.

2

Вот что я взбил:

add_to_path ()
{
    path_list=`echo $PATH | tr ':' ' '`
    new_dir=$1
    for d in $path_list
    do
        if [ $d == $new_dir ]
        then
            return 0
        fi
    done
    export PATH=$new_dir:$PATH
}

Сейчас в .bashrc у меня есть:

add_to_path /usr/local/mysql/bin

Обновленная версия после комментария о том, что мой оригинал не будет обрабатывать каталоги с пробелами (спасибо за этот вопрос за указание использовать IFS):

add_to_path ()
{
    new_dir=$1
    local IFS=:
    for d in $PATH
    do
        if [[ "$d" == "$new_dir" ]]
        then
            return 0
        fi
    done
    export PATH=$new_dir:$PATH
}
2

См. Как избежать дублирования переменной пути в csh? на StackOverflow за один набор ответов на этот вопрос.

2

Я немного удивлен, что никто еще не упомянул об этом, но вы можете использовать readlink -f чтобы преобразовать относительные пути в абсолютные и добавить их в PATH как таковые.

Например, чтобы улучшить ответ Гийома Перро-Аршамбо,

pathappend() {
  for ARG in "$@"
  do
    if [ -d "$ARG" ] && [[ ":$PATH:" != *":$ARG:"* ]]; then
        PATH="${PATH:+"$PATH:"}$ARG"
    fi
  done
}

становится

pathappend() {
    for ARG in "$@"
    do
        if [ -d "$ARG" ] && [[ ":$PATH:" != *":$ARG:"* ]]
        then
            if ARGA=$(readlink -f "$ARG")               #notice me
            then
                if [ -d "$ARGA" ] && [[ ":$PATH:" != *":$ARGA:"* ]]
                then
                    PATH="${PATH:+"$PATH:"}$ARGA"
                fi
            else
                PATH="${PATH:+"$PATH:"}$ARG"
            fi
        fi
    done
}

1. Основы - что хорошего это делает?

Команда readlink -f (среди прочего) преобразует относительный путь в абсолютный.  Это позволяет вам сделать что-то вроде

$ cd /path/to/my/bin/dir
$ pathappend .
$ echo "$PATH"
<your_old_path>:/path/to/my/bin/dir

2. Почему мы дважды тестируемся на наличие PATH?

Хорошо, рассмотрим приведенный выше пример.  Если пользователь говорит pathappend . из каталога /path/to/my/bin/dir во второй раз будет ARG . ,  Конечно . не будет присутствовать в PATH . Но тогда ARGA будет установлен в /path/to/my/bin/dir (абсолютный эквивалент пути .), Который уже находится в PATH . Поэтому нам нужно избегать добавления /path/to/my/bin/dir к PATH во второй раз.

Возможно, что еще более важно, основная цель readlink состоит в том, чтобы, как следует из названия, посмотреть на символическую ссылку и прочитать путь, который она содержит (т.е. указывает на).  Например:

$ ls -ld /usr/lib/perl/5.14
-rwxrwxrwx  1   root   root    Sep  3  2015 /usr/lib/perl/5.14 -> 5.14.2
$ readlink /usr/lib/perl/5.14
5.14.2
$ readlink -f /usr/lib/perl/5.14
/usr/lib/perl/5.14.2

Теперь, если вы говорите pathappend /usr/lib/perl/5.14 , и у вас уже есть /usr/lib/perl/5.14 в вашей переменной PATH, это нормально; мы можем просто оставить все как есть.  Но если /usr/lib/perl/5.14 еще нет в вашем PATH, мы вызываем readlink и получаем ARGA = /usr/lib/perl/5.14.2 , а затем добавляем это в PATH . Но подождите минуту - если вы уже сказали, что pathappend /usr/lib/perl/5.14 , то у вас уже есть /usr/lib/perl/5.14.2 в вашей PATH, и, опять же, нам нужно проверить это, чтобы избежать добавления это PATH во второй раз.

3. В чем дело, if ARGA=$(readlink -f "$ARG")?

В случае, если неясно, эта строка проверяет успешность readlink ссылки.  Это просто хорошая, защитная практика программирования.  Если мы собираемся использовать выходные данные команды m как часть команды n (где m < n), целесообразно проверить, не завершилась ли команда m, и обработать ее каким-либо образом.  Я не думаю, что вполне вероятно, что readlink потерпит неудачу - но, как обсуждалось в разделе Как извлечь абсолютный путь произвольного файла из OS X и других источников, readlink - это изобретение GNU.  Он не указан в POSIX, поэтому его доступность в Mac OS, Solaris и других не-Linux Unix-системах вызывает сомнения.  Установив систему безопасности, мы делаем наш код несколько более переносимым.

Конечно, если вы работаете в системе, которая не имеет readlink , вам не захочется делать pathappend . ,

Второй -d тест ([ -d "$ARGA" ]) действительно, вероятно, не нужен.  Я не могу представить ни одного сценария, в котором $ARG - это каталог, а readlink успешен, но $ARGA - это не каталог.  Я просто скопировал и вставил первый оператор if чтобы создать третий, и оставил там тест -d из-за лени.

4. Любые другие комментарии?

Да уж.  Как и многие другие ответы здесь, этот тест проверяет, является ли каждый аргумент каталогом, обрабатывает его, если он есть, и игнорирует его, если это не так.  Это может (или не может) быть адекватным, если вы используете pathappend только в « . »(Например, .bash_profile и .bashrc) и другие скрипты.  Но, как показал этот ответ (выше), вполне возможно использовать его в интерактивном режиме.  Вы будете очень озадачены, если вы делаете

$ pathappend /usr/local/nysql/bin
$ mysql
-bash: mysql: command not found

Вы заметили, что я сказал nysql в команде pathappend , а не mysql?  И этот pathappend ничего не сказал; он просто молча проигнорировал неверный аргумент?

Как я уже говорил выше, это хорошая практика для обработки ошибок.  Вот пример:

pathappend() {
    for ARG in "$@"
    do
        if [ -d "$ARG" ]
        then
            if [[ ":$PATH:" != *":$ARG:"* ]]
            then
                if ARGA=$(readlink -f "$ARG")           #notice me
                then
                    if [[ ":$PATH:" != *":$ARGA:"* ]]
                    then
                        PATH="${PATH:+"$PATH:"}$ARGA"
                    fi
                else
                    PATH="${PATH:+"$PATH:"}$ARG"
                fi
            fi
        else
            printf "Error: %s is not a directory.\n" "$ARG" >&2
        fi
    done
}
1

Этот способ отлично работает:

if [[ ":$PATH:" != *":/new-directory:"* ]]; then PATH=${PATH}:/new-directory; fi
1
function __path_add(){  
if [ -d "$1" ] ; then  
    local D=":${PATH}:";   
    [ "${D/:$1:/:}" == "$D" ] && PATH="$PATH:$1";  
    PATH="${PATH/#:/}";  
    export PATH="${PATH/%:/}";  
fi  
}  
0

Мои версии менее осторожны с пустыми путями и настаивают на правильности путей и каталогов, чем некоторые, опубликованные здесь, но я нахожу большую коллекцию prepend/append/clean/unique-ify/etc. Функции оболочки должны быть полезны для манипулирования путями. Все, в их текущем состоянии, здесь: http://pastebin.com/xS9sgQsX (отзывы и улучшения очень приветствуются!)

0

Вот POSIX-совместимый способ.

# USAGE: path_add [include|prepend|append] "dir1" "dir2" ...
#   prepend: add/move to beginning
#   append:  add/move to end
#   include: add to end of PATH if not already included [default]
#          that is, don't change position if already in PATH
# RETURNS:
# prepend:  dir2:dir1:OLD_PATH
# append:   OLD_PATH:dir1:dir2
# If called with no paramters, returns PATH with duplicate directories removed
path_add() {
    # use subshell to create "local" variables
    PATH="$(path_unique)"
    export PATH="$(path_add_do "$@")"
}

path_add_do() {
    case "$1" in
    'include'|'prepend'|'append') action="$1"; shift ;;
    *)                            action='include'   ;;
    esac

    path=":$PATH:" # pad to ensure full path is matched later

    for dir in "$@"; do
        #       [ -d "$dir" ] || continue # skip non-directory params

        left="${path%:$dir:*}" # remove last occurrence to end

        if [ "$path" = "$left" ]; then
            # PATH doesn't contain $dir
            [ "$action" = 'include' ] && action='append'
            right=''
        else
            right=":${path#$left:$dir:}" # remove start to last occurrence
        fi

        # construct path with $dir added
        case "$action" in
            'prepend') path=":$dir$left$right" ;;
            'append')  path="$left$right$dir:" ;;
        esac
    done

    # strip ':' pads
    path="${path#:}"
    path="${path%:}"

    # return
    printf '%s' "$path"
}

# USAGE: path_unique [path]
# path - a colon delimited list. Defaults to $PATH is not specified.
# RETURNS: `path` with duplicated directories removed
path_unique() {
    in_path=${1:-$PATH}
    path=':'

    # Wrap the while loop in '{}' to be able to access the updated `path variable
    # as the `while` loop is run in a subshell due to the piping to it.
    # https://stackoverflow.com/questions/4667509/shell-variables-set-inside-while-loop-not-visible-outside-of-it
    printf '%s\n' "$in_path" \
    | /bin/tr -s ':' '\n'    \
    | {
            while read -r dir; do
                left="${path%:$dir:*}" # remove last occurrence to end
                if [ "$path" = "$left" ]; then
                    # PATH doesn't contain $dir
                    path="$path$dir:"
                fi
            done
            # strip ':' pads
            path="${path#:}"
            path="${path%:}"
            # return
            printf '%s\n' "$path"
        }
}

Он взят из ответа Гийома Перро-Аршамбо на этот вопрос и ответа mike511 здесь.

ОБНОВЛЕНИЕ 2017-11-23: Исправлена ошибка в @Scott

0

Этот скрипт позволяет добавить в конце $PATH:

PATH=path2; add_to_PATH after path1 path2:path3
echo $PATH
path2:path1:path3

Или добавьте в начале $PATH:

PATH=path2; add_to_PATH before path1 path2:path3
echo $PATH
path1:path3:path2

# Add directories to $PATH iff they're not already there
# Append directories to $PATH by default
# Based on https://unix.stackexchange.com/a/4973/143394
# and https://unix.stackexchange.com/a/217629/143394
add_to_PATH () {
  local prepend  # Prepend to path if set
  local prefix   # Temporary prepended path
  local IFS      # Avoid restoring for added laziness

  case $1 in
    after)  shift;; # Default is to append
    before) prepend=true; shift;;
  esac

  for arg; do
    IFS=: # Split argument by path separator
    for dir in $arg; do
      # Canonicalise symbolic links
      dir=$({ cd -- "$dir" && { pwd -P || pwd; } } 2>/dev/null)
      if [ -z "$dir" ]; then continue; fi  # Skip non-existent directory
      case ":$PATH:" in
        *":$dir:"*) :;; # skip - already present
        *) if [ "$prepend" ]; then
           # ${prefix:+$prefix:} will expand to "" if $prefix is empty to avoid
           # starting with a ":".  Expansion is "$prefix:" if non-empty.
            prefix=${prefix+$prefix:}$dir
          else
            PATH=$PATH:$dir  # Append by default
          fi;;
      esac
    done
  done
  [ "$prepend" ] && PATH=$prefix:$PATH
}
0

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

if [ "$MYPATHS" != "true" ]; then
    export MYPATHS="true"
    export PATH="$PATH:$HOME/bin:"

    # java stuff
    export JAVA_HOME="$(/usr/libexec/java_home)"
    export M2_HOME="$HOME/Applications/apache-maven-3.3.9"
    export PATH="$JAVA_HOME/bin:$M2_HOME/bin:$PATH"

    # etc...
fi

Конечно, эти записи могут все еще дублироваться, если они добавлены другим скриптом, таким как /etc/profile .

0

Вы можете использовать perl one liner:

appendPaths() { # append a group of paths together, leaving out redundancies
    # use as: export PATH="$(appendPaths "$PATH" "dir1" "dir2")
    # start at the end:
    #  - join all arguments with :,
    #  - split the result on :,
    #  - pick out non-empty elements which haven't been seen and which are directories,
    #  - join with :,
    #  - print
    perl -le 'print join ":", grep /\w/ && !$seen{$_}++ && -d $_, split ":", join ":", @ARGV;' "$@"
}

Вот это в bash:

addToPath() { 
    # inspired by Gordon Davisson, http://superuser.com/a/39995/208059
    # call as: addToPath dir1 dir2
    while (( "$#" > 0 )); do
    echo "Adding $1 to PATH."
    if [[ ! -d "$1" ]]; then
        echo "$1 is not a directory.";
    elif [[ ":$PATH:" == *":$1:"* ]]; then
        echo "$1 is already in the path."
    else
            export PATH="${PATH:+"$PATH:"}$1" # ${x:-defaultIfEmpty} ${x:+valueIfNotEmpty}
    fi
    shift
    done
}
0

Я немного изменил ответ Гордона Дэвиссона, чтобы использовать текущий каталог, если он не указан. Так что вы можете просто сделать padd из каталога, который хотите добавить в PATH.

padd() {
  current=`pwd`
  p=${1:-$current}
  if [ -d "$p" ] && [[ ":$PATH:" != *":$p:"* ]]; then
      PATH="$p:$PATH"
  fi
}

Всё ещё ищете ответ? Посмотрите другие вопросы с метками .