Как я могу аккуратно добавить в $ PATH?


31

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

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

Мне известны вопросы о том, как удалить дубликаты из $ PATH, но я не хочу удалять дубликаты . Я хотел бы, чтобы способ добавить пути, только если они еще не присутствуют.



Голди, я не знаю почему, но я видел твой первый комментарий даже с пустым. Но да, префиксы имен тоже работают, не беспокойтесь! Закрытие другого пути тоже хорошо.
Сиро Сантилли 新疆 改造 中心 法轮功 六四 事件

Хорошо, пока ты получил мое сообщение. Иногда подобное изменение вызывает небольшой хаос, я думаю, мы посмотрим, что произойдет.
Златовласка

Ответы:


35

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

new=/opt/bin

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

case ":${PATH:=$new}:" in
    *:"$new":*)  ;;
    *) PATH="$new:$PATH"  ;;
esac

Обратите внимание на использование двоеточий. Без двоеточий, мы могли бы подумать, что, скажем, new=/binуже был на пути, потому что это образец соответствовал /usr/bin. Хотя PATH обычно имеют много элементов, особые случаи нуля и одного элемента в PATH также обрабатываются. Случай PATH, изначально не имеющий элементов (будучи пустым), обрабатывается с помощью ${PATH:=$new}которого присваивается PATHзначение, $newесли оно пустое. Установка значений по умолчанию для параметров таким способом является свойством всех оболочек POSIX: см. Раздел 2.6.2 документации по POSIX .)

Вызываемая функция

Для удобства приведенный выше код можно поместить в функцию. Эту функцию можно определить в командной строке или, чтобы она была доступна постоянно, добавить в скрипт инициализации вашей оболочки (для пользователей bash это будет так ~/.bashrc):

pupdate() { case ":${PATH:=$1}:" in *:"$1":*) ;; *) PATH="$1:$PATH" ;; esac; }

Чтобы использовать эту функцию обновления пути для добавления каталога в текущий PATH:

pupdate /new/path

1
Вы можете сохранить 2 различия в регистре - ср. unix.stackexchange.com/a/40973/1131 .
maxschlepzig

3
Если PATHпусто, это добавит пустую запись (т.е. текущий каталог) в PATH. Я думаю, что вам нужен еще один случай.
CB Bailey

2
@CharlesBailey Не другой case. Просто делай case "${PATH:=$new}". Смотрите мой собственный ответ для аналогичных откатов.
mikeserv

1
@ mc0e Я добавил пример того, как использовать функцию оболочки, чтобы скрыть «шум строки».
John1024

1
@Doogle: uniq обнаруживает дубликаты, только если они находятся рядом друг с другом, поэтому я не думаю, что это удалит дубликаты, если в начале и конце $ PATH появится путь.
Ральф

9

Создайте файл в /etc/profile.dвызываемой, например, mypath.sh(или как вы хотите). Если вы используете lightdm, убедитесь, что он жизнеспособен или используется, /etc/bashrcили файл получен из него. Добавьте к этому следующие функции:

checkPath () {
        case ":$PATH:" in
                *":$1:"*) return 1
                        ;;
        esac
        return 0;
}

# Prepend to $PATH
prependToPath () {
        for a; do
                checkPath $a
                if [ $? -eq 0 ]; then
                        PATH=$a:$PATH
                fi
        done
        export PATH
}

# Append to $PATH
appendToPath () {
        for a; do
                checkPath $a
                if [ $? -eq 0 ]; then
                        PATH=$PATH:$a
                fi
        done
        export PATH
}

Вещи в начале (с добавлением) $ PATH имеют приоритет перед тем, что следует, и наоборот, вещи в конце (с добавлением) будут заменены тем, что предшествует. Это означает, что если ваш $ PATH есть /usr/local/bin:/usr/binи gotchaв обоих каталогах есть исполняемый файл, /usr/local/binпо умолчанию будет использоваться тот, который в.

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

appendToPath /some/path /another/path
prependToPath /some/path /yet/another/path

