Adicione o diretório a $ PATH se ele ainda não estiver lá

126

Alguém escreveu uma função bash para adicionar um diretório ao $ PATH apenas se ele ainda não estiver lá?

Eu normalmente adiciono ao PATH usando algo como:

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

Se eu construir meu PATH em .bash_profile, ele não será lido, a menos que a sessão em que eu participe seja uma sessão de logon - o que nem sempre é verdade. Se eu construir meu PATH no .bashrc, ele será executado com cada subshell. Portanto, se eu iniciar uma janela do Terminal e, em seguida, executar a tela e, em seguida, executar um script de shell, obtenho:

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

Vou tentar criar uma função bash chamada add_to_path()que apenas adiciona o diretório se ele não estiver lá. Mas, se alguém já escreveu (ou encontrou) uma coisa dessas, não vou dedicar tempo a isso.

Doug Harris
fonte
Consulte stackoverflow.com/questions/273909/… para obter alguma infraestrutura que possa ajudar.
dmckee
Se você enquadrar o problema como "apenas adicionando se ainda não estiver lá", ficará surpreso quando chegar o dia em que for importante que o item inserido esteja no início, mas ele não acabará lá. Uma abordagem melhor seria inserir o elemento e remover duplicatas; portanto, se a nova entrada já estava lá, ela será efetivamente movida para o início.
Don escotilha

Respostas:

125

Do meu .bashrc:

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

Observe que PATH já deve estar marcado como exportado, portanto, a reexportação não é necessária. Isso verifica se o diretório existe e é um diretório antes de adicioná-lo, com o qual você pode não se importar.

Além disso, isso adiciona o novo diretório ao final do caminho; para colocar no início, use em PATH="$1${PATH:+":$PATH"}"vez da PATH=linha acima .

Gordon Davisson
fonte
26
Eu me importo.
Dennis Williamson
4
@Neil: Ele faz o trabalho, porque compara com ":$PATH:"em vez de apenas"$PATH"
Gordon Davisson
3
@GordonDavisson: Peço desculpas, meu teste estava errado e você está correto.
5113 Neil
2
@GordonDavisson Qual é o objetivo das coisas entre colchetes? Eu não consigo entender " ${PATH:+"$PATH:"}$ 1"
boatcoder 21/12/13
5
@ Mark0978: Foi o que fiz para corrigir o problema que bukzor apontou. ${variable:+value}significa verificar se variableestá definido e tem um valor não vazio e se fornece o resultado da avaliação value. Basicamente, se PATH não for em branco, ele será definido como "$PATH:$1"; se estiver em branco, será definido apenas "$1"(observe a falta de dois pontos).
Gordon Davisson
19

Expandindo a resposta de Gordon Davisson, isso suporta vários argumentos

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

Assim, você pode fazer o caminho: path1 path2 path3 ...

Para anexar,

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

Semelhante ao pathappend, você pode fazer

pathprepend path1 path2 path3 ...

