【Bash】awkコマンドを色々試してみる!

どーも!

たかぽんです!

今回は先日初めて使ったのですが、awkコマンドが結構便利だったため、他にどんなことができるのか?

基本的なところからawkコマンドで色々と試してみようと思います!

awkコマンドとは?

そもそもawkコマンドは、文字列を扱うためのプログラミング言語で、Linuxではデフォルトで用いることができたりします。

実はこのコマンド、区切り文字等で区切られたテキストととっても相性が良く、例えばカンマ区切りのcsvだったり、普段ls等で出力される標準出力内容などもスペース区切りのため、比較的便利に扱えたりします。

詳細は後述しますが、以下はwhoコマンドの結果から、consoleがttys002のもののみ検索している例です。

みなさんご存知、grepパターンと比較しています。

taka@Taka Downloads % who
taka             console      12 29 23:58
taka             ttys000       1  3 05:15
taka             ttys001       1 20 19:34
taka             ttys002       1 20 19:36
taka             ttys003       1 20 20:02
taka             ttys004       1 27 02:10

taka@Taka Downloads % who | grep ttys002
taka             ttys002       1 20 19:36

taka@Taka Downloads % who | awk '/ttys002/ {print $0}'
taka             ttys002       1 20 19:36

awkコマンドで色々試してみる!

簡単な例は先ほど見ましたが、先ほどのwhoコマンドの出力を用いて、少し深掘りしてみます。

whoコマンド自体は現在コンソールがいくつ開かれていて、それぞれいつ開かれたものか、開いたのはだれか?といった情報が記載されています。

あくまで、awkの操作のためのテストデータなので、出力の詳細はきにしなくてOKです!

taka@Taka Downloads % who
taka             console      12 29 23:58
taka             ttys000       1  3 05:15
taka             ttys001       1 20 19:34
taka             ttys002       1 20 19:36
taka             ttys003       1 20 20:02
taka             ttys004       1 27 02:10

では、早速ですがいくつかのパターンで試したものを以下に示してみます。

awkの基本を理解する!

では、まずはawkコマンドの基本を理解するために簡単な例をいくつか挙げます。

taka@Taka Downloads % who | awk '{print $1 $2 $3 $4 $5}'
takaconsole122923:58
takattys0001305:15
takattys00112019:34
takattys00212019:36
takattys00312020:02
takattys00412702:10
taka@Taka Downloads % who | awk '{print $0}'
taka             console      12 29 23:58
taka             ttys000       1  3 05:15
taka             ttys001       1 20 19:34
taka             ttys002       1 20 19:36
taka             ttys003       1 20 20:02
taka             ttys004       1 27 02:10

awkコマンドでは、$0 ~ $nまでの変数、そして、特別な$NFという値を用いることができます。

前述の出力を見ていただけるとわかりますが...

$0: 入力されたテキスト

$1: 入力されたテキストのうち、区切り文字で区切った際の一番最初の要素(=taka)

$2: 入力されたテキストのうち、区切り文字で区切った際の二番最初の要素(=ttys...)

....

$NF: 入力されたテキストのうち、区切り文字で区切った際の最後の要素

といった具合になっています。

$0で書き出されている内容と、$1 ~ $5で書き出されている内容は大体一緒なのが確認できると思います。

$NFは特別枠で、一番最後の要素を参照しています。

そのため、今回だと、$5と$NFは同じ値になります。

taka@Taka Downloads % who | awk '{print $NF}'
23:58
05:15
19:34
19:36
20:02
02:10
taka@Taka Downloads % who | awk '{print $5}'
23:58
05:15
19:34
19:36
20:02
02:10

このように、区切り文字で値を分けて、それぞれの値に対して何かしらの操作をおこなったりすることができるんですね!

ちなみに、最初の例で、バラして表示する時、全ての値がくっついていたのですが...

以下のようにprintする際にスペースを開けても出力には影響がなく、カンマ区切りにすることで、区切り文字で区切ることができるようです。(今回は区切り文字がスペースなので、スペースが入っている)

taka@Taka Downloads % who | awk '{print $1$2$3$4$5}'
takaconsole122923:58
takattys0001305:15
takattys00112019:34
takattys00212019:36
takattys00312020:02
takattys00412702:10
taka@Taka Downloads % who | awk '{print $1,$2,$3,$4,$5}'
taka console 12 29 23:58
taka ttys000 1 3 05:15
taka ttys001 1 20 19:34
taka ttys002 1 20 19:36
taka ttys003 1 20 20:02
taka ttys004 1 27 02:10

