Додайте каталог до $ PATH, якщо його ще немає


126

Хтось написав функцію bash, щоб додати каталог до $ PATH, лише якщо його ще немає?

Я зазвичай додаю PATH, використовуючи щось на кшталт:

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

Якщо я будую свій PATH в .bash_profile, він не читається, якщо сесія, в якій я перебуваю, не є сеансом входу - що не завжди відповідає дійсності. Якщо я будую свій PATH в .bashrc, то він працює з кожною підзаголовком. Отже, якщо я запускаю вікно терміналу, а потім запускаю екран, а потім запускаю скрипт оболонки, я отримую:

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

Я спробую створити функцію bash під назвою, add_to_path()яка додасть лише каталог, якщо його немає. Але якщо хтось уже написав (або знайшов) таку річ, я не витрачу на це часу.


Дивіться stackoverflow.com/questions/273909/… про певну інфраструктуру, яка може допомогти.
dmckee


Якщо ви поставите проблему як "лише додавання, якщо її вже немає", ви будете грубо здивовані, коли настає день, коли важливо, щоб вставлений елемент був на початку, але він не закінчується. Кращим підходом було б вставити елемент, а потім видалити дублікати, тож якщо новий запис вже був там, він буде ефективно переміщений на початок.
Дон Хетч

Відповіді:


125

З мого .bashrc:

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

Зауважте, що PATH вже слід позначати як експортований, тому реекспорт не потрібен. Це перевіряє, чи існує каталог & є каталог перед його додаванням, що може вас не хвилювати.

Також це додає новий каталог до кінця шляху; поставити на початку, використовуйте PATH="$1${PATH:+":$PATH"}"замість вищевказаного PATH=рядка.


26
Мені не байдуже.
Денніс Вільямсон,

4
@Neil: Це спрацює, тому що порівнюється з ":$PATH:"замість просто"$PATH"
Гордон Девіссон

3
@GordonDavisson: Прошу вибачення, мій тест був неправильним, і ти прав.
Ніл

2
@GordonDavisson Який сенс матеріалу у фігурних дужках. Я, здається, не можу це ${PATH:+"$PATH:"}
загадувати

5
@ Mark0978: Саме цим я і вирішив проблему, яку вказав bukzor. ${variable:+value}означає перевірити, чи variableвизначено воно і чи не має порожнього значення, і чи воно дає результат оцінки value. В основному, якщо PATH не пустий для початку, він встановлює його "$PATH:$1"; якщо він порожній, він встановлює його просто "$1"(відзначте відсутність двокрапки).
Гордон Девіссон

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


