Come posso aggiungere chiaramente a $ PATH?


31

Vorrei un modo per aggiungere cose a $ PATH, a livello di sistema o per un singolo utente, senza potenzialmente aggiungere lo stesso percorso più volte.

Un motivo per volerlo fare è che si possano effettuare aggiunte .bashrc, che non richiedono un login, ed è anche più utile sui sistemi che usano (ad esempio) lightdm, che non chiama mai .profile.

Sono a conoscenza di domande su come pulire i duplicati da $ PATH, ma non desidero rimuovere i duplicati . Vorrei un modo per aggiungere percorsi solo se non sono già presenti.



goldi, non so perché ma avevo visto il tuo primo commento anche con il vuoto. Ma sì, funzionano anche i prefissi dei nomi, non preoccuparti! Anche la chiusura dell'altro modo va bene.
Ciro Santilli 新疆 改造 中心 法轮功 六四 事件

Ok, fintanto che hai ricevuto il mio messaggio. A volte fare un'inversione come questa provoca un po 'di caos, immagino che vedremo cosa succede.
Riccioli d'oro

Risposte:


35

Supponiamo che il nuovo percorso che vogliamo aggiungere sia:

new=/opt/bin

Quindi, utilizzando qualsiasi shell POSIX, possiamo testare per vedere se newè già nel percorso e aggiungerlo se non lo è:

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

Nota l'uso dei due punti. Senza i due punti, potremmo pensare che, diciamo, new=/binfosse già sulla strada perché il modello corrispondeva /usr/bin. Mentre i PERCORSI normalmente hanno molti elementi, vengono gestiti anche i casi speciali di zero e uno nel PERCORSO. Il caso del PERCORSO inizialmente privo di elementi (essendo vuoto) viene gestito dall'uso ${PATH:=$new}che assegna PATHa $newse è vuoto. L'impostazione dei valori predefiniti per i parametri in questo modo è una caratteristica di tutte le shell POSIX: vedere la sezione 2.6.2 dei documenti POSIX .)

Una funzione richiamabile

Per comodità, il codice sopra può essere inserito in una funzione. Questa funzione può essere definita dalla riga di comando o, per renderla disponibile in modo permanente, inserendola nello script di inizializzazione della shell (per gli utenti bash, sarebbe ~/.bashrc):

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

Per utilizzare questa funzione di aggiornamento del percorso per aggiungere una directory al PERCORSO corrente:

pupdate /new/path

1
È possibile salvare 2 distinzioni di casi - cfr. unix.stackexchange.com/a/40973/1131 .
maxschlepzig,

3
Se PATHè vuoto, questo aggiungerà una voce vuota (cioè la directory corrente) al file PATH. Penso che tu abbia bisogno di un altro caso.
CB Bailey,

2
@CharlesBailey Non un altro case. Fallo e basta case "${PATH:=$new}". Vedi la mia risposta per fallback simili.
Mikeserv,

1
@ mc0e Ho aggiunto un esempio di come utilizzare una funzione shell per nascondere il "rumore di linea".
Giovanni 1024

1
@Doogle: uniq rileva i duplicati solo se sono adiacenti l'uno all'altro, quindi non credo che questo rimuoverà i duplicati se viene visualizzato un percorso all'inizio e alla fine di $ PATH.
Ralph

9

Crea un file in /etc/profile.dchiamato, ad es. mypath.sh(O qualunque cosa tu voglia). Se stai usando lightdm, assicurati che sia fattibile oppure utilizza /etc/bashrcun file proveniente dallo stesso. Aggiungete a ciò le seguenti funzioni:

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
}

Le cose all'inizio di (anteposte a) $ PATH hanno la precedenza su ciò che segue e, al contrario, le cose alla fine (aggiunte) saranno sostituite da ciò che viene prima. Questo significa che se $ PATH è /usr/local/bin:/usr/bine c'è un eseguibile gotchain entrambe le directory, quello in /usr/local/binverrà usato di default.

Ora puoi - in questo stesso file, in un altro file di configurazione della shell o dalla riga di comando - usare:

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

