Como posso adicionar de forma limpa a $ PATH?

29

Eu gostaria de adicionar algumas coisas ao $ PATH, em todo o sistema ou para um usuário individual, sem potencialmente adicionar o mesmo caminho várias vezes.

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

Estou ciente de questões relacionadas a como limpar duplicatas de $ PATH, mas não quero remover duplicatas . Gostaria de adicionar caminhos apenas se ainda não estiverem presentes.

    
por goldilocks 12.04.2014 / 22:13

8 respostas

33

Suponha que o novo caminho que queremos adicionar é:

new=/opt/bin

Em seguida, usando qualquer shell POSIX, podemos testar para ver se new já 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, podemos pensar que, digamos, new=/bin já estava no caminho porque o padrão correspondia a /usr/bin . Embora os PATHs normalmente tenham muitos elementos, os casos especiais de zero e um dos elementos no PATH também são tratados. O caso do PATH inicialmente não ter elementos (estar vazio) é manipulado pelo uso de ${PATH:=$new} , que atribui PATH a $new se estiver vazio. Definir valores padrão para parâmetros dessa maneira é um recurso de todos os shells POSIX: consulte a seção 2.6.2 do POSIX docs .)

Uma função que pode ser chamada

Por conveniência, o código acima pode ser colocado em uma função. Esta função pode ser definida na linha de comando ou, para tê-la disponível permanentemente, colocada no script de inicialização do seu shell (para usuários do bash, isso 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
    
por 12.04.2014 / 22:34
9

Crie um arquivo em /etc/profile.d chamado, por exemplo, mypath.sh (ou o que você quiser). Se você estiver usando lightdm, verifique se isso é viável ou use /etc/bashrc ou um arquivo de origem. 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 começo do ($ precedente) $ PATH têm precedência sobre o que segue, e inversamente, as coisas no final (acrescentadas) serão substituídas pelo que vem antes. Isso significa que se seu $ PATH for /usr/local/bin:/usr/bin e houver um executável gotcha em ambos os diretórios, o em /usr/local/bin será usado por padrão.

Você pode agora - neste mesmo arquivo, em outro arquivo de configuração do shell, ou a partir da linha de comando - use:

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

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

    
por 12.04.2014 / 22:13
5

Você pode fazer assim:

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

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

    
por 12.04.2014 / 22:29
5

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

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

Ou seja, certifique-se de que cada caminho em PATH esteja delimitado em ambos lados pelo separador PATH ( : ), então verifique ( -q ) se a string literal ( -F ) consistindo de um separador PATH , seu caminho e outro separador PATH existe lá. Se não, 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ê quiser trabalhar com caminhos que contenham nova linha, sendo compatível com POSIX, mas se você tem um grep que suporta -z você pode usar isso.

    
por 12.04.2014 / 22:43
4

Eu tenho carregado esta pequena função comigo em vários arquivos ~/.profile por anos. Eu acho que foi escrito pelo administrador de sistemas em um laboratório que eu costumava trabalhar, mas não tenho certeza. De qualquer forma, é semelhante à abordagem de Goldilock, mas ligeiramente diferente:

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

Então, para adicionar um novo diretório ao início do PATH :

pathmunge /new/path

e até o fim:

pathmunge /new/path after
    
por 13.04.2014 / 01:19
4

UPDATE:

Percebi que sua própria resposta tinha uma função separada para anexar ou preceder ao $PATH . Eu gostei da ideia. Então eu adicionei um pouco de manipulação de argumentos. Eu também propriamente _ namespaced ele:

_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

OUTPUT:

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

Por padrão, ela será -A ppend para $PATH , mas você pode alterar esse comportamento para -P repend adicionando um -P em qualquer parte de sua lista de argumentos. Você pode voltar para -A ppending, entregando novamente -A .

AVALIAÇÃO SEGURA

Na maioria dos casos, recomendo que as pessoas evitem o uso de eval . Mas isso, eu acho, se destaca como um exemplo de seu uso para bom. Nesse caso, a única instrução eval pode ver é P= ou A= . Os valores de seus argumentos são rigorosamente testados antes mesmo 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
}

Isso aceitará quantos argumentos você der e adicionará cada um a $PATH apenas uma vez e somente se ainda não estiver em $PATH . Ele faz uso apenas do shell script POSIX totalmente portátil, depende apenas dos built-ins do 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
    
por 13.04.2014 / 06:52
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
}
    
por 17.11.2017 / 02:53
0