なんとなく基本がわかったので、さらに細かく見ていきます!

入力・出力の区切り文字を明示する!

まずは先ほどの以下にて、スペース区切りで書き出されたのですが、これに明確に区切り文字を指定して、入力と出力の区切りを別の形にしてみようと思います!

taka@Taka Downloads % who | awk '{print $1,$2,$3,$4,$5}'
taka console 12 29 23:58
taka ttys000 1 3 05:15
taka ttys001 1 20 19:34
taka ttys002 1 20 19:36
taka ttys003 1 20 20:02
taka ttys004 1 27 02:10

入力の区切りを指定する場合は以下のようにします。

taka@Taka Downloads % who | awk -F':' '{print $1}'
taka             console      12 29 23
taka             ttys000       1  3 05
taka             ttys001       1 20 19
taka             ttys002       1 20 19
taka             ttys003       1 20 20
taka             ttys004       1 27 02
taka@Taka Downloads % who | awk -F':' '{print $2}'
58
15
34
36
02
10

上記は、区切り文字として":"を指定しています。

そのため、ちょっと例が悪く、少し分かりづらいですが...

$1は最初っから初めて":"がでてくるまで、そして、$2が最後の部分のみ...となっています。

では、今度は書き出しの時の区切り文字を指定する場合は、OFSという変数をオプションとして指定します。

taka@Taka Downloads % who | awk -v 'OFS=:' '{print $1,$2,$3,$4,$5}'
taka:console:12:29:23:58
taka:ttys000:1:3:05:15
taka:ttys001:1:20:19:34
taka:ttys002:1:20:19:36
taka:ttys003:1:20:20:02
taka:ttys004:1:27:02:10

上記例では、デフォルトのスペースではなく、":"を区切りにしてみた例です。(最後の時間と被っちゃって分かりづらくなっちゃっていますが...w)

このようにして、入力の時はスペース区切りで、そして書き出す時はコロン区切りで...といったことも可能です。

最後に、入力は":"区切り、出力は"-"区切りにした場合は以下のような感じです。

taka@Taka Downloads % who | awk -F'[:]' -v 'OFS=-' '{print $1,$2}'
taka             console      12 29 23-58
taka             ttys000       1  3 05-15
taka             ttys001       1 20 19-34
taka             ttys002       1 20 19-36
taka             ttys003       1 20 20-02
taka             ttys004       1 27 02-10

入力の区切り文字が:なので、一番最後の時刻部分で二つの要素に分かれ、書き出し時は$1, $2を出力し、その間のカンマ部分が"-"になるため、上記のような結果となっています。

検索をしてみる

さて、では今度は先ほどすでに書いたのですが...

検索...といったこともやってみようと思います!

すでに一度お見せしましたが、特定のフィールド値の行だけ探す場合...

taka@Taka Downloads % who | awk '/ttys002/ {print $0}'
taka             ttys002       1 20 19:36

といった書き方が可能です。

{print $0}部分は出力の処理を行い、その手前に条件等を記載することで、ある条件を満たす場合だけ、処理を行う...といったことが可能です。

そのため、上記例だと、"//"部分で正規表現を用いて、該当する値がある場合のみprintする...といった形です。

ただし、行全体から探すため、ざっくりと検索する場合はいいのですが、もし他の行で同じ値があった場合はそれもヒットしてしまいます。

ちなみに、この場合、検索の都合上ヘッダー部分が消えてしまうのですが...NR変数という変数を用いると、1行目の...という条件を追加することが可能です。

taka@Taka Downloads % who | awk 'NR==1 {print $0} /ttys002/ {print $0}'
taka             console      12 29 23:58
taka             ttys002       1 20 19:36

(分かりづらいですが、まず、1行目を探し、出力、その後、ttys002のものを探し、出力...といった具合です。)

次に、先ほどだとやはりいろんなフィールド値で引っかかりそうなので、明確にある列のフィールドだけで指定したい場合は以下のようにすることが可能です。

taka@Taka Downloads % who | awk '$2 == "ttys002" {print $0}'
taka             ttys002       1 20 19:36

上記は見ていただければ分かりますが、$2が"ttys002"のものだけ...といった条件になっています。

また、さらに深掘りし、正規表現で特定のフィールドだけ指定したい場合は...

taka@Taka Downloads % who | awk '$2 ~ /ttys002/ {print $0}'
taka             ttys002       1 20 19:36

といった形で指定可能です。

また、応用すれば数値や時間形式での除外や、大小比較なんかももちろん可能です。