Se questo è in a .bashrc, eviterà che il valore appaia più di una volta quando si avvia una nuova shell. C'è una limitazione in quanto se vuoi aggiungere qualcosa che è stato anteposto (ovvero spostare un percorso all'interno di $ PATH) o viceversa, dovrai farlo da solo.


dividere il $PATHcon IFS=:è in definitiva più flessibile di case.
Mikeserv,

@mikeserv Senza dubbio. Questo è un tipo di uso di hack per case, IMO. Immagino che awkpotrebbe essere utile anche qui.
Riccioli d'oro

È un buon punto. E, come penso, gawkpotrebbe assegnare direttamente $PATH.
Mikeserv,

5

Puoi farlo in questo modo:

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

Nota: se si crea PATH da altre variabili, verificare che non siano vuote, poiché molte shell interpretano "" come "." .


+1 Secondo la pagina man -qè richiesto da POSIX per grep, ma non so se ciò significhi che ci sono ancora alcuni greps (non POSIX) che non ce l'hanno.
Riccioli d'oro,

1
si noti che il modello grep è eccessivamente ampio. Prendi in considerazione l'utilizzo di egrep -q "(^ |:) / my / bin (: | \ $)" invece di grep / my / bin> / dev / null. Con quella modifica la tua soluzione è corretta e penso che questa sia una soluzione più leggibile rispetto alla risposta attualmente preferita da @ john1024. Nota che ho usato virgolette doppie in modo da usare la sostituzione variabile al posto di/my/bin
mc0e

5

La parte importante del codice è verificare se PATHcontiene un percorso specifico:

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

Ossia, assicurati che ogni percorso PATHsia delimitato su entrambi i lati dal PATHseparatore ( :), quindi controlla ( -q) se esiste la stringa letterale ( -F) che consiste in un PATHseparatore, il tuo percorso e un altro PATHseparatore. In caso contrario, puoi aggiungere in sicurezza il percorso:

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

Questo dovrebbe essere compatibile con POSIX e dovrebbe funzionare con qualsiasi percorso che non contenga un carattere di nuova riga. È più complesso se si desidera che funzioni con percorsi contenenti newline pur essendo compatibile con POSIX, ma se si dispone di un grepsupporto che -zè possibile utilizzare.


4

Ho portato questa piccola funzione con me in vari ~/.profilefile per anni. Penso che sia stato scritto dal amministratore di sistema in un laboratorio in cui lavoravo ma non ne sono sicuro. Comunque, è simile all'approccio di Goldilock ma leggermente diverso:

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

Quindi, per aggiungere una nuova directory all'inizio di PATH:

pathmunge /new/path

e fino alla fine:

pathmunge /new/path after

Questo funziona per me! Ma sono passato alla logica per metterlo dopo per impostazione predefinita e sovrascrivere con "prima". :)
Kevin Pauli,

pathmunge fa parte della distribuzione / etc / profile di linux centos, ha un parametro prima e dopo. Non lo vedo nel mio ultimo Ubuntu 16.
Kemin Zhou

Sembra funzionare su macOS 10.12 dopo /bin/grep->grep
Ben Creasy,

4

AGGIORNARE:

Ho notato che la tua risposta aveva una funzione separata per aggiungere o anteporre a $PATH. Mi è piaciuta l'idea. Quindi ho aggiunto un po 'di gestione degli argomenti. L'ho anche _identificato correttamente :

_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

PRODUZIONE:

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

In base all'impostazione predefinita, verrà -Arisolto il problema $PATH, ma è possibile modificare questo comportamento per -Prispondere aggiungendo un -Ppunto qualsiasi nell'elenco degli argomenti. Puoi tornare a -Appending passandolo di -Anuovo.

VALUTAZIONE SICURA

Nella maggior parte dei casi, consiglio alle persone di evitare qualsiasi utilizzo eval. Ma questo, credo, spicca come esempio del suo uso per il bene. In questo caso l'unica affermazione che eval può mai vedere è P=o A=. I valori dei suoi argomenti sono rigorosamente testati prima di essere mai chiamati. Questo è ciò che 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
}