3
Це чудово! Я зробив одну невелику зміну. Для функції 'pathprepend' зручно читати аргументи в зворотному порядку, так що ви можете сказати, наприклад, pathprepend P1 P2 P3і закінчити PATH=P1:P2:P3. Щоб отримати таку поведінку, перейдіть for ARG in "$@" doнаfor ((i=$#; i>0; i--)); do ARG=${!i}
ishmael

Дякую @ishmael, хороша пропозиція, я відредагував відповідь. Я розумію, що вашому коментарю більше двох років, але я з тих пір не повертався. Я мушу розібратися, як змусити електронну пошту для обміну стеками висаджуватись у свою скриньку!
Гійом Перро-Архембо

14

Ось щось із моєї відповіді на це питання в поєднанні зі структурою функції Дуга Гарріса. Він використовує регулярні вирази Bash:

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

Це працювало для мене лише з використанням $1замість${1}
Андрій

@Andrei: Так, дужки в цьому випадку непотрібні. Я не впевнений, чому я їх включив.
Денніс Вільямсон

10

Помістіть це в коментарях до вибраної відповіді, але, здається, коментарі не підтримують формат PRE, тому додайте відповідь тут:

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

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

Це правильно обробляє випадки, коли в каталозі або PATH є пробіли. Виникає запитання щодо того, чи вбудований у регекс двигун bash досить повільний, щоб ця мережа була менш ефективною, ніж конкатенація рядків та інтерполяція, що це робить ваша версія, але якимось чином мені це здається більш естетично чистим.


1
Коментарі підтримуються formatting using the backtickлише, але ви не отримаєте пристойного контролю абзацу.
boatcoder

Це ставить додаток у кінці. Часто бажано додати до початку, щоб змінити існуючі місця.
Денніс Вільямсон

@DennisWilliamson Це справедливий момент, хоча я б не рекомендував це як поведінку за замовчуванням. Не важко розібратися, як змінити препенду.
Крістофер Сміт

@ChristopherSmith - re: unnecessary quotingозначає, що ви знаєте достроково, що $PATHне є нульовим. "$PATH"робить це нормально, чи PATH недійсний чи ні. Аналогічно, якщо $1містяться символи, які можуть заплутати аналізатор команд. Якщо ввести латекс у лапки, це "(^|:)$1(:|$)"заважає.
Джессі Чизгольм

@JesseChisholm: Насправді я вважаю, що справа Крістофера полягає в тому, що правила відрізняються між [[та ]]. Я вважаю за краще процитувати все, що можливо може бути необхідним для цитування, якщо тільки це не спричинить його невдачу, але я вважаю, що він має рацію, і цитати дійсно не потрібні навколо $PATH. З іншого боку, мені здається, ти маєш рацію $1.
Скотт

7
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 / бін, щоб з’явитися рівно один раз на початку $ PATH і ніде більше, не приймайте жодних замінників.


Дякую, це приємне елегантне рішення, але я виявив, що мені довелося зробити PATH=${PATH/"... а не PATH=${PATH//"... аби змусити його працювати.
Марк Бут

Форма подвійного косого кута повинна відповідати будь-якій кількості збігів; одна косою рисою відповідає лише першій (пошук "Заміна шаблону" на сторінці bash man). Не впевнений, чому це не вийшло ...
andybuckley

Це не вдається в незвичному випадку, який $1є єдиним записом (без колонок). Запис стає подвоєним.
Денніс Вільямсон

Він також видаляє занадто агресивно, як вказував PeterS6g .
Денніс Вільямсон

6

Ось альтернативне рішення, яке має додаткову перевагу видалення зайвих сукупностей:

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

Один аргумент цієї функції є попереднім для PATH, і перший екземпляр тієї ж строки видаляється з існуючого шляху. Іншими словами, якщо каталог вже існує на шляху, він просувається на фронт, а не додається як дублікат.

Функція працює, попередньо додавши двокрапку до шляху, щоб забезпечити, що всі записи мають двокрапку на передній панелі, а потім попередньо створивши новий запис до існуючого шляху, коли цей запис буде видалено. Остання частина виконується з використанням ${var//pattern/sub}позначень Баша; див . посібник з bash для отримання більш детальної інформації.


Гарна думка; хибна реалізація. Поміркуйте, що станеться, якщо ви вже є /home/robertв своєму PATHі вас pathadd /home/rob.
Скотт

5

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

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/

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
}

4

Простий псевдонім, як цей нижче, повинен зробити трюк:

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

Все, що він робить, це розділити шлях на:: персонаж і порівняти кожен компонент з аргументом, який ви передаєте.

Використання зразка:

$ 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 або якийсь подібний псевдонім / функцію.


Використання "grep -x", ймовірно, швидше, ніж цикл, який я помістив у свою функцію bash.
Дуг Харріс


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
}

1
Це може відбутися збій , якщо будь-яке ім'я каталогу вже PATHмістить прогалини, *, ?або [... ].
Скотт

Хороший момент ... але я хлопець з лінукса в старій школі і ніколи не використовував би шлях із пробілом у ньому :-)
Дуг Харріс

Добре для вас, що не створюєте речі з пробілами у своїх іменах. Соромно за те, що ти пишеш код, який не може впоратися з ними, коли вони існують. І що стосується того, щоб бути "старовинним хлопцем з Linux"? Віндоз, можливо, популяризував ідею (спасибі Documents and Settingsі Program Files), але Unix підтримував імена шляхів, що містять пробіли, ще до існування Windoze.
Скотт

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 / шлях / до / мій / бін / реж
$ pathappend .
$ echo "$ PATH"
<your_old_path> : / шлях / до / мій / бін / реж

2. Чому ми тестуємось на те, щоб бути в PATH двічі?

Ну, розглянемо вищенаведений приклад. Якщо користувач скаже з каталогу вдруге, буде . Звичайно, в Росії не буде присутній . Але тоді буде встановлено значення (абсолютний еквівалент шляху ), який вже є . Таким чином , ми повинні уникати додавань до другого разу.pathappend ./path/to/my/bin/dirARG..PATHARGA/path/to/my/bin/dir.PATH/path/to/my/bin/dirPATH

Можливо, ще важливіше, що головна мета 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ПАТ, то це добре; ми можемо просто залишити його таким, яким він є. Але, якщо цього /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вдруге.

3. З чим угоди if ARGA=$(readlink -f "$ARG")?

Якщо це незрозуміло, цей рядок перевіряє, чи readlinkвдалося це зробити. Це просто хороша практика оборонного програмування. Якщо ми збираємось використовувати вихід з команди  m як частину команди  n (де m  <  n ), доцільно перевірити, чи не вдалося команду  m та якось обробити це. Я не думаю, що це, ймовірно, readlinkне вдасться, але, як обговорювалось у розділі Як отримати абсолютний шлях довільного файлу з ОС X та інших країн, readlinkце винахід GNU. Це не визначено в POSIX, тому його доступність у Mac OS, Solaris та інших нелінуксних Unix викликає сумніви. (Насправді я просто прочитав коментар, в якому сказано:readlink -fсхоже, він не працює на Mac OS X 10.11.6, але realpathпрацює з вікна. "Отже, якщо ви працюєте в системі, яка не має readlink, або де readlink -fвона не працює, ви можете змінити це сценарій для використання realpath.) Встановлюючи мережу безпеки, ми робимо наш код дещо більш портативним.

Звичайно, якщо ви працюєте в системі, яка не має readlink(або  realpath), ви не збираєтесь цього робити .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) Ви повинні додати лапки: readlink -f "$ARG". (2) Я не знаю, чому це сталося (особливо після -d "$ARG"успіху тесту), але ви, можливо, захочете перевірити, чи readlinkне вдалося. (3) Ви, здається, не помічаєте readlinkголовну функцію - зіставити символьне ім'я посилання на "справжнє ім'я файлу". Якщо (наприклад) /binє символічним посиланням на /bin64, то повторні дзвінки до pathappend /binможуть призвести до висловлювання PATH …:/bin64:/bin64:/bin64:/bin64:…. Ви , ймовірно , слід (і) перевірити , є чи нове значення $ARGвже PATH.
Скотт

(1) Добре спостереження, я це зафіксував. (2) у якому випадку посилання зчитування не вдасться? Якщо припустити, що шлях правильний і він посилається на дійсне місце. (3) Я не впевнений, що диктує основну функцію readlink, я вважаю, що більшість (якщо не всі?) Шляхи в файловій системі Unix можна розділити на 2 типи посилань, символічні та жорсткі посилання. Що стосується введення дубліката шляху, ви маєте рацію, але метою моєї відповіді було не додати цього (оскільки я помітив, що інші відповіді вже згадували про це). Єдина причина, чому я додаю ще одну відповідь - це внести щось, про що я думав, що вже не сприяло
qwertyzw

(2) Я маю на увазі, якщо хоча б ім'я команди, що мається на увазі / натякає на її мету, то 'link' у readlink може посилатися на жорсткі або м'які посилання. Ти прав, але ти прав: man readlink каже: "readlink - друкувати вирішені символьні посилання або канонічні назви файлів", .в моєму прикладі я вважаю, що це може розглядатися як канонічне ім'я файлу. Правильно?
qwertyzw

(1) Для людей, які розуміють символічні посилання, readlinkім'я чітко означає його мету - воно переглядає символічне посилання та читає ім'я шляху, яке воно містить (тобто вказує на). (2) Ну, я сказав, що не знаю, чому readlinkне вдасться. Я зазначав загальну думку про те, що якщо скрипт або функція містить декілька команд, а команда  n гарантовано вийде з ладу катастрофічно (або взагалі не має сенсу), якщо команда  m не вдалася (де m  <  n ), це доцільно перевірити, чи не вдалося команду m, і чи попрацювати з цим якимось чином - принаймні,… (Cont'd)
Скотт

(Продовження)… скасувати сценарій / функцію діагностично. Гіпотетично це readlinkможе бути невдалим, якщо (a) каталог було перейменовано або видалено (іншим процесом) між вашими викликами до testта readlink, або (b), якщо /usr/bin/readlinkбуло видалено (або пошкоджено). (3) Ви, здається, не вистачаєте моєї точки зору. Я не закликаю дублювати інші відповіді; Я кажу, що, перевіряючи, чи вже є оригінал ARG(з командного рядка) PATH, але не повторюючи перевірку на нове ARG(вихід з readlink), ваша відповідь неповна і тому неправильна. … (Продовження)
Скотт

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

1

Цей спосіб працює добре:

if [[ ":$PATH:" != *":/new-directory:"* ]]; then PATH=${PATH}:/new-directory; fi

0

Мої версії менш обережні щодо порожніх шляхів і наполягають на тому, що шляхи є дійсними та каталогіми, ніж деякі розміщені тут, але я знаходжу велику колекцію препенда / додавання / чистого / унікального-ify / тощо. функції оболонки, щоб бути корисними для маніпулювання контуром. Весь лот, в їх нинішньому стані, тут: http://pastebin.com/xS9sgQsX (відгуки та покращення дуже вітаються!)


0

Можна використовувати один вкладиш perl:

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;' "$@"
}

Ось він у басі:

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

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

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

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

Цей сценарій дозволяє додавати в кінці $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

Ось спосіб, сумісний з 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"
        }
}