Guillaume Perrault-Archambault
fonte
3
Isso é ótimo! Fiz uma pequena alteração. Para a função 'pathprepend', é conveniente ler os argumentos ao contrário, para que você possa dizer, por exemplo, pathprepend P1 P2 P3e terminar com PATH=P1:P2:P3. Para obter esse comportamento, altere for ARG in "$@" doparafor ((i=$#; i>0; i--)); do ARG=${!i}
ishmael
Obrigado @ishmael, boa sugestão, editei a resposta. Sei que seu comentário tem mais de dois anos, mas não voltei desde então. Eu tenho que descobrir como fazer com que os e-mails de troca de pilhas cheguem à minha caixa de entrada!
Guillaume Perrault-Archambault
14

Aqui está algo da minha resposta a esta pergunta combinada com a estrutura da função de Doug Harris. Ele usa expressões regulares do Bash:

add_to_path ()
{
    if [[ "$PATH" =~ (^|:)"${1}"(:|$) ]]
    then
        return 0
    fi
    export PATH=${1}:$PATH
}
Dennis Williamson
fonte
Isso funcionou para mim usando apenas em $1vez de #${1}
Andrei
@ Andrei: Sim, os aparelhos são desnecessários neste caso. Não sei por que os incluí.
Dennis Williamson
10

Coloque isso nos comentários da resposta selecionada, mas os comentários parecem não suportar a formatação PRE, portanto adicione a resposta aqui:

@ gordon-davisson Eu não sou um grande fã de citações e concatenações desnecessárias. Supondo que você esteja usando uma versão do bash> = 3, você pode usar regexs integrados do bash e fazer:

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

Isso manipula corretamente os casos em que há espaços no diretório ou no PATH. Há alguma dúvida sobre se o mecanismo de regex do bash é lento o suficiente para que isso possa ser menos eficiente do que a concatenação e interpolação de strings que sua versão faz, mas de alguma forma parece mais esteticamente limpo para mim.

Christopher Smith
fonte
11
Os comentários são compatíveis formatting using the backtickapenas, mas você não obtém nenhum controle de parágrafo decente.
boatcoder
Isso coloca a adição no final. Muitas vezes, é desejável adicionar ao início para substituir os locais existentes.
Dennis Williamson
@DennisWilliamson Esse é um argumento justo, embora eu não recomende isso como comportamento padrão. Não é difícil descobrir como mudar para um prefpend.
Christopher Smith
@ChristopherSmith - re: unnecessary quotingimplica que você sabe com antecedência que $PATHnão é nulo. "$PATH"torna OK se PATH é nulo ou não. Da mesma forma, se $1contém caracteres que podem confundir o analisador de comandos. Colocar o regex entre aspas "(^|:)$1(:|$)"impede isso.
Jesse Chisholm
@JesseChisholm: Na verdade, acredito que o argumento de Christopher é que as regras são diferentes entre [[e ]]. Eu prefiro citar tudo o que pode possivelmente precisam ser citado, a menos que faz com que ele falhar, mas eu acredito que ele está certo, e que cita realmente não são necessários ao redor $PATH. Por outro lado, parece-me que você está certo $1.
Scott
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
}

Quando você precisar que $ HOME / bin apareça exatamente uma vez no início do seu $ PATH e em nenhum outro lugar, não aceite substitutos.

Russell
fonte
Obrigado, essa é uma solução elegante e agradável, mas achei que tinha que fazer PATH=${PATH/"... em vez de PATH=${PATH//"... para fazê-la funcionar.
Mark Booth
O formulário com barra dupla deve corresponder a qualquer número de correspondências; a barra única corresponde apenas à primeira (pesquise "Substituição de padrão" na página do manual do bash). Não sei por que não funcionou ...
andybuckley
Isso falha no caso incomum que $1é a única entrada (sem dois pontos). A entrada fica dobrada.
Dennis Williamson
Ele também exclui muito agressivamente, como apontado por PeterS6g .
Dennis Williamson
6

Aqui está uma solução alternativa que tem a vantagem adicional de remover entradas redundantes:

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

O argumento único para essa função é anexado ao PATH e a primeira instância da mesma sequência é removida do caminho existente. Em outras palavras, se o diretório já existir no caminho, ele será promovido para a frente em vez de adicionado como duplicado.

A função funciona adicionando dois pontos ao caminho para garantir que todas as entradas tenham dois pontos à frente e, em seguida, acrescentando a nova entrada ao caminho existente com a entrada removida. A última parte é executada usando a ${var//pattern/sub}notação do bash ; veja o manual do bash para mais detalhes.

Rob Hague
fonte
Bom pensamento; implementação falha. Considere o que acontece se você já possui o /home/robertseu PATHe você pathadd /home/rob.
Scott
5

Aqui está o meu (acredito que foi escrito anos atrás por Oscar, o administrador de sistemas do meu antigo laboratório, tudo creditado a ele), está no meu bashrc há séculos. Ele tem o benefício adicional de permitir que você acrescente ou anexe o novo diretório conforme desejado:

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

Uso:

$ 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/
Terdon
fonte
5

Para anexar, eu gosto da solução de Russell, mas há um pequeno erro: se você tentar anexar algo como "/ bin" a um caminho de "/ sbin: / usr / bin: / var / usr / bin: / usr / local / bin: / usr / sbin "substitui" / bin: "3 vezes (quando realmente não correspondia). Combinando uma correção para isso com a solução anexa de @ gordon-davisson, recebo o seguinte:

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
}
PeterS6g
fonte
4

Um alias simples como este abaixo deve fazer o truque:

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

Tudo o que faz é dividir o caminho no caractere: e comparar cada componente com o argumento que você passa. O grep verifica se há uma correspondência completa de linhas e imprime a contagem.

Uso da amostra:

$ 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

Substitua o comando echo por addToPath ou algum apelido / função semelhante.

Nagul
fonte
Usar "grep -x" é provavelmente mais rápido que o loop que coloquei na minha função bash.
Doug Harris
2

Aqui está o que eu criei:

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
}

Agora em .bashrc eu tenho:

add_to_path /usr/local/mysql/bin

Versão atualizada após o comentário sobre como meu original não manipula diretórios com espaços (obrigado a esta pergunta por me indicar o uso 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
}
Doug Harris
fonte
11
Isso pode falhar se qualquer nome de diretório já PATHcontém espaços em branco, *, ?, ou [... ].
Scott
Bom ponto ... mas eu sou um linux cara old-school e não iria nunca utilizar um caminho com espaços em branco nele :-)
Doug Harris
Bom para você, por não criar coisas com espaços em branco em seus nomes. Que vergonha por escrever código que não pode lidar com eles quando eles existem. E o que ser "um cara Linux da velha escola" tem a ver com isso? O Windoze pode ter popularizado a idéia (obrigado Documents and Settingse Program Files), mas o Unix suporta nomes de caminho que contêm espaços em branco desde antes da existência do Windoze.
Scott
2

