Как я могу аккуратно добавить в $ 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.