Він списаний з Гійого Перро-Archambault «s відповіді на це питання і mike511 » s відповіді тут .

ОНОВЛЕННЯ 2017-11-23: Виправлена ​​помилка на @Scott


Я збирався схвалити це для надання опції командного рядка для вибору між попередньою та відкладеною (додаючою), за замовчуванням. Але тоді я подумав: це жахливо багато дещо криптовалютного коду без пояснень. (А той факт, що у вас є дві функції, коли одна змінює PATH і повторює нове значення, а інша фіксує, що виводить і призначає його знову PATH , - лише зайва складність.)… (Продовжував)
Скотт

(Продовжував)… І тоді я помітив, що посилання були невірними. (І я не впевнений, чому ви звинувачуєте цих хлопців; ви, здається, не дуже скопіювали їх відповіді.) І тоді я помітив, що код був невірним. Я думаю, це добре працює з підтриманням добре сформованої PATH, але немає гарантії, що вона вже добре сформована, особливо якщо у вас є непросвічений /etc/profile. Каталог, який ви намагаєтеся додати до PATH, може бути вже не один раз , і ваш код замовчується на цьому. … (Продовжив)
Скотт

(Продовження) ... Наприклад, якщо ви намагаєтеся випереджати /a/ckдо /b:/a/ck:/tr:/a/ck, ви отримаєте /a/ck:/b:/a/ck:/tr:/tr:/a/ck.
Скотт
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.