Supprimer les entrées $ PATH en double avec la commande awk


48

J'essaie d'écrire une fonction shell bash qui me permettra de supprimer les copies dupliquées des répertoires de ma variable d'environnement PATH.

On m'a dit qu'il est possible d'y parvenir avec une commande d'une ligne à l'aide de la awkcommande, mais je ne sais pas comment le faire. Quelqu'un sait comment?



Réponses:


37

Si vous n'avez pas déjà de doublons dans le PATHet que vous voulez seulement ajouter des répertoires s'ils ne sont pas déjà là, vous pouvez le faire facilement avec le shell seul.

for x in /path/to/add …; do
  case ":$PATH:" in
    *":$x:"*) :;; # already there
    *) PATH="$x:$PATH";;
  esac
done

Et voici un extrait de shell qui supprime les doublons $PATH. Il passe en revue les entrées une par une et copie celles qui n'ont pas encore été vues.

if [ -n "$PATH" ]; then
  old_PATH=$PATH:; PATH=
  while [ -n "$old_PATH" ]; do
    x=${old_PATH%%:*}       # the first remaining entry
    case $PATH: in
      *:"$x":*) ;;          # already there
      *) PATH=$PATH:$x;;    # not there yet
    esac
    old_PATH=${old_PATH#*:}
  done
  PATH=${PATH#:}
  unset old_PATH x
fi

Il serait préférable que les éléments de $ PATH soient répétés de manière inverse, car les éléments les plus récents sont généralement ajoutés récemment, et ils peuvent avoir la valeur à jour.
Eric Wang

2
@ EricWang Je ne comprends pas votre raisonnement. Les éléments PATH sont parcourus d'avant en arrière. Ainsi, lorsqu'il y a des doublons, le deuxième doublon est effectivement ignoré. Une itération de l'arrière à l'avant changerait l'ordre.
Gilles 'SO- arrête d'être méchant'

@Gilles Lorsque vous avez dupliqué la variable dans PATH, elle est probablement ajoutée de la manière suivante:, PATH=$PATH:x=ble x dans PATH d'origine peut avoir la valeur a; la valeur prendra effet.
Eric Wang

4
@EricWang Dans ce cas, la valeur ajoutée n'a aucun effet et doit donc être ignorée. En reculant, vous apportez la valeur ajoutée avant. Si la valeur ajoutée devait exister auparavant, elle aurait été ajoutée comme PATH=x:$PATH.
Gilles 'SO- arrête d'être méchant'

@Gilles Lorsque vous ajoutez quelque chose, cela signifie qu'il n'est pas encore terminé ou que vous souhaitez remplacer l'ancienne valeur. Vous devez donc rendre visible la nouvelle variable ajoutée. Et, par convention, il est généralement ajouté de la manière suivante: PATH=$PATH:...non PATH=...:$PATH. Il est donc plus approprié d'itérer un ordre inversé. Même si votre méthode fonctionne également, alors les gens s’appliquent de la manière inverse.
Eric Wang

23

Voici une solution intelligible à une ligne qui fait tout ce qui est bien: élimine les doublons, préserve l’ordre des chemins et n’ajoute pas deux points à la fin. Cela devrait donc vous donner un PATH dédupliqué qui donne exactement le même comportement que l'original:

PATH="$(perl -e 'print join(":", grep { not $seen{$_}++ } split(/:/, $ENV{PATH}))')"

Il se divise simplement en deux-points ( split(/:/, $ENV{PATH})), utilise des grep { not $seen{$_}++ }filtres pour filtrer toutes les occurrences répétées des chemins, à l'exception de la première, puis réunit les autres en les séparant par des points et affiche le résultat ( print join(":", ...)).

Si vous voulez un peu plus de structure autour de cela, ainsi que la possibilité de dédupliquer d'autres variables, essayez cet extrait, que j'utilise actuellement dans ma propre configuration:

# Deduplicate path variables
get_var () {
    eval 'printf "%s\n" "${'"$1"'}"'
}
set_var () {
    eval "$1=\"\$2\""
}
dedup_pathvar () {
    pathvar_name="$1"
    pathvar_value="$(get_var "$pathvar_name")"
    deduped_path="$(perl -e 'print join(":",grep { not $seen{$_}++ } split(/:/, $ARGV[0]))' "$pathvar_value")"
    set_var "$pathvar_name" "$deduped_path"
}
dedup_pathvar PATH
dedup_pathvar MANPATH

Ce code dédupliquera PATH et MANPATH, et vous pourrez facilement appeler dedup_pathvard'autres variables contenant des listes de chemins séparées par des points (par exemple, PYTHONPATH).


Pour une raison quelconque, j'ai dû ajouter un chomppour supprimer une nouvelle ligne. Cela a fonctionné pour moi:perl -ne 'chomp; print join(":", grep { !$seen{$_}++ } split(/:/))' <<<"$PATH"
Håkon Hægland Le

12

En voici une élégante:

printf %s "$PATH" | awk -v RS=: -v ORS=: '!arr[$0]++'

Plus long (pour voir comment ça marche):

printf %s "$PATH" | awk -v RS=: -v ORS=: '{ if (!arr[$0]++) { print $0 } }'

Ok, puisque vous êtes nouveau sur Linux, voici comment configurer PATH sans un ":"

PATH=`printf %s "$PATH" | awk -v RS=: '{ if (!arr[$0]++) {printf("%s%s",!ln++?"":":",$0)}}'`

Par ailleurs, assurez-vous de ne PAS avoir de répertoires contenant ":" dans votre PATH, sinon cela va être gâché.

un peu de crédit à:


-1 ça ne marche pas. Je vois encore des doublons sur mon chemin.
Dogbane

4
@dogbane: Cela supprime les doublons pour moi. Cependant, il a un problème subtil. La sortie a un: à la fin qui, si défini comme votre $ PATH, signifie que le répertoire actuel est ajouté au chemin. Cela a des implications de sécurité sur un ordinateur multi-utilisateur.
camh

@ dogbane, cela fonctionne et j'ai édité post pour avoir une commande d'une ligne sans le trailing:
akostadinov

@dogbane votre solution a une fin: dans la sortie
akostadinov

hmm, votre troisième commande fonctionne, mais les deux premières ne fonctionnent que si je les utilise echo -n. Vos commandes ne semblent pas fonctionner avec "ici les chaînes", par exemple, essayez:awk -v RS=: -v ORS=: '!arr[$0]++' <<< ".:/foo/bin:/bar/bin:/foo/bin"
dogbane

6

Voici une doublure AWK.

$ PATH=$(printf %s "$PATH" \
     | awk -vRS=: -vORS= '!a[$0]++ {if (NR>1) printf(":"); printf("%s", $0) }' )

où:

  • printf %s "$PATH"imprime le contenu de $PATHsans nouvelle ligne
  • RS=: modifie le caractère séparateur de l'enregistrement d'entrée (par défaut, nouvelle ligne)
  • ORS= change le délimiteur d'enregistrement de sortie en chaîne vide
  • a le nom d'un tableau créé implicitement
  • $0 référence l'enregistrement en cours
  • a[$0] est une déréférence de tableau associatif
  • ++ est l'opérateur post-incrément
  • !a[$0]++ garde le côté droit, c’est-à-dire qu’il s’assure que l’enregistrement en cours est uniquement imprimé, s’il n’a pas été imprimé auparavant
  • NR le numéro d'enregistrement actuel, en commençant par 1

Cela signifie que AWK est utilisé pour scinder le PATHcontenu le long des :caractères de délimitation et pour filtrer les entrées en double sans modifier l'ordre.

Comme les tableaux associatifs AWK sont implémentés sous forme de tables de hachage, le temps d'exécution est linéaire (c'est-à-dire dans O (n)).

Notez que nous n’avons pas besoin de rechercher les :caractères entre guillemets , car les shells ne fournissent pas de guillemets pour prendre en charge les répertoires avec :son nom dans la PATHvariable.

Awk + coller

Ce qui précède peut être simplifié avec coller:

$ PATH=$(printf %s "$PATH" | awk -vRS=: '!a[$0]++' | paste -s -d:)

La pastecommande est utilisée pour intercaler la sortie awk avec des deux points. Cela simplifie l’action awk lors de l’impression (action par défaut).

Python

La même chose que Python deux-liner:

$ PATH=$(python3 -c 'import os; from collections import OrderedDict; \
    l=os.environ["PATH"].split(":"); print(":".join(OrderedDict.fromkeys(l)))' )

ok, mais est-ce que cela supprime les doublons d'une chaîne existante délimitée par des deux points ou empêche-t-il l'ajout de doublons à une chaîne?
Alexander Mills

1
ressemble à l'ancien
Alexander Mills

2
@AlexanderMills, eh bien, l'OP vient de demander de supprimer les doublons, c'est donc ce que fait l'appel awk.
maxschlepzig

1
La pastecommande ne fonctionne pas pour moi sauf si j'ajoute une fin -pour utiliser STDIN.
Wisbucky

2
En outre, je dois ajouter des espaces après le -vsinon je reçois une erreur. -v RS=: -v ORS=. Juste différents types de awksyntaxe.
Wisbucky

4

Une discussion similaire a eu lieu à ce sujet ici .

Je prends une approche légèrement différente. Au lieu d'accepter simplement le PATH défini parmi tous les différents fichiers d'initialisation installés, je préfère utiliser getconfpour identifier le chemin système et le placer en premier, puis ajouter mon ordre de chemin préféré, puis utiliser awkpour supprimer les doublons. Cela peut ou ne peut pas vraiment accélérer l'exécution de la commande (et en théorie être plus sûr), mais cela me donne des fuzzies chaleureux.

# I am entering my preferred PATH order here because it gets set,
# appended, reset, appended again and ends up in such a jumbled order.
# The duplicates get removed, preserving my preferred order.
#
PATH=$(command -p getconf PATH):/sbin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH
# Remove duplicates
PATH="$(printf "%s" "${PATH}" | /usr/bin/awk -v RS=: -v ORS=: '!($0 in a) {a[$0]; print}')"
export PATH

[~]$ echo $PATH
/bin:/usr/bin:/sbin:/usr/sbin:/usr/local/bin:/usr/local/sbin:/usr/lib64/ccache:/usr/games:/home/me/bin

3
Ceci est très dangereux car vous ajoutez une fin :à la PATH(c.-à-d. Une entrée de chaîne vide), car alors le répertoire de travail en cours fait partie de votre PATH.
maxschlepzig

3

Tant que nous ajoutons des imprimeurs non-awk:

PATH=$(zsh -fc "typeset -TU P=$PATH p; echo \$P")

(Peut-être aussi simple que cela, PATH=$(zsh -fc 'typeset -U path; echo $PATH')mais zsh lit toujours au moins un zshenvfichier de configuration qui peut être modifié PATH.)

Il utilise deux fonctionnalités intéressantes de zsh:

  • scalaires liés à des tableaux ( typeset -T)
  • et les tableaux qui suppriment automatiquement les valeurs en double ( typeset -U).

agréable! réponse la plus courte de travail, et nativement sans les deux points à la fin.
jaap

2
PATH=`perl -e 'print join ":", grep {!$h{$_}++} split ":", $ENV{PATH}'`
export PATH

Cela utilise perl et présente plusieurs avantages:

  1. Il supprime les doublons
  2. Il garde l'ordre de tri
  3. Il conserve l'apparence la plus précoce ( /usr/bin:/sbin:/usr/binentraînera /usr/bin:/sbin)

2

Aussi sed(ici, en utilisant la sedsyntaxe GNU ) peut faire le travail:

MYPATH=$(printf '%s\n' "$MYPATH" | sed ':b;s/:\([^:]*\)\(:.*\):\1/:\1\2/;tb')

celui-ci ne fonctionne bien que si le premier chemin est .comme dans l'exemple de dogbane.

En général, vous devez ajouter une autre scommande:

MYPATH=$(printf '%s\n' "$MYPATH" | sed ':b;s/:\([^:]*\)\(:.*\):\1/:\1\2/;tb;s/^\([^:]*\)\(:.*\):\1/:\1\2/')

Cela fonctionne même sur une telle construction:

$ echo "/bin:.:/foo/bar/bin:/usr/bin:/foo/bar/bin:/foo/bar/bin:/bar/bin:/usr/bin:/bin" \
| sed ':b;s/:\([^:]*\)\(:.*\):\1/:\1\2/;tb;s/^\([^:]*\)\(:.*\):\1/\1\2/'

/bin:.:/foo/bar/bin:/usr/bin:/bar/bin

2

Comme d'autres l'ont démontré, il est possible d'utiliser awk, sed, perl, zsh ou bash sur une ligne, en fonction de votre tolérance aux longues lignes et de votre lisibilité. Voici une fonction bash

  • supprime les doublons
  • conserve l'ordre
  • autorise les espaces dans les noms de répertoire
  • vous permet de spécifier le délimiteur (par défaut, ':')
  • peut être utilisé avec d'autres variables, pas seulement PATH
  • fonctionne dans les versions <4 de bash, important si vous utilisez OS X qui, pour les problèmes de licence, n’expédie pas la version 4 de Bash

fonction bash

remove_dups() {
    local D=${2:-:} path= dir=
    while IFS= read -d$D dir; do
        [[ $path$D =~ .*$D$dir$D.* ]] || path+="$D$dir"
    done <<< "$1$D"
    printf %s "${path#$D}"
}

usage

Pour supprimer les doublons de PATH

PATH=$(remove_dups "$PATH")

1

Ceci est ma version:

path_no_dup () 
{ 
    local IFS=: p=();

    while read -r; do
        p+=("$REPLY");
    done < <(sort -u <(read -ra arr <<< "$1" && printf '%s\n' "${arr[@]}"));

    # Do whatever you like with "${p[*]}"
    echo "${p[*]}"
}

Usage: path_no_dup "$PATH"

Exemple de sortie:

rany$ v='a:a:a:b:b:b:c:c:c:a:a:a:b:c:a'; path_no_dup "$v"
a:b:c
rany$

1

Les versions récentes de bash (> = 4) également des tableaux associatifs, c’est-à-dire que vous pouvez également utiliser un bash 'one liner':

PATH=$(IFS=:; set -f; declare -A a; NR=0; for i in $PATH; do NR=$((NR+1)); \
       if [ \! ${a[$i]+_} ]; then if [ $NR -gt 1 ]; then echo -n ':'; fi; \
                                  echo -n $i; a[$i]=1; fi; done)

où:

  • IFS change le séparateur de champ d'entrée en :
  • declare -A déclare un tableau associatif
  • ${a[$i]+_}est un paramètre expansion qui signifie: _est substitué si et seulement si a[$i]est défini. Ceci est similaire à celui ${parameter:+word}qui teste également pour non-null. Ainsi, dans l'évaluation suivante du conditionnel, l'expression _(c'est-à-dire une chaîne de caractères unique) est évaluée à true (cela équivaut à -n _), tandis qu'une expression vide est évaluée à false.

+1: style de script sympa, mais pouvez-vous expliquer la syntaxe particulière: ${a[$i]+_}en modifiant votre réponse et en ajoutant une puce. Le reste est parfaitement compréhensible, mais vous m'y avez perdu. Je vous remercie.
Cbhihe

1
@ CBhihe, j'ai ajouté un point qui répond à cette expansion.
maxschlepzig

Merci beaucoup. Très intéressant. Je ne pensais pas que c'était possible avec les tableaux (non-chaînes) ...
Cbhihe

1
PATH=`awk -F: '{for (i=1;i<=NF;i++) { if ( !x[$i]++ ) printf("%s:",$i); }}' <<< "$PATH"`

Explication du code awk:

  1. Séparez l'entrée par des deux points.
  2. Ajoutez de nouvelles entrées de chemin au tableau associatif pour une recherche rapide des doublons.
  3. Imprime le tableau associatif.

En plus d'être laconique, ce one-liner est rapide: awk utilise une table de hachage pour obtenir une performance amortie de O (1).

basé sur Supprimer les entrées $ PATH en double


Poste vieux, mais pourriez - vous expliquer: if ( !x[$i]++ ). Merci.
Cbhihe

0

Utilisez awkpour diviser le chemin :, puis passez en boucle sur chaque champ et stockez-le dans un tableau. Si vous rencontrez un champ qui est déjà dans le tableau, cela signifie que vous l'avez déjà vu, alors ne l'imprimez pas.

Voici un exemple:

$ MYPATH=.:/foo/bar/bin:/usr/bin:/foo/bar/bin
$ awk -F: '{for(i=1;i<=NF;i++) if(!($i in arr)){arr[$i];printf s$i;s=":"}}' <<< "$MYPATH"
.:/foo/bar/bin:/usr/bin

(Mise à jour pour supprimer la fin :.)


0

Une solution - pas aussi élégante que celles qui modifient les variables * RS, mais peut-être raisonnablement claire:

PATH=`awk 'BEGIN {np="";split(ENVIRON["PATH"],p,":"); for(x=0;x<length(p);x++) {  pe=p[x]; if(e[pe] != "") continue; e[pe] = pe; if(np != "") np=np ":"; np=np pe}} END { print np }' /dev/null`

L'ensemble du programme fonctionne dans les blocs BEGIN et END . Il extrait votre variable PATH de l’environnement et le divise en unités. Il itère ensuite sur le tableau résultant p (qui est créé dans l'ordre split()). Le tableau e est un tableau associatif utilisé pour déterminer si nous avons déjà vu ou non l'élément de chemin actuel (par exemple, / usr / local / bin ), et si ce n'est pas le cas, est ajouté à np , avec la logique pour ajouter un point-virgule à np s'il y a déjà du texte dans np . Le bloc END renvoie simplement np . Cela pourrait être encore simplifié en ajoutant le-F:flag, en éliminant le troisième argument de split()(comme il est par défaut FS ), et en passant np = np ":"à np = np FS, nous donnant:

awk -F: 'BEGIN {np="";split(ENVIRON["PATH"],p); for(x=0;x<length(p);x++) {  pe=p[x]; if(e[pe] != "") continue; e[pe] = pe; if(np != "") np=np FS; np=np pe}} END { print np }' /dev/null

Naïvement, je pensais que for(element in array)cela préserverait l'ordre, mais ce n'est pas le cas, donc ma solution initiale ne fonctionne pas, car les gens s'énerveraient si quelqu'un changeait soudainement l'ordre de leur $PATH:

awk 'BEGIN {np="";split(ENVIRON["PATH"],p,":"); for(x in p) { pe=p[x]; if(e[pe] != "") continue; e[pe] = pe; if(np != "") np=np ":"; np=np pe}} END { print np }' /dev/null

0
export PATH=$(echo -n "$PATH" | awk -v RS=':' '(!a[$0]++){if(b++)printf(RS);printf($0)}')

Seule la première occurrence est conservée et l'ordre relatif est bien maintenu.


-1

Je le ferais simplement avec des outils de base tels que tr, sort et uniq:

NEW_PATH=`echo $PATH | tr ':' '\n' | sort | uniq | tr '\n' ':'`

S'il n'y a rien de spécial ou d'étrange sur votre chemin, ça devrait marcher


Au fait, vous pouvez utiliser sort -uau lieu de sort | uniq.
Rush

11
Étant donné que l'ordre des éléments PATH est significatif, cela n'est pas très utile.
maxschlepzig
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.