1. 概要

awk は、テキストを処理するための便利で強力なコマンドラインユーティリティです。 場合によっては、複数の入力ファイルを読み取って処理する必要があります。

このチュートリアルでは、awkコマンドを使用して複数の入力ファイルを処理する方法を学習します。

2. 複数のファイルの処理

データファイルのコレクションを処理して出力を生成したい場合があります。

たとえば、ユーザースコアを含む3つの入力ファイルがあるとします。

$ head score*.txt
==> score1.txt <==
Tom 20
Jerry 40
Mark 25
Amanda 37

==> score2.txt <==
Mark 75
Tom 70
Jerry 7
Amanda 40

==> score3.txt <==
Mark 73
Amanda 47
Jerry 79
Tom 40

すべてのファイルが同じ形式を共有していることに注意してください。各行には、空白で区切られた名前とスコアが含まれています。

上記のファイルから各ユーザーのスコアの合計を計算してみましょう。

$ awk '{ sum[$1]+=$2 } END { for(user in sum) print user, sum[user] }' score*.txt
Tom 130
Jerry 126
Mark 173
Amanda 124

上記のコードでは、連想配列 sum を作成して、各ユーザーのスコアの合計を計算して保存しました。 最後に、 END ブロックで、配列内の要素を出力しました。

入力ファイルが同じ形式を共有する場合、 複数の入力ファイルを単一のマージされた入力として扱うことができます。 これは比較的単純な状況です。

ただし、実際には、入力ファイル間の関連付けを処理する必要があることがよくあります。 次のセクションでは、これらの状況について詳しく説明します。

3. 2つの関連する入力ファイルの処理

次の例では、行番号とawkの組み込みNRおよびFNR変数を使用して2つの関連する入力ファイルを処理する方法を示します。

3.1. NRFNRを理解する

NRFNRは、2つの組み込みのawk変数です。 NRは、これまでに読み取ったレコードの総数を示し、FNRは、現在の入力ファイルで読み取ったレコードの数を示します。

例を通して2つの変数を理解しましょう。 まず、2つのファイルを作成しましょう。

$ head file1.txt file2.txt 
==> file1.txt <==
file1-1
file1-2
file1-3
file1-4
file1-5

==> file2.txt <==
file2-1
file2-2
file2-3
file2-4
file2-5

次に、単純な awk ワンライナーを作成します。これは、上記の2つのファイルを入力として受け取り、 NRおよびFNR:の値とともに各ファイルの行を出力します。 ]

$ awk '{ printf "Line:%s, NR:%d, FNR:%d\n", $0, NR, FNR}' file1.txt file2.txt
Line:file1-1, NR:1, FNR:1
Line:file1-2, NR:2, FNR:2
Line:file1-3, NR:3, FNR:3
Line:file1-4, NR:4, FNR:4
Line:file1-5, NR:5, FNR:5
Line:file2-1, NR:6, FNR:1
Line:file2-2, NR:7, FNR:2
Line:file2-3, NR:8, FNR:3
Line:file2-4, NR:9, FNR:4
Line:file2-5, NR:10, FNR:5

上記の出力は次のことを示しています。

  • 最初の入力ファイルの場合、NRFNRの値は常に同じです
  • awk が新しい入力ファイルを読み取ると、FNR変数は1にリセットされますが、NRは増加し続けます

次のセクションでは、入力ファイルをNRおよびFNRから区別し、関係を処理する方法を説明します。

3.2. 定義された行番号で行を印刷する

例から始めましょう。

2つのファイルを用意しました。

$ head all_lines.txt lines_to_show.txt 
==> all_lines.txt <==
line-01
line-02
line-03
line-04
line-05
line-06
line-07
line-08
line-09
line-10

==> lines_to_show.txt <==
2
3
4
5
7

ファイルall_lines.txtには10行のテキストがあり、ファイルlines_to_show.txtには行番号が格納されています。 ここで、 all_lines.txt ファイルから行を出力するのは、その行番号がファイルlines_to_show.txtで定義されている場合のみです。

ソリューションを見て、それがどのように機能するかを理解しましょう。

$ awk 'NR==FNR { out[$1]=1; next } { if (out[FNR]==1) print $0 }' lines_to_show.txt all_lines.txt 
line-02
line-03
line-04
line-05
line-07

この問題は2つのステップで解決しました。

  1. ファイルlines_to_show.txtを読み取り、行番号を配列に保存します。
  2. ファイルall_lines.txtから行を読み取るときに、現在の行番号が配列に存在する場合はその行を出力します。

それでは、上記の awk コードを詳しく見て、どのように機能するかを理解しましょう。

