awkコマンドで重複した$ PATHエントリを削除する


48

PATH環境変数からディレクトリの重複コピーを削除できるようにするbashシェル関数を作成しようとしています。

コマンドを使用して1行のコマンドでこれを実現することは可能であると言われましたがawk、その方法はわかりません。誰もが方法を知っていますか?



回答:


37

に重複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内のアイテムを逆に反復すると、後のアイテムが通常新しく追加され、最新の値を持つ可能性があるため、より良いでしょう。
エリックワン

2
@EricWang私はあなたの推論を理解していません。PATH要素は前から後ろに移動するため、重複がある場合、2番目の重複は事実上無視されます。後ろから前へ繰り返すと順序が変わります。
ジル 'SO-悪であるのをやめる'

@GillesあなたはPATH変数を重複している場合は、おそらくそれは、このように追加されます:PATH=$PATH:x=b反復が順番に、その新しい値は無視されますが、時に逆の順序で、新しいされたときに、元のパスのかもしれない中、Xは、このように、値aを持っています値が有効になります。
エリックワン

4
@EricWangその場合、追加された値は効果がないため、無視する必要があります。後戻りすることで、付加価値を前に出すことができます。追加された値が前に行くことになっていた場合、それはとして追加されていたでしょうPATH=x:$PATH
ジル 'SO-悪であるのをやめる'

@Gilles何かを追加するとき、それはまだそこにないか、古い値を上書きしたいので、新しく追加した変数を表示する必要があります。そして、慣例により、通常はそれがこのように追加します:PATH=$PATH:...ありませんPATH=...:$PATH。したがって、逆の順序を繰り返す方が適切です。あなたの方法も機能しますが、人々は逆の方法で追加します。
エリックワン

23

正しいことをすべて行うわかりやすいワンライナーソリューションを次に示します。重複を削除し、パスの順序を保持し、最後にコロンを追加しません。したがって、元とまったく同じ動作をする重複排除された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"
ホーコンHægland

12

ここに洗練されたものがあります:

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に ":"を含むディレクトリが含まれていないことを確認してください。そうしないと、混乱してしまいます。

いくつかのクレジット:


-1これは機能しません。まだパスに重複があります。
ドッグベイン

4
@dogbane:重複を削除します。ただし、微妙な問題があります。出力の末尾には:があり、$ PATHとして設定されている場合、現在のディレクトリにパスが追加されます。これは、マルチユーザーマシンでセキュリティに影響します。
-camh

@dogbane、それは動作し、末尾を付けずに1行のコマンドを持つように投稿を編集しました:
-akostadinov

@dogbaneあなたのソリューションは末尾にあります:出力
-akostadinov

うーん、3番目のコマンドは機能しますが、最初の2つはを使用しない限り機能しませんecho -n。コマンドは「here文字列」では機能しないようです。例:awk -v RS=: -v ORS=: '!arr[$0]++' <<< ".:/foo/bin:/bar/bin:/foo/bin"
-dogbane

6

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変数に名前が含まれるディレクトリをサポートするために引用符を提供しないため、引用符で囲まれた文字を探す必要がないことに注意してください。

Awk +貼り付け

上記は貼り付けで簡単にできます:

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

このpasteコマンドは、awk出力にコロンを散在させるために使用されます。これにより、awkアクションが印刷(デフォルトのアクション)に簡単になります。

Python

Pythonの2ライナーと同じ:

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

わかりましたが、これはコロンで区切られた既存の文字列から重複を削除しますか、それとも文字列に重複が追加されるのを防ぎますか?
アレクサンダーミルズ

1
前者のように見える
アレクサンダーミルズ

2
@AlexanderMills、まあ、OPはちょうど重複を削除するように尋ねたので、これはawkコールが行うことです。
maxschlepzig

1
paste私は、末尾に追加しない限り、コマンドは私のために動作しません-STDINを使用するために。
ウィスバッキー

2
また、-vエラーの後にスペースを追加する必要があります。-v RS=: -v ORS=awk構文の異なるフレーバー。
ウィスバッキー

4

ここでこれについて同様の議論がありました

私は少し異なるアプローチを取ります。インストールされるすべての異なる初期化ファイルから設定された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

3
これは、現在の作業ディレクトリがの一部であるため、末尾:PATH空の文字列エントリ(つまり、空の文字列エントリ)を追加するため、非常に危険ですPATH
maxschlepzig 14

3

非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)。

いいね!最短の作業回答、およびネイティブで最後にコロンなし。
jaap 16

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

これにはperlが使用され、いくつかの利点があります。

  1. 重複を削除します
  2. ソート順を保持します
  3. 最古の外観を保持します(に/usr/bin:/sbin:/usr/binなります/usr/bin:/sbin

2

また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

2

他の人が示したように、awk、sed、perl、zsh、またはbashを使用して1行で実行できることは、長い行に対する許容度と読みやすさに依存します。これがbash関数です

  • 重複を削除します
  • 秩序を保つ
  • ディレクトリ名にスペースを許可します
  • 区切り文字を指定できます(デフォルトは「:」です)
  • PATHだけでなく、他の変数とともに使用できます
  • bashバージョン4未満で動作します。OSXを使用する場合は重要です。ライセンス問題のためにbashバージョン4は出荷されません。

バッシュ関数

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

1

これは私のバージョンです:

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$

1

連想配列の最近の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に評価されます。

+1:すてきなスクリプトスタイルですが、特定の構文を説明できます${a[$i]+_}。答えを編集して1つの箇条書きを追加します。残りは完全に理解できますが、あなたはそこで私を失いました。ありがとうございました。
Cbhihe

1
@Cbhihe、この拡張に対処する箇条書きを追加しました。
maxschlepzig

どうもありがとうございました。とても興味深い。配列(非文字列)でそれが可能だとは思わなかった
...-Cbhihe

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

awkコードの説明:

  1. 入力をコロンで区切ります。
  2. 高速な重複検索のために、新しいパスエントリを連想配列に追加します。
  3. 連想配列を出力します。

簡潔であることに加えて、このワンライナーは高速です。awkは連鎖ハッシュテーブルを使用して、償却されたO(1)パフォーマンスを実現します。

重複する$ PATHエントリの削除に基づく


古い投稿ですが、説明してもらえますかif ( !x[$i]++ )。ありがとう。
Cbhihe

0

を使用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

(末尾のを削除するために更新されました:。)


0

解決策-* 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は、すでにテキストがある場合は、NPENDのブロック単にエコー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

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

最初の出現のみが保持され、相対的な順序は適切に維持されます。


-1

私は、tr、sort、uniqなどの基本的なツールを使用してそれを行います。

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

パスに特別なものや奇妙なものが何もない場合は、動作するはずです


ところで、のsort -u代わりに使用できますsort | uniq
ラッシュ

11
PATH要素の順序は重要なので、これはあまり役に立ちません。
maxschlepzig 14
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.