Questo accetterà tanti argomenti quanti ne darai e li aggiungerà a $PATHuna sola volta e solo se non è già presente $PATH. Utilizza solo shell-script POSIX completamente portatili, si basa solo su shell incorporate ed è molto veloce.

% 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 'guarda qui l'aggiornamento - mi hai ispirato.
Mikeserv,

+1 Per curiosità (forse questo sarebbe un buon D&R separato), da dove viene l'idea che il _prefisso delle funzioni della shell le renda "correttamente spaziate"? In altre lingue, di solito indicherebbe una funzione globale interna (ovvero, una che deve essere globale, ma non deve essere utilizzata esternamente come parte di un'API). I miei nomi non sono certamente delle grandi scelte, ma mi sembra che il solo utilizzo _non risolva affatto i problemi di collisione: sarebbe meglio puntare su un vero spazio dei nomi, ad es. mikeserv_path_assign().
Riccioli d'oro

@ TAFKA'goldilocks '- sarebbe meglio essere ancora più specifici con esso, ma più il nome diventa meno conveniente è il suo uso. Ma se hai dei binari eseguibili corretti con il prefisso, _devi cambiare i gestori dei pacchetti. In ogni caso, questo, essenzialmente, è solo una "funzione globale, interna" - è globale per ogni shell invocata dalla shell in cui viene dichiarata, ed è solo un po 'di script di linguaggio interpretato che si trovano nella memoria dell'interprete . unix.stackexchange.com/questions/120528/…
mikeserv

Non puoi unset a(o equivalente) alla fine del profilo?
sourcejedi

0

Ecco! La funzione di shell industriale a 12 linee ... tecnicamente bash e zsh-portatile che ama devotamente il tuo script di avvio ~/.bashrco di tua ~/.zshrcscelta:

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

Preparati alla gloria istantanea. Quindi, piuttosto che fare questo e sperare con speranza per il meglio:

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

Fallo invece e assicurati di ottenere il meglio, che tu lo volessi o meno:

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

Molto bene, definire "Il migliore".

Accodare e anteporre in modo sicuro alla corrente ${PATH}non è la faccenda banale che viene comunemente definita. Mentre conveniente e apparentemente sensato, le linee guida del modulo export PATH=$PATH:~/opt/bininvitano complicazioni diaboliche con:

  • Nomi indiretti relativi (ad es export PATH=$PATH:opt/bin.). Mentre bashe zshaccettano silenziosamente e per lo più ignorano i nomi relativi nella maggior parte dei casi, i nomi relativi prefissati da uno ho t(e forse altri personaggi nefasti) fanno sì che entrambi si vergognino di mutilarsi nell'ala del capolavoro fondamentale di Masaki Kobayashi del 1962 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.
  • Dirnames duplicati accidentalmente. Mentre i ${PATH}nomi duplicati sono in gran parte innocui, sono anche indesiderati, ingombranti, leggermente inefficienti, impediscono il debuggability e promuovono l'usura dell'unità - una specie come questa risposta. Mentre gli SSD in stile NAND sono ( ovviamente ) immuni dall'usura da lettura, gli HDD non lo sono. L'accesso non necessario al filesystem su ogni tentativo di comando implica un'usura della testina di lettura non necessaria allo stesso tempo. I duplicati sono particolarmente insidiosi quando si invocano conchiglie annidate in sottoprocessi annidati, a quel punto una linea apparentemente innocua come quella export PATH=$PATH:~/watesplode rapidamente nel Settimo Cerchio ${PATH}dell'Inferno come PATH=/usr/local/bin:/usr/bin:/bin:/home/leycec/wat:/home/leycec/wat:/home/leycec/wat:/home/leycec/wat. Solo Beelzebubba può aiutarti se poi aggiungi altri dirnames su quello. (Non lasciare che questo accada ai tuoi preziosi figli. )

  • Dirnames mancanti per errore. Ancora una volta, sebbene i ${PATH}nomi mancanti siano in gran parte innocui, sono anche in genere indesiderati, ingombranti, leggermente inefficienti, impediscono il debuggability e promuovono l'usura dell'unità.

Ergo, automazione amichevole come la funzione shell definita sopra. Dobbiamo salvarci da noi stessi.

