Como posso adicionar corretamente ao $ PATH?

31

Gostaria de adicionar uma maneira a $ PATH, em todo o sistema ou para um usuário individual, sem adicionar o mesmo caminho várias vezes.

Uma razão para querer fazer isso é para que as adições possam ser feitas .bashrc, o que não requer login, e também é mais útil em sistemas que usam (por exemplo) lightdm, que nunca chama .profile.

Estou ciente das perguntas relacionadas à limpeza de duplicatas do $ PATH, mas não desejo remover duplicatas . Gostaria de adicionar caminhos apenas se eles ainda não estiverem presentes.

Cachinhos Dourados
fonte
possível duplicata de manter duplica fora do $ PATH na fonte
Ciro Santilli新疆改造中心法轮功六四事件
goldi, eu não sei por que, mas eu tinha visto seu primeiro comentário, mesmo com o vazio. Mas sim, prefixos de nomes também funcionam, não se preocupe! Fechar o outro lado também é bom.
Ciro Santilli escreveu
Ok, desde que você tenha recebido minha mensagem. Às vezes, fazer uma reversão como essa causa um pouco de caos, acho que veremos o que acontece.
precisa saber é o seguinte
Veja também Adicionar diretório ao $ PATH, se ele ainda não estiver lá (no Superusuário ).
G-Man diz 'Reinstate Monica'

Respostas:

35

Suponha que o novo caminho que queremos adicionar seja:

new=/opt/bin

Então, usando qualquer shell POSIX, podemos testar para ver se newjá está no caminho e adicioná-lo se não estiver:

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

Observe o uso de dois pontos. Sem os dois pontos, poderíamos pensar que, digamos, new=/binjá estava no caminho porque o padrão correspondia /usr/bin. Enquanto os PATHs normalmente têm muitos elementos, os casos especiais de zero e um elemento no PATH também são tratados. O caso do caminho inicialmente não contendo elementos (sendo vazio) é tratada pela utilização de ${PATH:=$new}que cessionários PATHpara $newse estiver vazia. Definir valores padrão para parâmetros dessa maneira é um recurso de todos os shells do POSIX: consulte a seção 2.6.2 dos documentos do POSIX .)

Uma função que pode ser chamada

Por conveniência, o código acima pode ser colocado em uma função. Essa função pode ser definida na linha de comando ou, para disponibilizá-la permanentemente, ser inserida no script de inicialização do seu shell (para usuários do bash, seria ~/.bashrc):

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

Para usar esta função de atualização de caminho para adicionar um diretório ao PATH atual:

pupdate /new/path
John1024
fonte
@hammar OK. Eu adicionei um caso para isso.
precisa saber é o seguinte
1
Você pode salvar duas distinções de caso - cf. unix.stackexchange.com/a/40973/1131 .
precisa saber é o seguinte
3
Se PATHestiver vazio, isso adicionará uma entrada vazia (ou seja, o diretório atual) ao arquivo PATH. Eu acho que você precisa de outro caso.
CB Bailey
2
@CharlesBailey Não é outro case. Apenas faça case "${PATH:=$new}". Veja minha própria resposta para fallbacks semelhantes.
precisa saber é o seguinte
1
@ mc0e Adicionei um exemplo de como usar uma função shell para ocultar o "ruído da linha".
precisa saber é o seguinte
9

Crie um arquivo /etc/profile.dchamado, por exemplo, mypath.sh(ou o que você quiser). Se você estiver usando o lightdm, verifique se é viável ou use /etc/bashrcum arquivo originado do mesmo. Adicione a isso as seguintes funções:

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
}

As coisas no início de (precedido por) $ PATH têm precedência sobre o que se segue e, inversamente, as coisas no final (em anexo) serão substituídas pelo que vem antes. Isso significa que se o seu $ PATH é /usr/local/bin:/usr/bine existe um executável gotchanos dois diretórios, aquele em /usr/local/binque será usado por padrão.

Agora você pode - neste mesmo arquivo, em outro arquivo de configuração do shell ou na linha de comando - usar:

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

