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?
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).
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 à:
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 finRS=:
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 videa
le nom d'un tableau créé implicitement$0
Références L'enregistrement actuela[$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é avantNR
le numéro d'enregistrement actuel, en commençant par 1Cela 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
.
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).
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)))' )
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
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:
typeset -T
)typeset -U
).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
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")
PATH=`Perl -e 'print join ":", grep {!$h{$_}++} split ":", $ENV{PATH}'`
export PATH
Cela utilise Perl et a plusieurs avantages:
/usr/bin:/sbin:/usr/bin
aura pour résultat /usr/bin:/sbin
)PATH=`awk -F: '{for (i=1;i<=NF;i++) { if ( !x[$i]++ ) printf("%s:",$i); }}' <<< "$PATH"`
Explication du code AWK:
En plus d'être TERSE, cette doublure est rapide: AWK utilise une table de hachage enchaînée pour obtenir amortize O(1) Performance.
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.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$
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
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 :
.)
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.