Comment puis-je ajouter proprement à $ PATH?


31

Je voudrais un moyen d'ajouter des choses à $ PATH, à l'échelle du système ou pour un utilisateur individuel, sans potentiellement ajouter plusieurs fois le même chemin.

Une raison de vouloir le faire est que des ajouts puissent être effectués .bashrc, qui ne nécessitent pas de connexion, et sont également plus utiles sur les systèmes qui utilisent (par exemple) lightdm, qui n'appelle jamais .profile.

Je connais des questions sur la façon de nettoyer les doublons de $ PATH, mais je ne veux pas supprimer les doublons . Je voudrais un moyen d' ajouter des chemins uniquement s'ils ne sont pas déjà présents.



goldi, je ne sais pas pourquoi mais j'avais vu ton premier commentaire même avec le vide. Mais oui, les préfixes de noms fonctionnent aussi, pas de soucis! Fermer dans l'autre sens est également une bonne chose.
Ciro Santilli 新疆 改造 中心 法轮功 六四 事件

D'accord, tant que vous recevez mon message. Parfois, faire un renversement comme celui-ci provoque un peu de chaos, je suppose que nous verrons ce qui se passe.
goldilocks

Réponses:


35

Supposons que le nouveau chemin que nous voulons ajouter soit:

new=/opt/bin

Ensuite, en utilisant n'importe quel shell POSIX, nous pouvons tester pour voir s'il newest déjà dans le chemin et l'ajouter s'il ne l'est pas:

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

Notez l'utilisation de deux points. Sans les deux points, nous pourrions penser que, disons, new=/binétait déjà sur le chemin parce que le modèle correspondait /usr/bin. Alors que les CHEMINS comportent normalement de nombreux éléments, les cas particuliers de zéro et d'un élément dans le CHEMIN sont également traités. Le cas du chemin ayant un premier pas d' éléments (étant vide) est assurée par l'utilisation de ${PATH:=$new}qui attribue PATHà $newsi elle est vide. La définition de valeurs par défaut pour les paramètres de cette manière est une caractéristique de tous les shells POSIX: voir la section 2.6.2 de la documentation POSIX .)

Une fonction appelable

Pour plus de commodité, le code ci-dessus peut être mis dans une fonction. Cette fonction peut être définie sur la ligne de commande ou, pour la rendre disponible en permanence, insérée dans le script d'initialisation de votre shell (pour les utilisateurs bash, ce serait ~/.bashrc):

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

Pour utiliser cette fonction de mise à jour de chemin d'accès pour ajouter un répertoire au PATH actuel:

pupdate /new/path

1
Vous pouvez enregistrer 2 distinctions de cas - cf. unix.stackexchange.com/a/40973/1131 .
maxschlepzig

3
Si PATHest vide, cela ajoutera une entrée vide (c'est-à-dire le répertoire courant) au PATH. Je pense que vous avez besoin d'un autre cas.
CB Bailey

2
@CharlesBailey Pas un autre case. Faites-le case "${PATH:=$new}". Voir ma propre réponse pour des solutions de rechange similaires.
mikeserv

1
@ mc0e J'ai ajouté un exemple d'utilisation d'une fonction shell pour masquer le "bruit de ligne".
John1024

1
@Doogle: uniq ne détecte les doublons que s'ils sont adjacents les uns aux autres, donc je ne pense pas que cela supprimera les doublons si un chemin apparaît au début et à la fin de $ PATH.
Ralph

9

Créez un fichier /etc/profile.dappelé, par exemple, mypath.sh(ou ce que vous voulez). Si vous utilisez lightdm, assurez-vous que c'est viable ou bien utilisez /etc/bashrcou un fichier provenant de celui-ci. Ajoutez à cela les fonctions suivantes:

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
}

Les choses au début de (précédé de) $ PATH ont priorité sur ce qui suit, et inversement, les choses à la fin (ajoutées) seront remplacées par ce qui précède. Cela signifie que si votre $ PATH est /usr/local/bin:/usr/binet qu'il y a un exécutable gotchadans les deux répertoires, celui de /usr/local/binsera utilisé par défaut.

Vous pouvez maintenant - dans ce même fichier, dans un autre fichier de configuration du shell ou depuis la ligne de commande - utiliser:

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