ステップ1: NR == FNR {out [$ 1] = 1; 次へ}

  • awk は、最初のファイル lines_to_show.txt、から最初の行を読み取ります。これは 2
  • NRFNRの両方の値が同じ1、になっているため、 out という名前の連想配列を作成し、out[を設定します。 2] = 1
  • next ステートメントは、 awk に残りの処理をスキップさせ、次のレコードを読み取ります
  • 最初の入力ファイルの処理中、 NR ==FNRは常にTrueであるため、awkがファイルlines_to_show.txtを処理した後、次のようになります。 out [2] = out [3] = out [4] = out [5] = out [7] = 1

ステップ2: {if(out [FNR] == 1)print $ 0}

  • 2番目のファイルall_lines.txtの処理を開始すると、FNR1 にリセットされるため、FNRNRの値は異なります。
  • 配列outには、要素 out [1]、がないため、出力しません。
  • awkは次の行line-02を読み取ります。 現在FNR2、であり、 out [2] = 1 があるため、この行は print $0[によって印刷されます。 X163X]
  • このようにして、 awk が2番目の入力ファイルを通過した後、必要な出力を取得します

awk:で言及する価値があります

  • ゼロ以外の数値はTrue として評価されます。つまり、’ {if(out [FNR] == 1)print $ 0}’ can ‘ {if(out [FNR])print $0}’と書く
  • True値は、デフォルトのアクションをトリガーします。現在のレコードを出力します。したがって、’ {if(out [FNR])print $0}’‘out[として記述できます。 FNR]’

したがって、この問題に対するawkワンライナーソリューションをよりコンパクトに記述できます。

$ awk 'NR==FNR { out[$1]=1; next } out[FNR]' lines_to_show.txt all_lines.txt

3.3. 参加して計算する

このセクションでは、別の実際的な例を示します。 いつものように、最初に2つの入力ファイルを見てみましょう。

$ head price.txt purchasing.txt
==> price.txt <==
Product Price(USD/Kg) Supplier
Apple 3.20 Supplier_X
Orange 3.00 Supplier_Y
Peach 5.35 Supplier_Y
Pear 5.00 Supplier_X
Mango 12.00 Supplier_Y
Pineapple 7.70 Supplier_X

==> purchasing.txt <==
Product Volume(Kg) Date
Orange 120 2020-04-02
Apple 400 2020-04-03
Peach 70 2020-04-05
Pear 50 2020-04-17

Product Date 、および新しい列 Cost を含むコストレポートを生成します。ここで、 Cost = Price *Volumeです。

最初に解決策を見てみましょう:

$ awk 'BEGIN { print "Product Cost Date" }
       FNR>1 && NR==FNR { price[$1]=$2; next }
       FNR>1 { printf "%s $%.2f %s\n",$1, price[$1]*$2, $3}' price.txt purchasing.txt

Product Cost Date
Orange $360.00 2020-04-02
Apple $1280.00 2020-04-03
Peach $374.50 2020-04-05
Pear $250.00 2020-04-17

それでは、コードを詳しく見て、どのように機能するかを理解しましょう。

  • BEGINブロックはヘッダーを印刷します
  • FNR>1は入力ファイルのヘッダー行をスキップします
  • NR ==FNR{価格[$1]= $ 2; next} は、連想配列 price を作成し、最初の入力ファイルから各行を読み取り、 Name:PriceKey:Value要素として配列
  • 2番目のファイルを処理するとき、連想配列 price から価格値を見つけ、 Cost を計算し、printfを使用して出力を出力します。

3.4. 2つの入力ファイルを処理するための一般的なパターン

awk を使用して2つの入力ファイルを処理する必要がある場合、この典型的なパターンを使用して問題を解決することを検討できます

awk 'NR==FNR {
    // read lines from the first input file
    // do calculation and save required value
    // in variables or arrays
    next
}

{
    // process the lines from the second file
    // with the variables or arrays we prepared above
}'  inputFile1 inputFile2

4. 2つ以上の関連する入力ファイルの処理

FNRNRの値を比較することにより、2つの入力ファイルを処理するコンパクトな方法を学びました。

ただし、3つ以上の入力ファイルがある場合、この方法は機能しません。

これは、入力ファイルが変更されると、 FNRが常に1、にリセットされるためです。 FNR変数で入力ファイルを区別できなくなりました。

4.1. FILENAME変数

FILENAMEは、awkコマンドが現在処理している入力ファイルの名前を格納する組み込み変数です