Se estiver em um .bashrc, impedirá que o valor apareça mais de uma vez quando você inicia um novo shell. Há uma limitação em que, se você quiser acrescentar algo que foi acrescentado (ou seja, mover um caminho dentro de $ PATH) ou vice-versa, precisará fazer isso sozinho.

Cachinhos Dourados
fonte
dividir o $PATHcom IFS=:é, em última análise, mais flexível que case.
precisa saber é o seguinte
@mikeserv Sem dúvida. Este é um tipo de uso de hack para caseIMO. Eu imagino que awkpoderia ser usado aqui também.
Goldilocks
Este é um bom ponto. E, como eu acho, gawkpoderia atribuir diretamente $PATH.
precisa saber é o seguinte
5

Você pode fazer assim:

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

Nota: se você criar PATH a partir de outras variáveis, verifique se elas não estão vazias, pois muitos shells interpretam "" como "". .

contra-modo
fonte
+1 De acordo com a página de manual -qé exigida pelo POSIX para grep, mas não sei se isso significa que ainda existem alguns greps (não POSIX) que não o possuem.
Goldilocks
1
note que o padrão grep é excessivamente amplo. Considere usar egrep -q "(^ |:) / my / bin (: | \ $)" em vez de grep / my / bin> / dev / null. Com essa modificação, sua solução está correta e acho que é uma solução mais legível do que a resposta atualmente preferida de @ john1024. Note que eu usei aspas para que você use substituição de variáveis no lugar de/my/bin
mc0e
5

A parte importante do código é verificar se PATHcontém um caminho específico:

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

Ou seja, verifique se cada caminho PATHé delimitado em ambos os lados pelo PATHseparador ( :) e, em seguida, verifique ( -q) se a string literal ( -F) que consiste em um PATHseparador, seu caminho e outro PATHseparador existe nele. Caso contrário, você pode adicionar o caminho com segurança:

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

Isso deve ser compatível com POSIX e deve funcionar com qualquer caminho que não contenha um caractere de nova linha. É mais complexo se você deseja que ele trabalhe com caminhos contendo nova linha enquanto é compatível com POSIX, mas se você tiver um grepsuporte, -zpoderá usá-lo.

l0b0
fonte
4

Eu tenho carregado essa pequena função comigo em vários ~/.profilearquivos há anos. Eu acho que ele foi escrito pelo administrador do sistema em um laboratório Eu costumava trabalhar, mas não tenho certeza. De qualquer forma, é semelhante à abordagem de Goldilock, mas um pouco diferente:

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

Portanto, para adicionar um novo diretório ao início do PATH:

pathmunge /new/path

e até o fim:

pathmunge /new/path after
terdon
fonte
Isso funciona para mim! Mas troquei para a lógica para colocá-lo depois por padrão e substituí-lo por "antes". :)
Kevin Pauli
O pathmunge faz parte da distribuição linux centos / etc / profile, possui um parâmetro antes e depois. Eu não vejo isso na minha última ubuntu 16.
Kemin Zhou
Parece funcionar bem no macOS 10.12 após /bin/grep->grep
Ben Creasy 23/02
4

ATUALIZAR:

Notei que sua própria resposta tinha uma função separada para anexar ou anexar à $PATH. Gostei da ideia. Então eu adicionei um pouco de manipulação de argumentos. Eu também o coloquei no _namespace corretamente :

_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

SAÍDA:

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

Por padrão, ele -A anexado a $PATH, mas você pode alterar esse comportamento para -Padicionar novamente em -Pqualquer lugar da sua lista de argumentos. Você pode voltar ao -Apêndulo, entregando-o -Anovamente.

AVALIAÇÃO SEGURA

Na maioria dos casos, recomendo que as pessoas evitem qualquer uso de eval. Mas acho que isso se destaca como um exemplo de seu uso para sempre. Nesse caso, a única instrução que você eval pode ver é P=ou A=. Os valores de seus argumentos são rigorosamente testados antes de serem chamados. É para isso que eval serve.

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
}