Si c'est dans un .bashrc, cela empêchera la valeur d'apparaître plus d'une fois lorsque vous démarrez un nouveau shell. Il y a une limitation en ce que si vous voulez ajouter quelque chose qui a été ajouté (c'est-à-dire déplacer un chemin dans $ PATH) ou vice versa, vous devrez le faire vous-même.


diviser le $PATHavec IFS=:est finalement plus flexible que case.
mikeserv

@mikeserv Sans aucun doute. C'est une sorte d'utilisation de hack pour case, OMI. J'imagine qu'on awkpourrait en faire bon usage ici aussi.
goldilocks

C'est un bon point. Et, comme je pense, gawkpourrait directement attribuer $PATH.
mikeserv

5

Vous pouvez le faire de cette façon:

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

Remarque: si vous créez PATH à partir d'autres variables, vérifiez qu'elles ne sont pas vides, car de nombreux shells interprètent "" comme "". .


+1 Selon la page de manuel -qrequise par POSIX pour grep, mais je ne sais pas si cela signifie qu'il y a encore des greps (non POSIX) qui ne l'ont pas.
goldilocks

1
notez que le modèle grep est trop large. Pensez à utiliser egrep -q "(^ |:) / my / bin (: | \ $)" au lieu de grep / my / bin> / dev / null. Avec cette modification, votre solution est correcte, et je pense que c'est une solution plus lisible que la réponse actuellement préférée de @ john1024. Notez que j'ai utilisé des guillemets doubles afin que vous /my/bin
utilisiez la

5

La partie importante du code est de vérifier s'il PATHcontient un chemin spécifique:

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

Autrement dit, assurez-vous que chaque chemin d'accès PATHest délimité des deux côtés par le PATHséparateur ( :), puis vérifiez ( -q) si la chaîne littérale ( -F) constituée d'un PATHséparateur, de votre chemin d'accès et d'un autre PATHséparateur existe à l'intérieur. Si ce n'est pas le cas, vous pouvez ajouter le chemin en toute sécurité:

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

Cela devrait être compatible POSIX et devrait fonctionner avec tout chemin ne contenant pas de caractère de nouvelle ligne. C'est plus complexe si vous voulez qu'il fonctionne avec des chemins contenant des sauts de ligne tout en étant compatible POSIX, mais si vous en avez un grepqui le prend en charge, -zvous pouvez l'utiliser.


4

Je porte cette petite fonction avec moi dans divers ~/.profilefichiers depuis des années. Je pense qu'il a été écrit par l'administrateur système dans un laboratoire dans lequel je travaillais, mais je ne suis pas sûr. Quoi qu'il en soit, il est similaire à l'approche de Goldilock mais légèrement différent:

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

Donc, pour ajouter un nouveau répertoire au début de la PATH:

pathmunge /new/path

et à la fin:

pathmunge /new/path after

Ça marche pour moi! Mais je suis passé à la logique pour le mettre après par défaut et remplacer par "avant". :)
Kevin Pauli

pathmunge fait partie de la distribution linux centos / etc / profile, il a un paramètre avant et après. Je ne le vois pas dans mon dernier ubuntu 16.
Kemin Zhou

Semble fonctionner correctement sur macOS 10.12 après /bin/grep->grep
Ben Creasy

4

MISE À JOUR:

J'ai remarqué que votre propre réponse avait une fonction distincte pour chaque ajout ou ajout au $PATH. J'ai aimé l'idée. J'ai donc ajouté un petit traitement des arguments. Je l'ai également correctement _nommé:

_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

SORTIE:

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

Par défaut, il -Adépendra de $PATH, mais vous pouvez modifier ce comportement pour qu'il se -Prépète en ajoutant un -Pn'importe où dans votre liste d'arguments. Vous pouvez le remettre en -Aattente en lui remettant un -A.

ÉVALUATION SÉCURITAIRE

Dans la plupart des cas, je recommande aux gens d'éviter toute utilisation de eval. Mais cela, je pense, se démarque comme un exemple de son utilisation pour de bon. Dans ce cas, la seule déclaration que vous eval pouvez voir est P=ou A=. Les valeurs de ses arguments sont strictement testées avant d'être appelée. C'est pour ça eval .

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
}