Ma ... Perché "+ path.append ()"? Perché non semplicemente append_path ()?

Per disambiguity (ad esempio, con comandi esterni negli attuali ${PATH}funzioni di shell o di sistema definiti altrove), funzioni di shell definite dall'utente sono idealmente prefisso o come suffisso sottostringhe unici supportati da bashe zshma altrimenti vietati per BaseNames comando standard - come, ad esempio, +.

Hey. Funziona. Non giudicarmi.

Ma ... Perché "+ path.append ()"? Perché non "+ path.prepend ()"?

Perché accodarsi alla corrente ${PATH}è più sicuro che anteporre alla corrente ${PATH}, a parità di condizioni, cosa che non sono mai. Sostituire i comandi a livello di sistema con comandi specifici dell'utente può essere insensato nella migliore delle ipotesi e pazzo nel peggiore dei casi. Sotto Linux, ad esempio, le applicazioni a valle generalmente si aspettano le varianti di comandi GNU coreutils piuttosto che derivate o alternative personalizzate non standard.

Detto questo, ci sono assolutamente casi d'uso validi per farlo. Definire la +path.prepend()funzione equivalente è banale. Nebulosità sans prolix, per la sua sanità mentale condivisa:

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

Ma ... Perché non Gilles?

La risposta accettata da Gilles altrove è straordinariamente ottimale nel caso generale come "append idempotente agnostico della shell" . Nel caso comune bashe zshcon nessun link simbolici indesiderati, tuttavia, la pena di prestazioni necessario per rattrista il ricer Gentoo in me. Anche in presenza di collegamenti simbolici indesiderabili, è discutibile se il biforcazione di una subshell per add_to_PATH()argomento valga il potenziale inserimento di duplicati del collegamento simbolico.

Per i casi d'uso rigorosi che richiedono l'eliminazione anche dei duplicati del zshcollegamento simbolico, questa specifica variante lo fa tramite builtin efficienti piuttosto che fork inefficienti:

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

Nota *":${dirname:A}:"*piuttosto che *":${dirname}:"*l'originale. :Aè un meraviglioso zsh-ismo tristemente assente sotto la maggior parte delle altre conchiglie - incluso bash. Per citare man zshexpn:

A : Trasforma un nome di file in un percorso assoluto come fa il amodificatore, quindi passa il risultato attraverso la realpath(3)funzione di libreria per risolvere i collegamenti simbolici. Nota: sui sistemi che non dispongono di una realpath(3)funzione di libreria, i collegamenti simbolici non vengono risolti, quindi su tali sistemi ae Asono equivalenti.

Nessuna ulteriore domanda.

Prego. Goditi bombardamenti sicuri. Ora te lo meriti.


0

Ecco la mia versione in stile di programmazione funzionale.

  • Funziona con qualsiasi *PATHvariabile delimitata da due punti , non solo PATH.
  • Non accede allo stato globale
  • Funziona solo con / sui dati immutabili forniti
  • Produce un singolo output
  • Nessun effetto collaterale
  • Memorizzabile (in linea di principio)

Degno di nota anche:

  • Agnostico per quanto riguarda exporting; lasciato al chiamante (vedi esempi)
  • Puro bash; niente biforcazioni
path_add () {
  # $ 1: elemento per garantire che si trovi esattamente nella stringa di percorso specificata una volta
  # $ 2: valore della stringa del percorso esistente ("$ PATH", non "PATH")
  # $ 3 (opzionale, non importa): se indicato, aggiungi $ 1; altrimenti, anteporre
  #
  # Esempi:
  # $ export PATH = $ (path_add '/ opt / bin' "$ PATH")
  # $ CDPATH = $ (path_add '/ Music' "$ CDPATH" at_end)

  local -r already_present = "(^ |:) $ {1} ($ | :)"
  if [["$ 2" = ~ $ already_present]]; poi
    echo "$ 2"
  elif [[$ # == 3]]; poi
    echo "$ {2}: $ {1}"
  altro
    echo "$ {1}: $ {2}"
  fi
}

0

Questo script ti consente di aggiungere alla fine di $PATH:

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

Oppure aggiungi all'inizio di $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
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.