Isto irá aceitar tantos argumentos como você dá-lo e adicionar cada um para $PATHapenas uma vez e somente se ele já não estiver no $PATH. Ele usa apenas shell-script POSIX totalmente portátil, depende apenas de embutidos no shell e é muito rápido.

% 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
mikeserv
fonte
@ TAFKA'goldilocks 'veja a atualização aqui - você me inspirou.
precisa saber é o seguinte
+1 Por curiosidade (talvez essa seja uma boa perguntas e respostas separadas), de onde vem a idéia de que _prefixar funções de shell as torna "espaçadas corretamente"? Em outros idiomas, normalmente indica uma função global interna (ou seja, que precisa ser global, mas não se destina a ser usada externamente como parte de uma API). Meus nomes certamente não são ótimas opções, mas me parece que apenas o uso _não resolve os problemas de colisão - seria melhor usar um espaço de nome real, por exemplo. mikeserv_path_assign().
25914
@ TAFKA'goldilocks '- seria melhor ficar ainda mais específico com ele, mas quanto mais o nome ficar, menos conveniente será o seu uso. Mas se você tiver algum binário executável adequado com o prefixo _, precisará trocar de gerenciador de pacotes. De qualquer forma, isso é essencialmente uma função "global, interna" - é global para todos os shell invocados no shell em que é declarado e é apenas um pouco de script de linguagem interpretada pendurado na memória do intérprete . unix.stackexchange.com/questions/120528/...
mikeserv
Você não pode unset a(ou equivalente) no final do perfil?
sourcejedi
0

Contemplar! A função shell industrial de 12 linhas ... com capacidade industrial e zsh portátil que adora devotamente o seu script de inicialização ~/.bashrcou ~/.zshrcde escolha:

# 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
}

Prepare-se para a glória instantânea. Então, ao invés de fazer isso e desejar o melhor:

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

Faça isso e tenha a garantia de obter o melhor, mesmo que você realmente queira ou não:

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

Muito bem, defina "Melhor".

Anexar e anexar com segurança à corrente ${PATH}não é o assunto trivial que costuma ser feito. Embora convenientes e aparentemente sensatos, as linhas de uma linha do formulário export PATH=$PATH:~/opt/binconvidam complicações diabólicas com:

  • Nomes de diretório acidentalmente relativos (por exemplo, export PATH=$PATH:opt/bin). Enquanto bashe zshsilenciosamente aceitar e principalmente ignorar dirnames relativas na maioria dos casos, dirnames relativos prefixado por qualquer hou t(e possivelmente outros personagens nefastos) causa tanto para vergonhosamente se mutilam ala de Masaki Kobayashi seminal 1962 masterpiece Harakiri :

    # 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.
  • Nomes de diretórios acidentalmente duplicados. Embora ${PATH}nomes de diretórios duplicados sejam amplamente inócuos, eles também são indesejados, pesados, levemente ineficientes, impedem a depuração e promovem o desgaste da unidade - mais ou menos como esta resposta. Embora os SSDs do estilo NAND sejam (é claro ) imunes ao desgaste de leitura, os HDDs não são. O acesso desnecessário ao sistema de arquivos em cada comando tentado implica desgaste desnecessário da cabeça de leitura no mesmo ritmo. As duplicatas são particularmente untuosas ao invocar conchas aninhadas em subprocessos aninhados, nesse ponto aparentemente inofensivas como Não deixe isso acontecer com seus preciosos filhos. )export PATH=$PATH:~/wat rapidamente explodem no Sétimo Círculo do ${PATH}Inferno PATH=/usr/local/bin:/usr/bin:/bin:/home/leycec/wat:/home/leycec/wat:/home/leycec/wat:/home/leycec/wat. Somente o Beelzebubba pode ajudá-lo se você anexar nomes de diretório adicionais a ele. (

  • Nomes de diretórios ausentes acidentalmente.Novamente, embora os ${PATH}nomes de diretórios ausentes sejam amplamente inócuos, eles também são geralmente indesejados, pesados, levemente ineficientes, impedem a depuração e promovem o desgaste da unidade.

Ergo, automação amigável como a função shell definida acima. Nós devemos nos salvar de nós mesmos.

Mas ... Por que "+ path.append ()"? Por que não simplesmente append_path ()?

Por desambigüidade (por exemplo, com comandos externos nas ${PATH}funções de shell atuais ou em todo o sistema definidas em outro lugar), as funções de shell definidas pelo usuário são idealmente prefixadas ou com sufixo com substrings exclusivos suportados por bashe, de zshoutra forma, proibidos para nomes de bases de comando padrão - como, por exemplo +,.

Ei. Funciona. Não me julgue.

Mas ... Por que "+ path.append ()"? Por que não "+ path.prepend ()"?

Porque anexar à corrente ${PATH}é mais seguro do que anexar à corrente ${PATH}, todas as coisas sendo iguais, o que nunca são. A substituição de comandos em todo o sistema por comandos específicos do usuário pode ser insalubre na melhor das hipóteses e loucura na pior. No Linux, por exemplo, os aplicativos downstream normalmente esperam as variantes de comandos do GNU coreutils , em vez de derivadas ou alternativas não padronizadas personalizadas.

Dito isto, existem casos de uso válidos para isso. Definir a +path.prepend()função equivalente é trivial. Sans prolix nebulosity, por sua sanidade compartilhada:

+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
}