Estou um pouco surpreso que ninguém tenha mencionado isso ainda, mas você pode usar readlink -fpara converter caminhos relativos em caminhos absolutos e adicioná-los ao PATH como tal.

Por exemplo, para melhorar a resposta de Guillaume Perrault-Archambault,

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

torna-se

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. O básico - que bem isso faz?

O readlink -fcomando (entre outras coisas) converterá um caminho relativo em um caminho absoluto. Isso permite que você faça algo como

$ cd / caminho / para / meu / bin / dir
$ pathappend .
$ echo "$ PATH"
<your_old_path> : / caminho / para / meu / bin / dir

2. Por que testamos duas vezes o CAMINHO?

Bem, considere o exemplo acima. Se o usuário disser a partir do diretório uma segunda vez, será . Claro, não estará presente em . Mas, em seguida, será definido como (o equivalente caminho absoluto ), que é já . Então, precisamos evitar a adição de uma segunda vez.pathappend ./path/to/my/bin/dirARG..PATHARGA/path/to/my/bin/dir.PATH/path/to/my/bin/dirPATH

Talvez o mais importante readlinkseja que, como o nome indica , o objetivo principal é olhar para um link simbólico e ler o nome do caminho que ele contém (ou seja, aponta para). Por exemplo:

$ 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

Agora, se você diz pathappend /usr/lib/perl/5.14, e você já tem /usr/lib/perl/5.14no seu CAMINHO, tudo bem; podemos simplesmente deixar como está. Mas, se /usr/lib/perl/5.14já não estiver no seu PATH, chamamos readlinke obtemos ARGA= /usr/lib/perl/5.14.2e, em seguida, adicionamos isso a PATH. Mas espere um minuto - se você disse pathappend /usr/lib/perl/5.14, então você já tem /usr/lib/perl/5.14.2seu PATH e, novamente, precisamos verificar isso para evitar adicioná-lo a PATHuma segunda vez.

3. Qual é o problema if ARGA=$(readlink -f "$ARG")?

