web-dev-qa-db-fra.com

Supprimer des entrées en double $ sur path avec la commande AWK

J'essaie d'écrire une fonction Bash Shell qui me permettra de supprimer des copies en double des répertoires de ma variable d'environnement de chemin.

On m'a dit qu'il était possible d'y parvenir avec une commande d'une ligne à l'aide de la commande awk, mais je ne peux pas comprendre comment le faire. Quelqu'un sache comment?

52
Johnny Williem

Si vous n'avez pas déjà de duplicates dans le PATH et que vous souhaitez ajouter des annuaires s'ils ne sont pas déjà là, vous pouvez le faire facilement avec la coque seule.

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

Et voici un extrait d'obus qui élimine les doublons de $PATH. Il passe par les entrées un par un et copie ceux qui n'ont pas encore été vus.

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

Voici une solution intelligible one-liner qui fait toutes les bonnes choses: supprime les duplicats, préserve la commande des chemins et n'ajoute pas de côlon à la fin. Il devrait donc vous donner un chemin 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 sur le côlon (split(/:/, $ENV{PATH})), utilise des utilisations grep { not $seen{$_}++ } Pour filtrer toutes les instances répétées de chemins, à l'exception de la première occurrence, puis relie les restantes de retour séparées par des colons et des impressions le résultat (print join(":", ...)).

Si vous voulez également une structure plus de structure, ainsi que la capacité de dédupliquer d'autres variables également, 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éduira à la fois chemin et manpathique, et vous pouvez facilement appeler dedup_pathvar Sur d'autres variables qui contiennent des listes de chemins séparés par le côlon (par exemple pythonpath).

27
Ryan C. Thompson

Voici un élégant:

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

Plus longtemps (pour voir comment ça fonctionne):

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

OK, puisque vous êtes nouveau à Linux, voici comment définir réellement le chemin sans fin ":"

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

bTW Assurez-vous de ne pas avoir de répertoires contenant ":" dans votre chemin, sinon il va être foiré.

quelque crédit à:

14
akostadinov

Voici une doublure AWK One.

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

où:

  • printf %s "$PATH" imprime le contenu de $PATH sans une nouvelle ligne de fin
  • RS=: Modifie le caractère de délimitation d'enregistrement d'entrée (par défaut est transversal)
  • ORS= Modifie le délimiteur d'enregistrement de sortie à la chaîne vide
  • a le nom d'un tableau créé implicitement
  • $0 Références L'enregistrement actuel
  • a[$0] est une déréférence de tableau associative
  • ++ est l'opérateur post-incrément
  • !a[$0]++ garde le côté droit, c'est-à-dire que l'enregistrement actuel n'est imprimé que s'il n'a pas été imprimé avant
  • NR le numéro d'enregistrement actuel, en commençant par 1

Cela signifie que AWK est utilisé pour scinder le contenu PATH le long de la : Personnages de délimitation et filtrer les entrées en double sans modifier la commande.

Étant donné que les matrices associatives AWK sont implémentées comme des tables de hausse, l'exécution est linéaire (c'est-à-dire O (n)).

Notez que nous n'avons pas besoin de chercher cité : caractères car des coquilles ne fournissez pas citant pour supporter les répertoires avec : Dans son nom dans la variable PATH.

Awk + pâte

Ce qui précède peut être simplifié avec la pâte:

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