Cela acceptera autant d'arguments que vous le donnerez et n'en ajoutera chacun $PATHqu'une seule fois et seulement s'il n'est pas déjà dedans $PATH. Il utilise uniquement un script shell POSIX entièrement portable, ne s'appuie que sur des shell intégrés et est très rapide.

% 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 'voyez la mise à jour ici - vous m'avez inspiré.
mikeserv

+1 Par curiosité (ce serait peut-être un bon Q&R séparé), d'où vient l'idée que le _préfixage des fonctions de shell les rend "correctement espacées"? Dans d'autres langues, cela indique généralement une fonction globale interne (c'est-à-dire une fonction qui doit être globale, mais qui n'est pas destinée à être utilisée en externe dans le cadre d'une API). Mes noms ne sont certainement pas d'excellents choix, mais il me semble que l'utilisation _ne résout pas du tout les problèmes de collision - il serait préférable de s'attaquer à un espace de noms réel, par exemple. mikeserv_path_assign().
goldilocks

@ TAFKA'goldilocks '- il serait préférable d'être encore plus précis avec lui, mais plus le nom est long, moins son utilisation est pratique. Mais si vous avez des binaires exécutables appropriés préfixés, _vous devez changer de gestionnaire de package. Dans tous les cas, c'est essentiellement une fonction "globale, interne" - elle est globale pour chaque shell invoqué depuis le shell dans lequel elle est déclarée, et ce n'est qu'un peu de script de langage interprété qui traîne dans la mémoire de l'interpréteur . unix.stackexchange.com/questions/120528/…
mikeserv

Pouvez-vous pas unset a(ou équivalent) à la fin du profil?
sourcejedi

0

Voir! La fonction de coque portable 12 lignes ... techniquement bash et zsh portable qui aime avec dévouement votre script de démarrage ~/.bashrcou ~/.zshrcde choix:

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

Préparez-vous à la gloire instantanée. Ensuite, plutôt que de le faire et en espérant le meilleur:

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

Faites-le à la place et soyez assuré d'obtenir le meilleur, que vous le vouliez vraiment ou non:

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

Très bien, définissez «le meilleur».