Caso não esteja claro, essa linha testa se o resultado readlinké bem - sucedido. Isso é apenas uma boa prática de programação defensiva. Se vamos usar a saída do comando  m como parte do comando  n (onde m  <  n ), é prudente verificar se o comando  m falhou e lidar com isso de alguma forma. Eu não acho que isso readlinkirá falhar - mas, como discutido em Como recuperar o caminho absoluto de um arquivo arbitrário do OS X e de outros lugares, readlinké uma invenção do GNU. Ele não está especificado no POSIX, portanto, sua disponibilidade no Mac OS, Solaris e outros Unixes não Linux é questionável. Na verdade, acabei de ler um comentário que diz "readlink -fparece não funcionar no Mac OS X 10.11.6, mas realpathfunciona imediatamente . ”Portanto, se você estiver em um sistema que não possui readlinkou onde readlink -fnão funciona, poderá modificá-lo script a ser usado realpath.) Ao instalar uma rede de segurança, tornamos nosso código um pouco mais portátil.

Claro, se você estiver em um sistema que não possui readlink(ou  realpath), não vai querer fazer isso .pathappend .

O segundo -dteste ( [ -d "$ARGA" ]) é realmente provavelmente desnecessário. Não consigo pensar em nenhum cenário em que $ARGum diretório seja readlinkbem - sucedido, mas  $ARGAnão seja um diretório. Apenas copiei e colei a primeira ifdeclaração para criar a terceira e deixei o  -dteste por preguiça.

4. Outros comentários?

Sim. Como muitas das outras respostas aqui, esta testa se cada argumento é um diretório, o processa se for e o ignora se não for. Isso pode (ou não) ser adequado se você estiver usando pathappend apenas .arquivos " " (como .bash_profilee .bashrc) e outros scripts. Mas, como essa resposta mostrou (acima), é perfeitamente viável usá-la interativamente. Você ficará muito confuso se fizer isso

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

Você notou que eu disse nysql no pathappendcomando, em vez de mysql? E isso pathappendnão disse nada; apenas ignorou silenciosamente o argumento incorreto?

Como eu disse acima, é uma boa prática lidar com erros. Aqui está um exemplo:

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
}
qwertyzw
fonte
(1) Você deve adicionar aspas: readlink -f "$ARG". (2) Não sei por que isso aconteceria (especialmente depois que o -d "$ARG"teste foi bem-sucedido), mas você pode querer testar se readlinkfalhou. (3) Você parece ignorar readlinka função principal - mapear um nome de link simbólico para um "nome real do arquivo". Se (por exemplo) /binfor um link simbólico para /bin64, chamadas repetidas para pathappend /binpoderão resultar em dizer PATH …:/bin64:/bin64:/bin64:/bin64:…. Você provavelmente deve (também) verificar se o novo valor $ARGjá está em PATH.
Scott
(1) Boa observação, eu consertei. (2) em que caso o readlink falharia? Supondo que um caminho esteja correto e se refira a um local válido. (3) Não tenho certeza do que determina a função principal do readlink, acredito que a maioria dos caminhos (se não todos?) Em um sistema de arquivos unix pode ser dividida em 2 tipos de links, links simbólicos e links físicos. Quanto à entrada de caminho duplicado, você está certo, mas o objetivo da minha resposta não foi adicioná-lo (como notei que outras respostas já o mencionaram). A única razão pela qual eu estou adicionando ainda outra resposta é contribuir algo que eu pensei que não estava já contribuiu
qwertyzw
(2) Quero dizer, se pelo menos o nome do comando implicar / sugerir sua finalidade, então 'link' no readlink pode se referir a links físicos ou virtuais. Você está correto, no entanto: man readlink diz 'readlink - imprima links simbólicos resolvidos ou nomes de arquivos canônicos'; .no meu exemplo, acredito que possa ser considerado um nome de arquivo canônico. Corrigir?
precisa saber é o seguinte
(1) Para as pessoas que entendem links simbólicos, readlinko nome implica claramente seu objetivo - ele olha para um link simbólico e lê o nome do caminho que ele contém (ou seja, aponta para). (2) Bem, eu disse que não sabia por que readlinkiria falhar. Eu estava afirmando que, se um script ou função contém vários comandos, e o comando  n é garantido como falha catastroficamente (ou não faz sentido) se o comando  m falhar (onde m  <  n ), é uma boa prática prudente verifique se o comando m falhou e lide com isso de alguma maneira - pelo menos, ... (continua)
Scott
(Continua) ... interrompa o script / função com um diagnóstico. Hipoteticamente, isso readlinkpoderá falhar se (a) o diretório for renomeado ou excluído (por outro processo) entre suas chamadas para teste readlink, ou (b) se /usr/bin/readlinkfor excluído (ou corrompido). (3) Você parece estar perdendo o meu argumento. Não estou incentivando você a duplicar outras respostas; Estou dizendo que, verificando se o original ARG(na linha de comando) já está dentro PATH, mas não repetindo a verificação do novo ARG(a saída de readlink), sua resposta está incompleta e, portanto, incorreta. ... (continua)
Scott
1
function __path_add(){  
if [ -d "$1" ] ; then  
    local D=":${PATH}:";   
    [ "${D/:$1:/:}" == "$D" ] && PATH="$PATH:$1";  
    PATH="${PATH/#:/}";  
    export PATH="${PATH/%:/}";  
fi  
}  
GreenFox
fonte
1

