web-dev-qa-db-ja.com

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

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

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

52
Johnny Williem

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を簡単に呼び出すことができます。

27

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

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に「:」を含むディレクトリがないことを確認してください。そうしないと、めちゃくちゃになります。

いくつかの信用:

14
akostadinov

こちらが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 Two-linerと同じ:

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

これについても同様の議論が行われています 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
4
George M

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

他の人がawk、sed、Perl、zsh、またはbashを使用して1行で可能であることを示したように、長い行と読みやすさの許容度によって異なります。ここにbash関数があります

  • 重複を削除します
  • 順序を維持する
  • ディレクトリ名にスペースを使用できます
  • 区切り文字を指定できます(デフォルトは ':'です)。
  • pATHだけでなく、他の変数でも使用できます
  • ライセンスの問題のためにbashバージョン4が同梱されていないOS Xを使用する場合に重要な、bashバージョン4未満で動作します。

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

また、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
rush
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
vol7ron
PATH=`awk -F: '{for (i=1;i<=NF;i++) { if ( !x[$i]++ ) printf("%s:",$i); }}' <<< "$PATH"`

Awkコードの説明:

  1. 入力はコロンで区切ります。
  2. 新しいパスエントリを連想配列に追加して、重複したルックアップを高速化します。
  3. 連想配列を出力します。

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

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

1
Leftium

連想配列の最近の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
maxschlepzig

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

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

解決策-* 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変数を環境からプルし、ユニットに分割します。次に、結果の配列psplit()によって順番に作成されます)を反復処理します。配列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
_
0
Andrew Beals

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

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

0
Cyker