Mas ... Por que não Gilles?

A resposta aceita de Gilles em outros lugares é impressionantemente ótima no caso geral como um "anexo idempotente independente de shell" . No caso comum de bashe zshcom não links simbólicos indesejáveis, no entanto, a pena de desempenho necessário para fazê-lo entristece o espremedor de Gentoo em mim. Mesmo na presença de links simbólicos indesejáveis, é discutível se bifurcar um subshell poradd_to_PATH() argumento vale a inserção potencial de duplicatas simbólicos.

Para casos de uso rigorosos que exigem a eliminação de duplicatas de links simbólicos, essa zshvariante específica faz isso por meio de componentes internos eficientes, em vez de garfos ineficientes:

+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
}

Observe o *":${dirname:A}:"*invés *":${dirname}:"*do original. :Aé um zshismo maravilhoso tristemente ausente na maioria das outras conchas - inclusive bash. Para citar man zshexpn:

R : Transforme um nome de arquivo em um caminho absoluto, como o amodificador, e passe o resultado pela realpath(3)função de biblioteca para resolver os links simbólicos. Nota: em sistemas que não possuem uma realpath(3)função de biblioteca, os links simbólicos não são resolvidos, portanto, nesses sistemas ae Asão equivalentes.

Sem mais perguntas.

De nada. Desfrute de bombardeios seguros. Agora você merece.

Cecil Curry
fonte
0

Aqui está minha versão no estilo de programação funcional.

  • Funciona para qualquer delimitado por dois pontos *PATH variável , não apenasPATH .
  • Não acessa estado global
  • Funciona apenas com / em suas entradas imutáveis
  • Produz uma única saída
  • Sem efeitos colaterais
  • Memorável (em princípio)

Também digno de nota:

  • Agnóstico em relação a export ing; deixado para o chamador (veja exemplos)
  • Puro bash; sem bifurcação
path_add () {
  # $ 1: elemento para garantir que esteja na cadeia de caminho fornecida exatamente uma vez
  # $ 2: valor da cadeia de caminho existente ("$ PATH", não "PATH")
  # $ 3 (opcional, qualquer coisa): se fornecido, acrescente $ 1; caso contrário, acrescente
  #
  # Exemplos:
  # $ export PATH = $ (caminho_adicionar '/ opt / bin' "$ PATH")
  # $ CDPATH = $ (path_add '/ Music' "$ CDPATH" em_end)

  local -r já_present = "(^ |:) $ {1} ($ | :)"
  if [["$ 2" = ~ $ já está presente]]; então
    eco "$ 2"
  elif [[$ # == 3]]; então
    eco "$ {2}: $ {1}"
  outro
    eco "$ {1}: $ {2}"
  fi
}
Phil Hudson
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" ] && [ "$prefix" != "" ] && PATH=$prefix:$PATH
}
Tom Hale
fonte