PATH環境変数からディレクトリの重複コピーを削除できるようにするbashシェル関数を作成しようとしています。
コマンドを使用して1行のコマンドでこれを実現することは可能であると言われましたがawk
、その方法はわかりません。誰もが方法を知っていますか?
PATH環境変数からディレクトリの重複コピーを削除できるようにするbashシェル関数を作成しようとしています。
コマンドを使用して1行のコマンドでこれを実現することは可能であると言われましたがawk
、その方法はわかりません。誰もが方法を知っていますか?
回答:
に重複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
PATH=$PATH:x=b
反復が順番に、その新しい値は無視されますが、時に逆の順序で、新しいされたときに、元のパスのかもしれない中、Xは、このように、値aを持っています値が有効になります。
PATH=x:$PATH
。
PATH=$PATH:...
ありませんPATH=...:$PATH
。したがって、逆の順序を繰り返す方が適切です。あなたの方法も機能しますが、人々は逆の方法で追加します。
正しいことをすべて行うわかりやすいワンライナーソリューションを次に示します。重複を削除し、パスの順序を保持し、最後にコロンを追加しません。したがって、元とまったく同じ動作をする重複排除されたPATHを提供する必要があります。
PATH="$(perl -e 'print join(":", grep { not $seen{$_}++ } split(/:/, $ENV{PATH}))')"
コロン(split(/:/, $ENV{PATH})
)で単純に分割し、use を使用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の両方を重複排除し、dedup_pathvar
コロンで区切られたパスのリスト(PYTHONPATHなど)を保持する他の変数を簡単に呼び出すことができます。
chomp
、末尾の改行を削除するにはa を追加する必要がありました。これは私のために働い:perl -ne 'chomp; print join(":", grep { !$seen{$_}++ } split(/:/))' <<<"$PATH"
ここに洗練されたものがあります:
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に ":"を含むディレクトリが含まれていないことを確認してください。そうしないと、混乱してしまいます。
いくつかのクレジット:
echo -n
。コマンドは「here文字列」では機能しないようです。例:awk -v RS=: -v ORS=: '!arr[$0]++' <<< ".:/foo/bin:/bar/bin:/foo/bin"
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の2ライナーと同じ:
$ PATH=$(python3 -c 'import os; from collections import OrderedDict; \
l=os.environ["PATH"].split(":"); print(":".join(OrderedDict.fromkeys(l)))' )
paste
私は、末尾に追加しない限り、コマンドは私のために動作しません-
STDINを使用するために。
-v
エラーの後にスペースを追加する必要があります。-v RS=: -v ORS=
。awk
構文の異なるフレーバー。
私は少し異なるアプローチを取ります。インストールされるすべての異なる初期化ファイルから設定された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
:
にPATH
空の文字列エントリ(つまり、空の文字列エントリ)を追加するため、非常に危険ですPATH
。
非awk onelinersを追加している限り:
PATH=$(zsh -fc "typeset -TU P=$PATH p; echo \$P")
(と同じくらい簡単かもしれませんPATH=$(zsh -fc 'typeset -U path; echo $PATH')
が、zshは常にzshenv
変更可能な少なくとも1つの構成ファイルを読み取りますPATH
。)
2つの優れたzsh機能を使用します。
typeset -T
)typeset -U
)。また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
他の人が示したように、awk、sed、perl、zsh、またはbashを使用して1行で実行できることは、長い行に対する許容度と読みやすさに依存します。これが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")
これは私のバージョンです:
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$
連想配列の最近の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に評価されます。${a[$i]+_}
。答えを編集して1つの箇条書きを追加します。残りは完全に理解できますが、あなたはそこで私を失いました。ありがとうございました。
PATH=`awk -F: '{for (i=1;i<=NF;i++) { if ( !x[$i]++ ) printf("%s:",$i); }}' <<< "$PATH"`
awkコードの説明:
簡潔であることに加えて、このワンライナーは高速です。awkは連鎖ハッシュテーブルを使用して、償却されたO(1)パフォーマンスを実現します。
if ( !x[$i]++ )
。ありがとう。
を使用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
(末尾のを削除するために更新されました:
。)
解決策-* 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に追加される前に表示されたかどうかを判断するために使用される連想配列です。NPは、すでにテキストがある場合は、NP。ENDのブロック単にエコーNP。これは、-F:
フラグ、への3番目の引数split()
(デフォルトは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
私は、tr、sort、uniqなどの基本的なツールを使用してそれを行います。
NEW_PATH=`echo $PATH | tr ':' '\n' | sort | uniq | tr '\n' ':'`
パスに特別なものや奇妙なものが何もない場合は、動作するはずです
sort -u
代わりに使用できますsort | uniq
。