$ awk '{ print $0 " => " FILENAME}' file1.txt file2.txt file3.txt
file1-1 => file1.txt
file1-2 => file1.txt
file1-3 => file1.txt
file1-4 => file1.txt
file1-5 => file1.txt
file2-1 => file2.txt
file2-2 => file2.txt
file2-3 => file2.txt
file2-4 => file2.txt
file2-5 => file2.txt
file3-1 => file3.txt
file3-2 => file3.txt
file3-3 => file3.txt
file3-4 => file3.txt
file3-5 => file3.txt

この変数を使用して、入力ファイルを区別し、さまざまな処理ロジックを適用できます。

4.2. 改訂された参加と計算

前のセクションで、果物の購入コストに関するレポートを作成しました。

例を簡単に確認しましょう。 2つの入力ファイルがあります。

  •   price.txt :価格とサプライヤーのデータが含まれています:製品、価格、サプライヤー
  • running.txt :購買活動の保存:製品、ボリューム(Kg)、日付

サプライヤーとの良好なパートナーシップにより、彼らは私たちにいくつかの割引を提供することに同意しました。 次に、3番目のファイルdiscount.txtを追加します。

$ cat discount.txt
Supplier Discount
Supplier_X 0.10
Supplier_Y 0.20

3つの入力ファイルから購入コストに関する新しいレポートを生成しましょう。

$ awk 'fname != FILENAME { fname = FILENAME; idx++ }
        FNR > 1 && idx == 1 { discount[$1] = $2 }
        FNR > 1 && idx == 2 { price[$1] = $2 * ( 1 - discount[$3] ) }
        FNR > 1 && idx == 3 { printf "%s $%.2f %s\n",$1, price[$1]*$2, $3 }
       ' discount.txt price.txt purchasing.txt
Orange $288.00 2020-04-02
Apple $1152.00 2020-04-03
Peach $299.60 2020-04-05
Pear $225.00 2020-04-17

上記のコードでは、 FNR> 1 を使用して、入力ファイルからヘッダー行をスキップしました。 また、異なるファイル処理間でデータを共有するための連想配列を作成しました。

ただし、入力ファイルを区別するための鍵は次のコード行です:

fname != FILENAME{ fname = FILENAME; idx++ }

それでは、それがどのように機能するかを理解しましょう。

  1. 現在のFILENAMEを格納する変数fnameを宣言し、現在の入力ファイルのインデックスを格納するidx変数を作成します。
  2. 現在の入力ファイルが変更されると、 fname!=FILENAMETrueになります。
  3. 次に、fnameを新しいFILENAMEで更新し、idx変数をインクリメントします。
  4. 後で、 idx変数によって入力ファイルを区別し、各入力ファイルを異なる方法で処理します

これは、複数の入力ファイルを処理するための一般的な手法の1つです。

4.3. 入力ファイルインデックスと ファイル名

組み込みのFILENAME変数には、現在の入力ファイルの名前が格納されていることがわかりました。 前のセクションのコードを読んでいるときに、次の質問が出てくることがあります。例のように、ファイル名を直接比較するのではなく、各入力のインデックスによって入力ファイルを区別するのはなぜですか

FNR > 1 && FILENAME == "discount.txt" {...}
FNR > 1 && FILENAME == "price.txt" {...}
FNR > 1 && FILENAME == "purchasing.txt" {...}

FILENAME変数とfilenameの比較は、この例でも機能します。 ただし、にはいくつかの欠点があります。

最も注目すべきは、ハードコードされたファイル名をawkスクリプトに取り込むことです。 つまり、ファイルの名前を変更するときは、コードも更新する必要があります

たとえば、2番目のファイル price.txtを「/full/path/to/price.txt」に変更すると、スクリプトを変更する必要があります。

場合によっては、「 $ PWD /price.txt」などのシェル変数を使用してファイル名を渡す必要があります。 この場合、FILENAME変数の正確な値はわかりません。

回避策は、次のように == の代わりに正規表現一致演算子〜を使用することです。

FNR > 1 && FILENAME ~ /\/price[.]txt$/ {...}

ただし、 awk コマンドにプロセス置換を入力「ファイル」としてフィードすると、回避策は失敗します。

プロセス置換を使用すると、入力ファイルの名前は pipe()システムコールによって自動的に生成されます。 ファイル名は動的になります。

この場合の例を見てみましょう:

$ echo "a dummy line" > dummy.txt
$ awk '{print FILENAME}' dummy.txt <(cat dummy.txt )
dummy.txt
/proc/self/fd/11

したがって、ファイル名よりも入力ファイルのインデックスを使用して、入力ファイルを区別することをお勧めします。

5. 結論

この記事では、awkコマンドを使用するときに複数の入力ファイルを処理する方法について説明しました。