// 数値の例
taka@Taka Downloads % who | awk '$4 != 20 {print $0}'
taka             console      12 29 23:58
taka             ttys000       1  3 05:15
taka             ttys004       1 27 02:10
taka@Taka Downloads % who | awk '$4 < 20 {print $0}'
taka             ttys000       1  3 05:15

// 時分の例
taka@Taka Downloads % who | awk '$5 <= "05:15" {print $0}'
taka             ttys000       1  3 05:15
taka             ttys004       1 27 02:10
taka@Taka Downloads % who | awk '$5 > "05:15" {print $0}'
taka             console      12 29 23:58
taka             ttys001       1 20 19:34
taka             ttys002       1 20 19:36
taka             ttys003       1 20 20:02


// 日時の例
// test-date.txtは以下のコマンドで作成可能です(要watchコマンド)
// watch -n 1 "date '+%Y-%m-%d %H:%M:%S'" >> test-date.txt

taka@Taka Downloads % cat test-date.txt
2024-01-28 17:37:38
2024-01-28 17:37:40
2024-01-28 17:37:41
2024-01-28 17:37:42
...
2024-01-28 17:38:17

taka@Taka Downloads % cat test-date.txt | awk '$0 < "2024-01-28 17:37:42" {print $0}'
2024-01-28 17:37:38
2024-01-28 17:37:40
2024-01-28 17:37:41
taka@Taka Downloads % cat test-date.txt | awk '$2 < "17:37:41" {print $0}'
2024-01-28 17:37:38
2024-01-28 17:37:40

ここら辺を把握できているだけで、ログ分析などがだいぶん捗りそうな気がしますね...!

最初と最後に特定の処理を挟む!

さて、今度はこのAwkコマンドの具体的な処理を行う前に、事前に実行する処理や一通り終わった後に行う処理などを書いてみます。

例えば以下のような形です。

直前に実行したものに少し加えたのですが...

taka@Taka Downloads % cat test-date.txt | awk 'BEGIN { print "Date from smaller than 17:37:41" } $2 < "17:37:41" {print $0}'
Date from smaller than 17:37:41
2024-01-28 17:37:38
2024-01-28 17:37:40

 who | awk '$4 != 20 {print $0}'

出力の一番最初に”Date from smaller than 17:37:41"といった値をだしました。

本来はもっと複雑なプログラムで、BEGINで変数の初期化をしたり...といった使い方をするようですが、ここでは簡単な例に留めておきます。

また、おなじように...

taka@Taka Downloads % cat test-date.txt | awk 'BEGIN { print "Date from smaller than 17:37:41" } $2 < "17:37:41" {print $0} END { print "Finish Date from smaller than 17:37:41" }'
Date from smaller than 17:37:41
2024-01-28 17:37:38
2024-01-28 17:37:40
Finish Date from smaller than 17:37:41

ENDとして、最後に行う処理もつけることが可能です。

awkプログラムを書いてみる!

では、最後に上記でやってきたawkの記述を別ファイルに書き出して、それを実行する...というのを試してみます!

BEGIN {
    print "処理開始"
}
# 1行目のヘッダー表示
NR==1 {
    print $0
}
# 時刻が17:37:41より前のもののみ
$2 < "17:37:41" {
    print $0
}
END{
    print "処理終了"
}

実行すると以下のような感じです。

taka@Taka Downloads % cat test-date.txt | awk -f test.awk
処理開始
2024-01-28 17:37:38
2024-01-28 17:37:38
2024-01-28 17:37:40
処理終了

後から気づきましたが、雑に作った日付データだったのでヘッダー不要でした...w

このように、-fオプションでawkコマンドにファイルを使う旨知らせれば、その後にawkのプログラムを書いたファイル指定で動作させることが可能です!

ちなみに、いまさらなんですが、今までtxtファイルの指定をパイプでつないでいましたが、awkの最後に指定するだけでもOKです。

例えば上記例だと...以下でも同じことができます。

awk -f test.awk test-date.txt
まとめ

今回、awkコマンドを用いてさまざまなことを試してみました...!

さらなる応用として、何かしらのソースから、一部の値だけ用いてcsvを作成する...といったこともできるので、これ使えると結構自由度が高くなりそうですよね!

表示だけでもlogの確認する時にとてもべんりになりそうなので...!

是非みなさんも使ってみてください!

ちなみに、今回の内容は以下などを参考にしていますが、やはりプログラミング言語なので、まだまだお伝えできていない部分もあると思います。

興味のある方は是非一読してみてください。

それでわ!

おすすめの記事