Если это в a .bashrc, это предотвратит появление значения более одного раза при запуске новой оболочки. Есть ограничение в том, что если вы хотите добавить что-то, что было добавлено (то есть переместить путь в $ PATH) или наоборот, вам придется сделать это самостоятельно.


расщепление $PATHс IFS=:в конечном итоге является более гибким, чем case.
mikeserv

@mikeserv Без сомнения. Это своего рода использование взлома case, IMO. Я полагаю, awkчто здесь тоже можно найти хорошее применение.
Златовласка

Неплохо подмечено. И, как мне кажется, gawkмог напрямую назначить $PATH.
mikeserv

5

Вы можете сделать это следующим образом:

echo $PATH | grep /my/bin >/dev/null || PATH=$PATH:/my/bin

Примечание: если вы строите PATH из других переменных, убедитесь, что они не пусты, для многих оболочек интерпретируйте «как». ,


+1 Согласно справочной странице -qPOSIX требуется для grep, но я не знаю, означает ли это, что есть еще некоторые (не POSIX) greps, у которых ее нет.
Златовласка

1
обратите внимание, что шаблон grep слишком широк. Попробуйте использовать egrep -q "(^ |:) / my / bin (: | \ $)" вместо grep / my / bin> / dev / null. С этой модификацией ваше решение будет правильным, и я думаю, что это более читаемое решение, чем предпочитаемый в настоящее время ответ от @ john1024. Обратите внимание, что я использовал двойные кавычки, поэтому вы используете подстановку переменных вместо/my/bin
mc0e

5

Важной частью кода является проверка, PATHсодержит ли конкретный путь:

printf '%s' ":${PATH}:" | grep -Fq ":${my_path}:"

То есть, убедитесь, что каждый путь в PATHразделен с обеих сторон PATHразделителем ( :), затем проверьте ( -q), существует ли там литеральная строка ( -F), состоящая из PATHразделителя, вашего пути и другого PATHразделителя. Если это не так, вы можете безопасно добавить путь:

if ! printf '%s' ":${PATH-}:" | grep -Fq ":${my_path-}:"
then
    PATH="${PATH-}:${my_path-}"
fi

Это должно быть POSIX-совместимым и должно работать с любым путем, не содержащим символ новой строки. Это более сложно, если вы хотите, чтобы он работал с путями, содержащими символ новой строки, будучи совместимым с POSIX, но если у вас есть, grepкоторый поддерживает, -zвы можете использовать это.


4

Я нес эту маленькую функцию со мной в различных ~/.profileфайлах в течение многих лет. Я думаю, что это было написано сисадмином в лаборатории, в которой я работал, но я не уверен. В любом случае, это похоже на подход Златовласки, но немного отличается:

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

Итак, чтобы добавить новый каталог в начало PATH:

pathmunge /new/path

и до конца:

pathmunge /new/path after

Это работает для меня! Но я переключился на логику, чтобы поставить его после по умолчанию, и переопределить с помощью «до». :)
Кевин Паули

pathmunge является частью дистрибутива linux centos / etc / profile, имеет параметр до и после. Я не вижу этого в моем последнем Ubuntu 16.
Кемин Чжоу

Кажется, работает нормально на MacOS 10.12 после /bin/grep->grep
Бен Creasy

4

ОБНОВИТЬ:

Я заметил, что у вашего собственного ответа была отдельная функция для добавления или добавления к $PATH. Мне понравилась идея. Поэтому я добавил немного обработки аргументов. Я также правильно _назвал это:

_path_assign() { oFS=$IFS ; IFS=: ; add=$* ; unset P A ; A=
    set -- ${PATH:=$1} ; for p in $add ; do {
        [ -z "${p%-[AP]}" ] && { unset P A
                eval ${p#-}= ; continue ; }
        for d ; do [ -z "${d%"$p"}" ] && break
        done ; } || set -- ${P+$p} $* ${A+$p}
        done ; export PATH="$*" ; IFS=$oFS
}

% PATH=/usr/bin:/usr/yes/bin
% _path_assign \
    /usr/bin \
    /usr/yes/bin \
    /usr/bin/nope \
    -P \
    /usr/nope/bin \
    /usr/bin \
    -A \
    /nope/usr/bin \
    /usr/nope/bin

% echo $PATH

ВЫХОД:

/usr/nope/bin:/usr/bin:/usr/yes/bin:/usr/bin/nope:/nope/usr/bin

По умолчанию это будет -Aзависеть от $PATH, но вы можете изменить это поведение, чтобы -Pповторить, добавив -Pлюбое место в вашем списке аргументов. Вы можете переключить его обратно в -Aрежим ожидания, передав его -Aснова.

БЕЗОПАСНОЕ EVAL

В большинстве случаев я рекомендую людям избегать любого использования eval. Но это, я думаю, выделяется как пример его использования насовсем. В этом случае единственное утверждение, которое eval можно увидеть, это P=или A=. Значения его аргументов строго проверяются до того, как он будет вызван. Это для чего eval .

assign() { oFS=$IFS ; IFS=: ; add=$* 
    set -- ${PATH:=$1} ; for p in $add ; do { 
        for d ; do [ -z "${d%"$p"}" ] && break 
        done ; } || set -- $* $p ; done
    PATH="$*" ; IFS=$oFS
}

Это примет столько аргументов, сколько вы дадите, и добавите каждый из них $PATHтолько один раз и только если его еще нет $PATH. Он использует только полностью переносимый POSIX-скрипт оболочки, опирается только на встроенные функции оболочки и очень быстр.

% PATH=/usr/bin:/usr/yes/bin
% assign \
    /usr/bin \
    /usr/yes/bin \
    /usr/nope/bin \
    /usr/bin \
    /nope/usr/bin \
    /usr/nope/bin

% echo "$PATH"
> /usr/bin:/usr/yes/bin:/usr/nope/bin:/nope/usr/bin

@ TAFKA'goldilocks 'смотрите обновление здесь - вы меня вдохновили.
mikeserv

+1 Из любопытства (может быть, это был бы хороший отдельный вопрос и ответ), откуда взялась идея, что _префиксные функции оболочки делают их «должным образом распределенными по пространству имен»? В других языках обычно указывается внутренняя глобальная функция (то есть та, которая должна быть глобальной, но не предназначена для внешнего использования как часть API). Мои имена, конечно, не _лучший выбор, но мне кажется, что простое использование не решает проблемы коллизий вообще - было бы лучше использовать собственное пространство имен, например. mikeserv_path_assign(),
Златовласка

@ TAFKA'goldilocks '- было бы лучше конкретизировать его, но чем длиннее имя, тем менее удобно его использование. Но если у вас есть соответствующие исполняемые двоичные файлы с префиксом, _вам нужно переключить менеджеры пакетов. В любом случае, это, по сути, просто «глобальная, внутренняя функция» - она ​​глобальна для каждой оболочки, вызванной из оболочки, в которой она объявлена, и это лишь небольшая часть интерпретируемого языкового скрипта, висящая в памяти интерпретатора. , unix.stackexchange.com/questions/120528/…
mikeserv

Вы не можете unset a(или эквивалент) в конце профиля?
sourcejedi

0

Вот! Индустриальная мощная 12-строчная ... техническая bash- и zsh-переносимая функция оболочки, которая преданно любит ваш ~/.bashrcили ~/.zshrcзапускаемый скрипт:

# void +path.append(str dirname, ...)
#
# Append each passed existing directory to the current user's ${PATH} in a
# safe manner silently ignoring:
#
# * Relative directories (i.e., *NOT* prefixed by the directory separator).
# * Duplicate directories (i.e., already listed in the current ${PATH}).
# * Nonextant directories.
+path.append() {
    # For each passed dirname...
    local dirname
    for   dirname; do
        # Strip the trailing directory separator if any from this dirname,
        # reducing this dirname to the canonical form expected by the
        # test for uniqueness performed below.
        dirname="${dirname%/}"

        # If this dirname is either relative, duplicate, or nonextant, then
        # silently ignore this dirname and continue to the next. Note that the
        # extancy test is the least performant test and hence deferred.
        [[ "${dirname:0:1}" == '/' &&
           ":${PATH}:" != *":${dirname}:"* &&
           -d "${dirname}" ]] || continue

        # Else, this is an existing absolute unique dirname. In this case,
        # append this dirname to the current ${PATH}.
        PATH="${PATH}:${dirname}"
    done

    # Strip an erroneously leading delimiter from the current ${PATH} if any,
    # a common edge case when the initial ${PATH} is the empty string.
    PATH="${PATH#:}"

    # Export the current ${PATH} to subprocesses. Although system-wide scripts
    # already export the ${PATH} by default on most systems, "Bother free is
    # the way to be."
    export PATH
}

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

export PATH=$PATH:~/opt/bin:~/the/black/goat/of/the/woods/with/a/thousand/young

Сделайте это вместо этого и будьте уверены, что получите лучшее, действительно ли вы этого хотели или нет:

+path.append ~/opt/bin ~/the/black/goat/of/the/woods/with/a/thousand/young

Отлично, определите «лучший».

Безопасное добавление и добавление к току ${PATH}не является обычным делом. В то время как удобные и кажущиеся разумными, однострочники формы export PATH=$PATH:~/opt/binвызывают дьявольские осложнения с:

  • Случайно относительные dirnames (например, export PATH=$PATH:opt/bin). Хотя в большинстве случаев bashи zshбезмолвно принимают и в основном игнорируют относительные имена dirname , относительные имена dirnames с префиксом либо или (и, возможно, других гнусных персонажей) заставляют обоих позорно изуродовать себя оригинальным шедевром аля Масаки Кобаяши 1962 года Харакири :ht

    # Don't try this at home. You will feel great pain.
    $ PATH='/usr/local/bin:/usr/bin:/bin' && export PATH=$PATH:harakiri && echo $PATH
    /usr/local/bin:/usr/bin:arakiri
    $ PATH='/usr/local/bin:/usr/bin:/bin' && export PATH=$PATH:tanuki/yokai && echo $PATH
    binanuki/yokai   # Congratulations. Your system is now face-up in the gutter.
    
  • Случайно дублируйте имена. Хотя дубликаты ${PATH}каталогов в основном безвредны, они также нежелательны, громоздки, слегка неэффективны, затрудняют отладку и способствуют износу дисков - как этот ответ. Хотя твердотельные накопители в стиле NAND ( конечно ) не подвержены износу при чтении, жесткие диски - нет. Ненужный доступ к файловой системе при каждой попытке команды подразумевает ненужный износ считывающей головки в одном и том же темпе. Дубликаты особенно неуместны, когда вызывают вложенные оболочки в вложенных подпроцессах, и в этот момент кажущиеся безобидными однострочники export PATH=$PATH:~/watбыстро взрываются, как Седьмой круг ${PATH}ада PATH=/usr/local/bin:/usr/bin:/bin:/home/leycec/wat:/home/leycec/wat:/home/leycec/wat:/home/leycec/wat. Только Вельзевул может помочь вам, если вы добавите к этому дополнительные имена. (Не позволяй этому случиться с твоими драгоценными детьми. )

  • Случайно пропущенные имена. Опять же, несмотря на то, что пропущенные ${PATH}имена каталогов в основном безвредны, они также обычно нежелательны, громоздки, слегка неэффективны, препятствуют отладке и способствуют износу дисков.

Ergo, дружественная автоматизация, подобная функции оболочки, определенной выше. Мы должны спасти себя от самих себя.

Но ... Почему "+ path.append ()"? Почему не просто append_path ()?

Для disambiguity (например, с внешними командами в текущей ${PATH}или всей системе функций оболочки , определенных в другом месте), определяемого пользователь функция оболочки идеально приставка или суффикс с уникальными подстроками , поддерживаемых bashи , zshно иным образом запрещена для стандартных команд basenames - как, скажем, +.

Привет. Оно работает. Не суди меня.

Но ... Почему "+ path.append ()"? Почему бы не "+ path.prepend ()"?

Поскольку присоединение к току ${PATH}безопаснее, чем присоединение к току ${PATH}, все вещи равны, а они никогда не равны. Переопределение общесистемных команд пользовательскими командами в лучшем случае может быть антисанитарным, а в худшем - безумным. Например, в Linux нижестоящие приложения обычно ожидают варианты команд GNU coreutils, а не нестандартные нестандартные производные или альтернативы.

Тем не менее, есть абсолютно допустимые варианты использования для этого. Определение эквивалентной +path.prepend()функции тривиально. Sans prolix nebulosity, для его и ее общего здравомыслия:

+path.prepend() {
    local dirname
    for dirname in "${@}"; do
        dirname="${dirname%/}"
        [[ "${dirname:0:1}" == '/' &&
           ":${PATH}:" != *":${dirname}:"* &&
           -d "${dirname}" ]] || continue
        PATH="${dirname}:${PATH}"
    done
    PATH="${PATH%:}"
    export PATH
}

Но ... Почему не Жиль?

Жиля ' приемлемого ответа в другом месте выразительно оптимальный в общем случае в виде „оболочка агностического идемпотентного Append“ . Однако в обычном случае bashи zshпри отсутствии нежелательных символических ссылок потеря производительности, требуемая для этого, огорчает меня в Gentoo . Даже при наличии нежелательных симлинок, это спорно ли разветвление один подоболочки в add_to_PATH()аргумент стоит потенциала вставки символических ссылок дублей.

Для строгих случаев использования, требующих устранения даже дубликатов символических ссылок, этот zsh-специфичный вариант делает это с помощью эффективных встроенных функций, а не неэффективных вилок:

+path.append() {
    local dirname
    for   dirname in "${@}"; do
        dirname="${dirname%/}"
        [[ "${dirname:0:1}" == '/' &&
           ":${PATH}:" != *":${dirname:A}:"* &&
           -d "${dirname}" ]] || continue
        PATH="${PATH}:${dirname}"
    done
    PATH="${PATH#:}"
    export PATH
}

Обратите внимание, *":${dirname:A}:"*а не *":${dirname}:"*оригинал. :AУдивительный zsh-изм, к сожалению, отсутствует в большинстве других оболочек - в том числе bash. Цитировать man zshexpn:

A : Превратите имя файла в абсолютный путь, как это aделает модификатор, а затем передайте результат через realpath(3)библиотечную функцию для разрешения символических ссылок. Примечание: на системах , которые не имеют realpath(3)функции библиотеки, символические ссылки не будут решены, так что в этих системах aи Aэквивалентны.

Больше нет вопросов.

Пожалуйста. Наслаждайтесь безопасным обстрелом. Теперь вы это заслужили.


0

Вот моя версия в стиле функционального программирования.

  • Работает для любой *PATHпеременной, разделенной двоеточиями , не только PATH.
  • Не имеет доступа к глобальному состоянию
  • Работает только с / на заданных неизменяемых входах
  • Производит один выход
  • Нет побочных эффектов
  • Запоминающийся (в принципе)

Также стоит отметить:

  • Агностика относительно exportИГ; это остается для звонящего (см. примеры)
  • Чистый bash; нет разветвления
path_add () {
  # $ 1: Элемент, который должен быть указан в заданной строке пути ровно один раз
  # $ 2: Значение строки существующего пути («$ PATH», а не «PATH»)
  # $ 3 (необязательно, все что угодно): если дано, добавить $ 1; в противном случае
  #
  # Примеры:
  # $ export PATH = $ (path_add '/ opt / bin' "$ PATH")
  # $ CDPATH = $ (path_add '/ Music' "$ CDPATH" at_end)

  local -rready_present = "(^ |:) $ {1} ($ | :)"
  if [["$ 2" = ~ $ready_present]]; тогда
    эхо "$ 2"
  элиф [[$ # == 3]]; тогда
    echo "$ {2}: $ {1}"
  еще
    echo "$ {1}: $ {2}"
  фи
}

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 /unix//a/4973/143394
# and /unix//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" ] && [ "$prefix" != "" ] && PATH=$prefix:$PATH
}
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.