Ajouter et ajouter en toute sécurité au courant ${PATH}n'est pas l'affaire banale qu'il est communément établi. Bien que pratiques et apparemment sensées, les lignes simples du formulaire export PATH=$PATH:~/opt/bininvitent à des complications diaboliques avec:

  • Noms de répertoires accidentellement relatifs (par exemple, export PATH=$PATH:opt/bin). Alors que bashet en zshsilence acceptent et ignorent la plupart du temps les noms relatifs dans la plupart des cas, les noms relatifs préfixés par l'un hou l' autre t(et peut-être d'autres personnages néfastes) font mutuellement honte de se mutiler ala Masaki Kobayashi, chef-d'œuvre de 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.
  • Duplication accidentelle de noms de répertoires. Bien que les ${PATH}noms de répertoires en double soient en grande partie inoffensifs, ils sont également indésirables, encombrants, légèrement inefficaces, entravent le débogage et favorisent l'usure du lecteur - en quelque sorte comme cette réponse. Alors que les SSD de style NAND sont ( bien sûr ) à l'abri de l'usure en lecture, les disques durs ne le sont pas. L'accès inutile au système de fichiers à chaque tentative de commande implique une usure inutile de la tête de lecture au même tempo. Les doublons sont particulièrement onctueux lors de l'invocation de coquilles imbriquées dans des sous-processus imbriqués, à ce moment-là, des lignes uniformes apparemment inoffensives comme export PATH=$PATH:~/watexplosent rapidement dans le septième cercle de l' ${PATH}enfer PATH=/usr/local/bin:/usr/bin:/bin:/home/leycec/wat:/home/leycec/wat:/home/leycec/wat:/home/leycec/wat. Seul Beelzebubba peut vous aider si vous ajoutez ensuite des noms de répertoires supplémentaires à cela. (Ne laissez pas cela arriver à vos précieux enfants. )

  • Répertoires manquants accidentellement. Encore une fois, bien que les ${PATH}noms manquants soient en grande partie inoffensifs, ils sont également généralement indésirables, encombrants, légèrement inefficaces, entravent le débogage et favorisent l'usure du lecteur.

Ergo, automatisation conviviale comme la fonction shell définie ci-dessus. Nous devons nous sauver de nous-mêmes.

Mais ... Pourquoi "+ path.append ()"? Pourquoi pas simplement append_path ()?

Pour disambiguity (par exemple, avec des commandes externes dans le courant de ${PATH}fonctions ou à l' échelle du système shell définies ailleurs) fonctions, shell définies par l' utilisateur sont idéalement préfixés ou suffixé avec des sous - chaînes uniques pris en charge par bashet zshmais par ailleurs interdit de commande standard des noms de base - comme, par exemple, +.

Hey. Ça marche. Ne me jugez pas.

Mais ... Pourquoi "+ path.append ()"? Pourquoi pas "+ path.prepend ()"?

Parce que l'ajout au courant ${PATH}est plus sûr que l'ajout au courant ${PATH}, toutes choses étant égales par ailleurs, ce qu'elles ne sont jamais. Remplacer les commandes à l'échelle du système par des commandes spécifiques à l'utilisateur peut être au mieux insalubre et au pire rendre fou. Sous Linux, par exemple, les applications en aval attendent généralement les variantes de commandes GNU coreutils plutôt que des dérivés ou alternatives non standard personnalisés.

Cela dit, il existe absolument des cas d'utilisation valables pour le faire. La définition de la +path.prepend()fonction équivalente est triviale. Nébulosité sans prolixe, pour sa santé mentale partagée:

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

Mais ... Pourquoi pas Gilles?

La réponse acceptée par Gilles ailleurs est remarquablement optimale dans le cas général en tant qu ' «appendice idempotent agnostique à la coque» . Dans le cas commun bashet zshavec aucun des liens symboliques indésirables, cependant, la pénalité de performance nécessaire pour le faire attriste le ricer Gentoo en moi. Même en présence de liens symboliques indésirables, il est discutable de savoir si la création d'un sous-shell par add_to_PATH()argument vaut l'insertion potentielle de doublons de liens symboliques.

Pour les cas d'utilisation stricts exigeant que même les doublons de liens symboliques soient éliminés, cette zshvariante spécifique le fait via des fonctions intégrées efficaces plutôt que des fourches inefficaces:

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

Notez le *":${dirname:A}:"*plutôt que *":${dirname}:"*l'original. :Aest un merveilleux zsh-isme malheureusement absent sous la plupart des autres obus - y compris bash. Pour citer man zshexpn:

R : Transformez un nom de fichier en chemin absolu comme le afait le modificateur, puis passez le résultat via la realpath(3)fonction de bibliothèque pour résoudre les liens symboliques. Remarque: sur les systèmes qui n'ont pas de realpath(3)fonction de bibliothèque, les liens symboliques ne sont pas résolus, donc sur ces systèmes aet Asont équivalents.

Pas d'autres questions.

De rien. Profitez de bombardements en toute sécurité. Vous le méritez maintenant.


0

Voici ma version de style de programmation fonctionnelle.

  • Fonctionne pour toute *PATHvariable délimitée par deux points , pas seulement PATH.
  • N'accède pas à l'état global
  • Fonctionne uniquement avec / sur ses entrées immuables données
  • Produit une sortie unique
  • Pas d'effets secondaires
  • Mémorisable (en principe)

A noter également:

  • Agnostique concernant exporting; c'est à l'appelant (voir les exemples)
  • Pure bash; pas de bifurcation
path_add () {
  # $ 1: L'élément à assurer est dans la chaîne de chemin d'accès donnée une seule fois
  # $ 2: Valeur de chaîne de chemin existante ("$ PATH", pas "PATH")
  # $ 3 (facultatif, n'importe quoi): si indiqué, ajoutez $ 1; sinon, ajoutez
  #
  # Exemples:
  # $ export PATH = $ (path_add '/ opt / bin' "$ PATH")
  # $ CDPATH = $ (path_add '/ Music' "$ CDPATH" at_end)

  local -r already_present = "(^ |:) $ {1} ($ | :)"
  if [["$ 2" = ~ $ already_present]]; puis
    écho "$ 2"
  elif [[$ # == 3]]; puis
    echo "$ {2}: $ {1}"
  autre
    echo "$ {1}: $ {2}"
  Fi
}

0

Ce script vous permet d'ajouter à la fin de $PATH:

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

Ou ajoutez au début 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
}
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.