私は、PATH環境変数からディレクトリの重複コピーを削除できるようにするbashシェル関数を作成しようとしています。
awk
コマンドを使用して1行のコマンドでこれを実現することは可能だと言われましたが、その方法がわかりません。誰か知ってる?
PATH
に重複がなく、ディレクトリがまだない場合にのみディレクトリを追加する場合は、シェルのみで簡単に行うことができます。
for x in /path/to/add …; do
case ":$PATH:" in
*":$x:"*) :;; # already there
*) PATH="$x:$PATH";;
esac
done
次に、$PATH
から重複を削除するシェルスニペットを示します。エントリを1つずつ確認し、まだ表示されていないエントリをコピーします。
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
これがintelligibleワンライナーソリューションで、すべての正しいことを実行します。重複を削除し、パスの順序を維持し、最後にコロンを追加しません。したがって、元のパスとまったく同じ動作をする重複排除されたPATHが得られます。
PATH="$(Perl -e 'print join(":", grep { not $seen{$_}++ } split(/:/, $ENV{PATH}))')"
これは単にコロン(split(/:/, $ENV{PATH})
)で分割し、grep { not $seen{$_}++ }
を使用して、最初に出現したものを除いて繰り返されるパスのインスタンスをすべて除外し、残りのインスタンスをコロンで区切って結合し、結果を出力します(print join(":", ...)
)。
他の変数を重複排除する機能だけでなく、その周りにさらに構造が必要な場合は、次のスニペットを試してください。このスニペットは、現在自分の構成で使用しています。
# 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
このコードはPATHとMANPATHの両方を重複排除し、コロンで区切られたパスのリストを保持する他の変数(PYTHONPATHなど)でdedup_pathvar
を簡単に呼び出すことができます。
ここに洗練されたものがあります:
printf %s "$PATH" | awk -v RS=: -v ORS=: '!arr[$0]++'
より長い(それがどのように機能するかを見るために):
printf %s "$PATH" | awk -v RS=: -v ORS=: '{ if (!arr[$0]++) { print $0 } }'
さて、あなたはLinuxに慣れていないので、末尾に ":"を付けずに実際にPATHを設定する方法を次に示します。
PATH=`printf %s "$PATH" | awk -v RS=: '{ if (!arr[$0]++) {printf("%s%s",!ln++?"":":",$0)}}'`
ところで、PATHに「:」を含むディレクトリがないことを確認してください。そうしないと、めちゃくちゃになります。
いくつかの信用:
こちらがAWKのワンライナーです。
$ PATH=$(printf %s "$PATH" \
| awk -vRS=: -vORS= '!a[$0]++ {if (NR>1) printf(":"); printf("%s", $0) }' )
どこ:
printf %s "$PATH"
は、末尾の改行なしで$PATH
の内容を印刷しますRS=:
入力レコードの区切り文字を変更します(デフォルトは改行です)ORS=
は、出力レコード区切り文字を空の文字列に変更しますa
暗黙的に作成された配列の名前$0
は現在のレコードを参照しますa[$0]
は、連想配列の逆参照です++
はポストインクリメント演算子です!a[$0]++
は右側をガードします。つまり、現在のレコードが印刷されていない場合は、現在のレコードのみが印刷されるようにします。NR
1から始まる現在のレコード番号つまり、AWKを使用してPATH
のコンテンツを:
区切り文字に沿って分割し、順序を変更せずに重複するエントリを除外します。
AWK連想配列はハッシュテーブルとして実装されるため、ランタイムは線形です(つまり、O(n)で)。
シェル は:
でディレクトリをサポートするための引用符 を提供しないため、引用符で囲まれた:
文字を探す必要がないことに注意してください。 PATH
変数の名前で。
上記はペーストで簡略化できます:
$ PATH=$(printf %s "$PATH" | awk -vRS=: '!a[$0]++' | paste -s -d:)
paste
コマンドを使用して、awk出力にコロンを挿入します。これにより、awkアクションが印刷に簡略化されます(これはデフォルトのアクションです)。
Python Two-linerと同じ:
$ PATH=$(python3 -c 'import os; from collections import OrderedDict; \
l=os.environ["PATH"].split(":"); print(":".join(OrderedDict.fromkeys(l)))' )
これについても同様の議論が行われています here 。
私は少し異なるアプローチを取ります。インストールされるすべての異なる初期化ファイルから設定されたPATHを受け入れるのではなく、getconf
を使用してシステムパスを特定し、最初に配置してから、優先パスの順序を追加し、次にawk
は、重複を削除します。これはコマンドの実行を実際に高速化する場合とそうでない場合があります(そして理論的にはより安全です)が、それは私に暖かいファジーを与えます。
# 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
非awkのワンライナーを追加する限り:
_PATH=$(zsh -fc "typeset -TU P=$PATH p; echo \$P")
_
(PATH=$(zsh -fc 'typeset -U path; echo $PATH')
のように単純にすることもできますが、zshは常に少なくとも1つのzshenv
構成ファイルを読み取り、PATH
を変更できます。)
2つのnice zsh機能を使用します。
typeset -T
_)typeset -U
_)。他の人がawk、sed、Perl、zsh、またはbashを使用して1行で可能であることを示したように、長い行と読みやすさの許容度によって異なります。ここにbash関数があります
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}"
}
使用量
PATHから重複を削除するには
PATH=$(remove_dups "$PATH")
また、sed
(ここではGNU sed
構文を使用)がジョブを実行できます。
MYPATH=$(printf '%s\n' "$MYPATH" | sed ':b;s/:\([^:]*\)\(:.*\):\1/:\1\2/;tb')
これは、dogbaneの例のように、最初のパスが.
の場合にのみ機能します。
一般的に、別のs
コマンドを追加する必要があります。
MYPATH=$(printf '%s\n' "$MYPATH" | sed ':b;s/:\([^:]*\)\(:.*\):\1/:\1\2/;tb;s/^\([^:]*\)\(:.*\):\1/:\1\2/')
それはそのような構造でも機能します:
$ 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
PATH=`Perl -e 'print join ":", grep {!$h{$_}++} split ":", $ENV{PATH}'`
export PATH
これはPerlを使用し、いくつかの利点があります。
/usr/bin:/sbin:/usr/bin
は/usr/bin:/sbin
になります)PATH=`awk -F: '{for (i=1;i<=NF;i++) { if ( !x[$i]++ ) printf("%s:",$i); }}' <<< "$PATH"`
Awkコードの説明:
簡潔であることに加えて、このワンライナーは高速です。awkは、チェーン化されたハッシュテーブルを使用して、償却済みのO(1)パフォーマンスを実現します。
重複する$ PATHエントリの削除 に基づく
連想配列の最近のbashバージョン(> = 4)。つまり、bashの「1つのライナー」を使用することもできます。
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)
どこ:
IFS
は、入力フィールド区切り文字を:
に変更しますdeclare -A
は連想配列を宣言します${a[$i]+_}
はパラメータ展開の意味です。_
は、a[$i]
が設定されている場合にのみ置き換えられます。これは${parameter:+Word}
と同様で、nullでないかどうかもテストします。したがって、次の条件の評価では、式_
(つまり、単一の文字列)はtrueに評価されます(これは-n _
と同等です)-空の式はfalseに評価されます。これは私のバージョンです:
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[*]}"
}
使用法:path_no_dup "$PATH"
サンプル出力:
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$
解決策-* RS変数を変更するものほどエレガントではありませんが、おそらくかなり明確です。
_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`
_
プログラム全体は[〜#〜] begin [〜#〜]および[〜#〜] end [〜#〜]ブロックで機能します。 PATH変数を環境からプルし、ユニットに分割します。次に、結果の配列p(split()
によって順番に作成されます)を反復処理します。配列eは、現在のパス要素(たとえば、/ usr/local/bin)の前とそうでない場合は、npに追加され、コロンをnpnpに既にテキストがある場合[〜#〜] end [〜#〜]ブロックは単にnpをエコーします。これは、_-F:
_フラグを追加し、split()
への3番目の引数を省略してさらに簡略化できます(デフォルトでは[〜#〜] fs [〜# 〜])、および_np = np ":"
_を_np = np FS
_に変更すると、次のようになります。
_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
_
素朴に、私はfor(element in array)
が順序を保持すると信じていましたが、それはそうではないので、誰かが_$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
_
awk
を使用して:
のパスを分割し、各フィールドをループして配列に格納します。すでに配列内にあるフィールドに遭遇した場合、それは以前に見たことがあるということなので、印刷しないでください。
次に例を示します。
$ 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
(末尾の:
を削除するように更新されました。)
export PATH=$(echo -n "$PATH" | awk -v RS=':' '(!a[$0]++){if(b++)printf(RS);printf($0)}')
最初の発生のみが保持され、相対的な順序は適切に維持されます。