awk '!a[$0]++'はどのように機能しますか?

awk '!a[$0]++'はどのように機能しますか?

このワンライナーは、事前ソートせずにテキスト入力から重複行を削除します。

例えば:

$ cat >f
q
w
e
w
r
$ awk '!a[$0]++' <f
q
w
e
r
$ 

インターネットで見つけた元のコードは次のとおりです。

awk '!_[$0]++'

これは私にとってさらに困惑させられるものでした。Perl_のように awk では特別な意味があると思っていましたが、実際には単なる配列の名前であることが判明しました。

今、私はこのワンライナーの背後にある論理を理解しました。 各入力行はハッシュ配列のキーとして使用されるので、完了すると、ハッシュには到着順に一意の行が含まれます。

私が知りたいのは、この表記が awk によってどのように解釈されるかということです。たとえば、感嘆符 ( !) の意味や、このコード スニペットの他の要素などです。

どのように機能しますか?

答え1

これは「直感的な」答えです。awkのメカニズムのより詳細な説明については、@Cuonglmの

この場合!a[$0]++、後置増分は++一時的に無視できますが、式の値は変わりません。したがって、 のみに注目してください!a[$0]。ここでは:

a[$0]

現在の行$0を配列のキーとして使用しa、そこに格納されている値を取得します。この特定のキーが以前に参照されていない場合は、a[$0]空の文字列として評価されます。

!a[$0]

!、前の値を否定します。空またはゼロ (false) だった場合、結果は true になります。ゼロ以外 (true) だった場合、結果は false になります。式全体が true と評価された場合、つまりa[$0]最初から設定されていなかった場合、行全体がデフォルトのアクションとして出力されます。

また、古い値に関係なく、後置インクリメント演算子は に 1 を追加するa[$0]ため、次に配列内の同じ値にアクセスすると、その値は正になり、条件全体が失敗します。

答え2

処理は次のとおりです。

  • a[$0]$0:連想配列内のキーの値を確認しますa。存在しない場合は、空の文字列で自動的に作成します。

  • a[$0]++: の値を増分しa[$0]、式の値として古い値を返します。++演算子は数値を返すため、 がa[$0]最初から空だった場合は0が返され、a[$0]に増分されます1

  • !a[$0]++: 式の値を否定します。a[$0]++返された場合0(偽値)、式全体が true と評価され、awkデフォルトのアクションが実行されますprint $0。それ以外の場合、式全体が false と評価された場合、それ以上のアクションは実行されません。

参考文献:

gawkを使うと、dgawk (またはawk --debug新しいバージョン)スクリプトをデバッグするにはgawk、まず、gawk次の名前のスクリプトを作成しますtest.awk

BEGIN {                                                                         
    a = 0;                                                                      
    !a++;                                                                       
}

次に以下を実行します:

dgawk -f test.awk

または:

gawk --debug -f test.awk

デバッガーコンソールで:

$ dgawk -f test.awk
dgawk> trace on
dgawk> watch a
Watchpoint 1: a
dgawk> run
Starting program: 
[     1:0x7fe59154cfe0] Op_rule             : [in_rule = BEGIN] [source_file = test.awk]
[     2:0x7fe59154bf80] Op_push_i           : 0 [PERM|NUMCUR|NUMBER]
[     2:0x7fe59154bf20] Op_store_var        : a [do_reference = FALSE]
[     3:0x7fe59154bf60] Op_push_lhs         : a [do_reference = TRUE]
Stopping in BEGIN ...
Watchpoint 1: a
  Old value: untyped variable
  New value: 0
main() at `test.awk':3
3           !a++;
dgawk> step
[     3:0x7fe59154bfc0] Op_postincrement    : 
[     3:0x7fe59154bf40] Op_not              : 
Watchpoint 1: a
  Old value: 0
  New value: 1
main() at `test.awk':3
3           !a++;
dgawk>

ご覧のとおり、Op_postincrementは前に実行されましたOp_not

より明確に表示するには、または の代わりにsiまたは を使用することもできます。stepisstep

dgawk> si
[     3:0x7ff061ac1fc0] Op_postincrement    : 
3           !a++;
dgawk> si
[     3:0x7ff061ac1f40] Op_not              : 
Watchpoint 1: a
  Old value: 0
  New value: 1