Veja! A função industrial de 12 linhas ... tecnicamente bash e zsh-portável que adora o seu% script de inicialização~/.bashrc ou ~/.zshrc de 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, em vez 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, quer 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 prefixar com segurança o ${PATH} atual não é o assunto trivial que costuma ser. Embora seja conveniente e aparentemente sensato, os marcadores da forma export PATH=$PATH:~/opt/bin apresentam complicações diabólicas:

  • Dirnames relativos a um acidente (por exemplo, export PATH=$PATH:opt/bin ). Enquanto bash e zsh silenciosamente aceitam e ignoram principalmente os dirnames relativos em mais casos, os nomes de diretório relativos prefixados por h ou t (e possivelmente outros caracteres nefastos) fazem com que ambos vergonhosamente mutilem a obra-prima seminal de 1962 do ala Masaki Kobayashi > 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.
    
  • Dircames acidentalmente duplicados. Embora% duplicado${PATH} dirnames sejam amplamente inócuos, eles também são indesejados, incômodos, levemente ineficientes, impedem a depuração e promovem o desgaste da unidade - sorta como essa resposta . Enquanto os SSDs estilo NAND são ( é claro ) imunes ao uso de leitura, os HDDs não são. O acesso desnecessário ao sistema de arquivos em todo comando tentado implica desgaste desnecessário da cabeça de leitura no mesmo andamento. As duplicatas são particularmente untuosas quando invocam shells aninhados em subprocessos aninhados, ponto no qual aparentemente one-liners inofensivos como export PATH=$PATH:~/wat rapidamente explodem no Sétimo Círculo de ${PATH} Inferno como PATH=/usr/local/bin:/usr/bin:/bin:/home/leycec/wat:/home/leycec/wat:/home/leycec/wat:/home/leycec/wat . Apenas Beelzebubba pode ajudá-lo se você adicionar mais nomes para isso. ( Não deixe que isso aconteça com seus filhos preciosos. )

  • Faltam nomes acidentalmente. Novamente, embora a falta de ${PATH} dirnames seja bastante inofensiva, eles também são geralmente indesejados, incômodos, levemente ineficientes, impedem a depuração e promovem o desgaste da unidade.

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

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

Por falta de ambiguidade (por exemplo, com comandos externos no atual ${PATH} ou funções de shell em todo o sistema definidas em outro lugar), as funções do shell definidas pelo usuário são idealmente prefixadas ou sufixadas com substrings exclusivas suportadas por bash e zsh de outra forma proibida para nomes de comando padrão - como, por exemplo, + .

Ei. Funciona. Não me julgue.

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

Como anexar ao atual ${PATH} é mais seguro do que prefixar ao atual ${PATH} , sendo todas as coisas iguais, o que elas nunca são. Substituir comandos de todo o sistema por comandos específicos do usuário pode ser, na melhor das hipóteses, insalubre e, na pior, maluco. No Linux, por exemplo, os aplicativos downstream geralmente esperam as variantes de comandos GNU coreutils em vez de derivativos ou alternativas não-padrão personalizados.

Dito isso, há casos de uso válidos para isso. Definir a função +path.prepend() equivalente é trivial. Nebulosa Sans prolix, 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?

Gilles ' aceite resposta em outro lugar é impressionantemente ideal no caso geral como um " shell idempotent agnóstico ". No caso comum de bash e zsh com não links simbólicos indesejáveis, no entanto, a penalidade de desempenho necessária para fazê-lo entristece o Gentoo ricer em mim. Mesmo na presença de links simbólicos indesejáveis, é discutível se forçar um subshell por add_to_PATH() argumento vale a inserção potencial de duplicatas symlink.

Para casos de uso rigoroso que exigem que até mesmo as duplicatas de links simbólicos sejam eliminadas, essa variante zsh -específica faz isso por meio de builtins eficientes em vez de forquilhas 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}:"* em vez de *":${dirname}:"* do original. :A é um zsh -ísmo maravilhoso ausente na maioria das outras camadas - incluindo bash . Para citar man zshexpn :

A: Turn a file name into an absolute path as the a modifier does, and then pass the result through the realpath(3) library function to resolve symbolic links. Note: on systems that do not have a realpath(3) library function, symbolic links are not resolved, so on those systems a and A are equivalent.

Sem mais perguntas.

De nada. Desfrute de bombardeios seguros. Você agora merece isso.

    
por 08.11.2018 / 09:43

Tags