La commande paste est utilisée pour intersperser la sortie AWK avec des points. Cela simplifie l'action AWK à l'impression (qui est l'action par défaut).

Python

Le même que Python Deux-doublure:

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

Il y a eu une discussion similaire à ce sujet ici .

Je prends un peu d'approche différente. Au lieu d'accepter simplement le chemin qui est défini à partir de tous les différents fichiers d'initialisation installés, je préfère utiliser getconf _ pour identifier le chemin du système et le placer d'abord, puis ajouter mon ordre de chemin préféré, puis utiliser awk pour éliminer les doublons. Cela peut ou non accélérer réellement l'exécution de la commande (et en théorie être plus sécurisé), mais cela me donne des fuzzés chauds.

# 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
4
George M

Tant que nous ajoutons des inélinateurs non awks:

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

(Pourrait être aussi simple que PATH=$(zsh -fc 'typeset -U path; echo $PATH') _ mais zsh lit toujours au moins un fichier de configuration zshenv, qui peut modifier PATH.)

Il utilise deux belles caractéristiques ZSH:

  • scalaires attachés à des tableaux (typeset -T)
  • et des tableaux qui permettent des valeurs en double (typeset -U).
4
Michał Politowski

Comme d'autres l'ont démontrée, il est possible d'une ligne d'une ligne avec AWK, SED, Perl, ZSH ou Bash, dépend de votre tolérance aux longues lignes et à la lisibilité. Voici une fonction Bash qui

  • supprime les doublons
  • préserve la commande
  • permet aux espaces des noms de répertoires
  • vous permet de spécifier le délimiteur (par défaut à ':')
  • peut être utilisé avec d'autres variables, pas seulement un chemin
  • fonctionne dans les versions Bash <4, IMPORTANT Si vous utilisez OS X, qui pour des problèmes de licence ne sont pas expédiés Bash version 4

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

tilisation

Pour supprimer des DUPS du chemin

PATH=$(remove_dups "$PATH")
2
amdn
PATH=`Perl -e 'print join ":", grep {!$h{$_}++} split ":", $ENV{PATH}'`
export PATH

Cela utilise Perl et a plusieurs avantages:

  1. Il supprime les doublons
  2. Il garde l'ordre de tri
  3. Il garde la première apparition (/usr/bin:/sbin:/usr/bin aura pour résultat /usr/bin:/sbin)
2
vol7ron
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 points.
  2. Ajoutez de nouvelles entrées de chemin à une matrice associative pour une recherche rapide en double.
  3. Imprime le tableau associatif.

En plus d'être TERSE, cette doublure est rapide: AWK utilise une table de hachage enchaînée pour obtenir amortize O(1) Performance.

basé sur Suppression des entrées en double $ path

1
Leftium

Versions récentes Bash (> = 4) En outre, des tableaux associatifs, c'est-à-dire, vous pouvez également utiliser une bash 'One Dower' pour cela:

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 Modifie le séparateur de champ de saisie sur :
  • declare -A déclare un tableau associatif
  • ${a[$i]+_} est un sens d'expansion des paramètres: _ est substitué si et seulement si a[$i] est défini. Ceci est similaire à ${parameter:+Word} qui teste également non-null. Ainsi, dans l'évaluation suivante du conditionnel, l'expression _ (c'est-à-dire une chaîne de caractères unique) évalue en vrai (cela équivaut à -n _) - tandis qu'une expression vide s'évalue à FALSE.
1
maxschlepzig

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[*]}"
}

tilisation :path_no_dup "$PATH"

Sortie d'échantillon :

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
Rany Albeg Wein

Une solution - pas une qui est aussi élégante que celles qui changent les * variables rs, mais aussi raisonnablement clairement:

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 le [~ # ~ ~] commence [~ # ~] et [~ # ~] fin [~ # ~ ~] blocs. Il tire la variable de votre chemin de l'environnement, le divisant en unités. Il itière ensuite sur le tableau résultant P (qui est créé dans l'ordre par split()). Le tableau E est un tableau associatif utilisé pour déterminer si nous avons vu ou non l'élément de chemin de courant (par exemple / usr/local/bin) avant , et sinon, est annexé à NP , avec la logique pour ajouter un côlon à NP s'il y a déjà du texte dans NP . Le [~ # ~] fin [~ # ~ ~] Block Simply Echos NP . Cela pourrait être encore simplifié en ajoutant le drapeau -F:, éliminant le troisième argument à split() (comme il est par défaut à [~ # ~ # ~ # ~] (+++)) et changeant 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, j'ai cru que for(element in array) conserve la commande, mais que ma solution originale ne fonctionne pas, car les gens se fâchent si une personne brouillait 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
Andrew Beals

Utilisez awk pour diviser le chemin sur :, puis boucle sur chaque champ et stockez-la dans un tableau. Si vous rencontrez un champ déjà dans la matrice, cela signifie que vous l'avez déjà vu avant, 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

(Mis à jour pour supprimer le sentier :.)

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

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

0
Cyker