main() at `test.awk':3
3           !a++;

答え3

ああ、どこにでもあるが不吉なawkの重複除去ツール

awk '!a[$0]++'

このかわいい子は、awk のパワーと簡潔さの愛の子です。awk ワンライナーの最高峰です。短いですが、強力で難解です。順序を維持しながら重複を削除します。隣接する重複のみを削除するuniqsort -u、重複を削除するために順序を壊す必要がある では達成できない偉業です。

ここでは、awk ワンライナーがどのように動作するかを説明しようと試みました。awk をまったく知らない人でも理解できるように説明に力を入れました。うまくできたと思います。

まず背景を説明します。awk はプログラミング言語です。このコマンドは、awk '!a[$0]++'awk コードに対して awk インタープリター/コンパイラーを呼び出します!a[$0]++python -c 'print("foo")'またはに似ていますnode -e 'console.log("foo")'。awk はテキスト フィルタリング用に簡潔になるように特別に設計されているため、awk コードは 1 行であることが多いです。

ここで擬似コードをいくつか示します。このワンライナーは基本的に次の処理を実行します。

for every line of input
  if i have not seen this line before then
    print line
  take note that i have now seen this line

これによって順序を維持しながら重複が削除される様子がおわかりいただけると思います。

しかし、ループ、if、print、および文字列の保存と取得のメカニズムが 8 文字の awk コードにどのように収まるのでしょうか。答えは暗黙的です。

ループ、if、print は暗黙的に行われます。

説明するために、もう一度疑似コードを調べてみましょう。

for every line of input
  if line matches condition then
    execute code block

これは、おそらくどの言語でも何らかの形でコードに何度も記述したことがある典型的なフィルターです。awk 言語は、このようなフィルターを非常に簡単に記述できるように設計されています。

awk はループを実行するので、ループ内にコードを書くだけで済みます。awk の構文では、if の定型句が省略されるため、条件とコード ブロックを書くだけで済みます。

condition { code block }

awk ではこれを「ルール」と呼びます。

条件またはコード ブロックのいずれかを省略できます (明らかに両方を省略することはできません)。awk は、不足している部分を暗黙的に埋めます。

条件を省略すると

{ code block }

それは暗黙的に真実となる

true { code block }

つまり、コードブロックは各行ごとに実行されることになります

コードブロックを省略すると

condition

すると暗黙的に現在の行が印刷されます

condition { print current line }

元のawkコードをもう一度見てみましょう

!a[$0]++

中括弧内に配置されないので、ルールの条件部分になります。

暗黙のループとif文を書いて印刷してみましょう

for every line of input
  if !a[$0]++ then
    print line

元の疑似コードと比較する

for every line of input                      # implicit by awk
  if i have not seen this line before then   # at least we know the conditional part
    print line                               # implicit by awk
  take note that i have now seen this line   # ???

ループ、if、print は理解できました。しかし、重複行でのみ false と評価されるように動作する仕組みは何でしょうか? また、すでに表示されている行をどのように記録するのでしょうか?

この獣を分解してみましょう:

!a[$0]++

C または Java をある程度知っていれば、いくつかのシンボルはすでに知っているはずです。セマンティクスは同一か、少なくとも類似しています。

感嘆符 ( !) は否定子です。式をブール値として評価し、結果が何であれ否定されます。式が true と評価された場合、最終結果は false になり、その逆も同様です。

a[..]は配列です。連想配列です。他の言語では、マップまたは辞書と呼ばれます。awk では、すべての配列は連想配列です。 には特別な意味はありません。これは単に配列の名前です。 または とすることaもできます。xeliminatetheduplicate

$0入力からの現在の行です。これは awk 固有の変数です。

プラスプラス ( ++) は、後置増分演算子です。この演算子は、変数の値が増分されるという 2 つのことを行うため、少し扱いに​​くいです。また、増分されていない元の値を「返して」、さらに処理を行います。

   !        a[         $0       ]        ++
negator   array   current line      post increment

それらはどのように連携するのでしょうか?

おおよそ次の順序です:

  1. $0現在の行
  2. a[$0]現在の行の配列の値です
  3. ポストインクリメント ( ++) は から値を取得しa[$0]、それを増分して に格納しa[$0]、その後、元の値を次の演算子である否定子に「返します」。
  4. 否定子 ( )は から元の値である!値を取得します。これはブール値に評価され、その後否定されて暗黙の if に渡されます。++a[$0]
  5. if は行を印刷するかどうかを決定します。

つまり、行が印刷されるかどうか、またはこの awk プログラムのコンテキストでは、行が重複しているかどうかは、最終的には の値によって決まりますa[$0]

拡張により、この行がすでに表示されているかどうかを記録するメカニズムは、++増分された値を に戻すときに発生する必要がありますa[$0]

もう一度擬似コードを見てみましょう

for every line of input
  if i have not seen this line before then   # decided based on value in a[$0]
    print line
  take note that i have now seen this line   # happens by increment from ++

皆さんの中には、これがどのように展開するかをすでに知っている人もいるかもしれませんが、ここまで来たので、最後のいくつかのステップを踏んで、++

暗黙のawkコードから始めます

for each line as $0
  if !a[$0]++ then
    print $0

作業の余地を残すために変数を導入しましょう

for each line as $0
  tmp = a[$0]++
  if !tmp then
    print $0

では分解してみましょう++

この演算子は、変数の値を増分し、さらに処理するために元の値を返すという 2 つのことを行うことに注意してください。したがって、は++2 行になります。

for each line as $0
  tmp = a[$0]       # get original value
  a[$0] = tmp + 1   # increment value in variable
  if !tmp then
    print $0

言い換えれば

for each line as $0
  tmp = a[$0]       # query if have seen this line
  a[$0] = tmp + 1   # take note that has seen this line
  if !tmp then
    print $0

最初の疑似コードと比較する

for every line of input:
  if i have not seen this line before:
    print line
  take note that i have now seen this line

これで完了です。ループ、if、print、クエリ、メモの取得があります。疑似コードとは順序が異なります。

8文字に凝縮

!a[$0]++

これは、awks の暗黙的なループ、暗黙的な if、暗黙的な print により可能になり、++クエリとメモ作成の両方が実行されるためです。

1 つの疑問が残ります。最初の行の の値は何でしょうかa[$0]? または、これまでに見たことのない行の の値は何でしょうか? 答えは暗黙的です。

awk では、初めて使用される変数は暗黙的に宣言され、空の文字列に初期化されます。配列を除きます。配列は宣言され、空の配列に初期化されます。

++暗黙的に数値に変換します。空の文字列はゼロに変換されます。他の文字列は、ベスト エフォート アルゴリズムによって数値に変換されます。文字列が数値として認識されない場合は、再びゼロに変換されます。

!暗黙的にブール値に変換されます。数値 0 と空の文字列は false に変換されます。それ以外は true に変換されます。

つまり、行が初めて表示されるときに はa[$0]空の文字列に設定されます。 空の文字列は によってゼロに変換されます++(これも 1 に増分され、 に戻されますa[$0])。 ゼロは によって false に変換されます!。 の結果は!true なので、行が印刷されます。

の値はa[$0]1 になります。

行が 2 回目に出現すると、a[$0]数値 1 が true に変換され、結果は!false になるため、印刷されません。

同じ行がさらに出現するたびに、数値が増加します。ゼロ以外のすべての数値は true であるため、結果は!常に false となり、その行は再度印刷されることはありません。

このようにして重複が削除されます。

要約: 行が何回表示されたかをカウントします。ゼロの場合は出力します。それ以外の数値の場合は出力しません。暗黙的な要素が多いため、短くなる場合があります。


ボーナス: ワンライナーのいくつかのバリエーションと、それが何をするのかについての非常に短い説明。

$0(行全体) を (2 番目の列) に置き換えると$2、2 番目の列に基づいてのみ重複が削除されます。

$ cat input 
x y z
p q r
a y b

$ awk '!a[$2]++' input 
x y z
p q r

!(否定)を(1に等しい)に置き換える==1と、重複している最初の行が出力されます。

$ cat input 
a
b
c
c
b
b

$ awk 'a[$0]++==1' input 
c
b

>0を(0 より大きい)に置き換えて追加すると、{print NR":"$0}重複するすべての行が行番号とともに出力されます。NRは、行番号 (awk 用語ではレコード番号) を含む特別な awk 変数です。

$ awk 'a[$0]++>0 {print NR":"$0}' input 
4:c
5:b
6:b

これらの例が、上で説明した概念をさらに理解するのに役立つことを願っています。

答え4

expr++と はどちらも++exprの省略形であることを付け加えておきますexpr=expr+1。しかし

$ awk '!a[$0]++' f # or 
$ awk '!(a[$0]++)' f

加算前にexpr++は評価されるので、すべての一意の値を出力します。expr

$ awk '!(++a[$0])' f

++exprは と評価され、この場合は常にゼロ以外の値を返すため、何も出力されません。expr+1また、否定は常にゼロ値を返します。

関連情報