Desta forma, funciona bem:

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

Minhas versões são menos cuidadosas com caminhos vazios e insistindo em que os caminhos sejam válidos e diretórios do que alguns postados aqui, mas eu acho uma coleção grande de prefpend / append / clean / unique-ify / etc. funções shell para serem úteis na manipulação de caminhos. O lote inteiro, em seu estado atual, está aqui: http://pastebin.com/xS9sgQsX (feedback e melhorias muito bem-vindos!)

andybuckley
fonte
0

Você pode usar um liner perl one:

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

Aqui está no 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
}
dpatru
fonte
0

Modifiquei levemente a resposta de Gordon Davisson para usar o dir atual, se nenhum for fornecido. Então você pode simplesmente fazer a paddpartir do diretório que deseja adicionar ao seu PATH.

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

Você pode verificar se uma variável personalizada foi definida, caso contrário, defina-a e adicione as novas entradas:

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

Obviamente, essas entradas ainda poderão ser duplicadas se adicionadas por outro script, como /etc/profile.

David Kennedy
fonte
0

Este script permite adicionar no final de $PATH:

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

Ou adicione no início de $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
}
Tom Hale
fonte
0

Aqui está uma maneira compatível com 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"
        }
}

É extraído da resposta de Guillaume Perrault-Archambault a esta pergunta e da resposta de mike511 aqui .

UPDATE 2017-11-23: Bug corrigido por @Scott

go2null
fonte
Eu ia aprovar isso por fornecer uma opção de linha de comando para escolher entre pré-anexar e pós-anexar (anexar), com um padrão. Mas então pensei: este é um monte de código um tanto enigmático, sem explicação. (E o fato de você ter duas funções, em que uma altera PATH e ecoa seu novo valor, e a outra captura essa saída e a atribui a PATH novamente , é apenas uma complexidade desnecessária.)… (Continua)
Scott
(Continua) ... E então eu notei que os links estavam errados. (E não sei por que você está culpando esses caras; parece que você não copiou muito as respostas deles.) E então percebi que o código estava errado. Eu acho que ele faz um bom trabalho em manter um PATH bem formado, mas não há garantia de que ele esteja bem formado, especialmente se você tiver um não esclarecido /etc/profile. O diretório que você está tentando adicionar ao PATH já pode estar lá mais de uma vez , e seu código é bloqueado por isso. ... (continua)
Scott
(Continua)… Por exemplo, se você tentar se anexar /a/cka /b:/a/ck:/tr:/a/ck, você recebe /a/ck:/b:/a/ck:/tr:/tr:/a/ck.
Scott Scott