awk 基礎

# hello
```shell-session
$ echo hello | awk '{print $0}'
hello
```

# サンプルデータ (num.txt) の準備
```shell-session:num.txt にデータを書き込む
$ awk 1 >num.txt
one   eins   yi  1  0
two   zwei   er  2  1
three drei   san 3  1
four  vier   si  4  2
five  funf   wu  5  3
six   sechs  liu 6  5
seven sieben qi  7  8
eight acht   ba  8  13
nine  neun   jiu 9  21
ten   zehn   shi 10 34
(Ctrl-d で入力終了)
```
```shell-session:num.txt の内容を確認 (cat)
$ awk 1 num.txt
one   eins   yi  1  0
two   zwei   er  2  1
three drei   san 3  1
four  vier   si  4  2
five  funf   wu  5  3
six   sechs  liu 6  5
seven sieben qi  7  8
eight acht   ba  8  13
nine  neun   jiu 9  21
ten   zehn   shi 10 34
```

# 基本
`$2` で2列目が取り出せる
```
$ cat num.txt | awk '{print $2}'
eins
zwei
drei
vier
funf
sechs
sieben
acht
neun
zehn
```

`$0` はすべての列 (正確には行全体)
```
$ cat num.txt | awk '{print $0}'
one   eins   yi  1  0
two   zwei   er  2  1
three drei   san 3  1
four  vier   si  4  2
five  funf   wu  5  3
six   sechs  liu 6  5
seven sieben qi  7  8
eight acht   ba  8  13
nine  neun   jiu 9  21
ten   zehn   shi 10 34
```

5列目の値が`8`の行の1列目を表示
```
$ cat num.txt | awk '$5==8{print $1}'
seven
```

`print` 方法いろいろ
```shell-session:print 方法色々
$ awk 'BEGIN{print "a" "b" 3}' # print 文に、"ab3" に連結された1つの文字列を渡す
ab3

$ awk 'BEGIN{print "a", "b", 3}' # print 文に3つの文字列を渡す (空白が入る)
a b 3

$ awk 'BEGIN{printf("%s|%s|%03d\n", "a", "b", 3)}' # printf 関数に4つの引数を与える
a|b|003
```

# 基本構造
```shell-session:基本構造
awk '
    条件式1{処理1}
    条件式2{処理2}
'
```

処理の流れ:
1. 入力行の1行目で条件1を評価し、真なら処理1を実行する。
1. 入力行の1行目で条件2を評価し、真なら処理2を実行する。
1. 入力行の2行目で条件1を評価し、真なら処理1を実行する。
1. 入力行の2行目で条件2を評価し、真なら処理2を実行する。

なお、偽は `""` (空文字) と `0` で、真はそれ以外 (e.g. `1` `"hoge"`) である。
変更した変数の値は後続の行の処理時にも引き継がれる (リセットされない)。

```shell-session:処理の流れ例
# 5列目の値が1の場合1列目を表示、3列目の値がsから始まる場合行全体を表示
$ cat num.txt | awk '$5==1{print $1} $3~/^s/{print $0}'
two
three
three drei   san 3  1
four  vier   si  4  2
ten   zehn   shi 10 34
```

条件式を省略すると、暗黙的に真となる。
```shell-session:条件式を省略
$ cat num.txt | awk '{print $1}'
one
two
three
four
five
six
seven
eight
nine
ten
```

```shell-session:条件式を省略 (複数処理)
$ cat num.txt | awk '{print $1}{print $4}'
one
1
two
2
three
3
four
4
five
5
six
6
seven
7
eight
8
nine
9
ten
10
```

処理を省略すると、暗黙的に `{print $0}` となる。
```shell-session:処理を省略
$ cat num.txt | awk '$5==1'
two   zwei   er  2  1
three drei   san 3  1
```

処理を省略して条件式だけを複数記述する場合、`;` を使う。
```shell-session:処理を省略 (複数条件)
$ cat num.txt | awk '$5==1; $1=="one"'
one   eins   yi  1  0
two   zwei   er  2  1
three drei   san 3  1
```

特別な条件式 `BEGIN`、`END` がある。
```shell-session:BEGIN END
$ cat num.txt | awk 'BEGIN{print "全ての処理の初めに1度だけ実行される"} $5==1; END{print "全ての処理の終わりに1度だけ実行される"}'
全ての処理の初めに1度だけ実行される
two   zwei   er  2  1
three drei   san 3  1
全ての処理の終わりに1度だけ実行される
```

# 正規表現関連
正規表現マッチは `~` 演算子で行う。
```shell-session: ~ 演算子 (正規表現マッチ)
$ cat num.txt | awk '$0 ~ /r /' # 正規表現 "r " にマッチした行を検索
two   zwei   er  2  1
four  vier   si  4  2

$ cat num.txt | awk '/r /' # $0 ~  は省略可能
two   zwei   er  2  1
four  vier   si  4  2
```

`sub("before", "after", 変数)` / `gsub` で `変数` を置換したものを `変数` に代入。
sub は最初のマッチのみ置換、gsub は全てのマッチを置換。
`変数` を省略すると暗黙的に `$0` となる。
関数の返り値は置換した個数。
```
$ echo hello world | awk '{print sub("l", "/", $0); print $0}'
1
he/lo world

$ echo hello world | awk '{print gsub("l", "/", $0); print $0}'
3
he//o wor/d

$ echo hello world | awk '{print sub("l", "/"); print $0}' # $0 は省略可
1
he/lo world

$ echo hello world | awk '{print gsub("l", "/"); print $0}' # $0 は省略可
3
he//o wor/d
```

# if/else/else if
一般的なプログラミング言語と同じ。
`""` (空文字) or `0` が偽、それ以外は真。
```shell-session: if/else/else if
$ echo 2 | awk '{if($0==1){print "one"} else if($0==2){print "two"} else {print "three"}}'
two
```

# for/while
```shell-session: for
$ awk 'BEGIN{for(i=0; i<3; i++){print i}}'
0
1
2
```

```shell-session: for(key in array)
$ awk 'BEGIN{array[1]="one"; array[2]="two"; array[3]="three"; for(key in array){print key, array[key]}}'
1 one
2 two
3 three
```

```shell-session: while
$ awk 'BEGIN{a=3; while(a--){print a}}'
2
1
0
```

# 変数

変数の宣言は特に不要。

```shell-session:変数定義と参照例
$ awk 'BEGIN{a=1; print a + 2}'
3
```

型は自動的に推論/変換される。
```shell-session:型の自動推論/変換
$ awk 'BEGIN{a="1"; print a + 2}'
3
```

文字列連結は、変数や値を空白で区切って並べるだけでいい。
(正確にはトークンが並んでいれば良いので、変数や値の切れ目が明確なら空白は必須ではない)
```shell-session:文字列連結
$ awk 'BEGIN{a="1"; print a "b"; c=2; d=c 3 4; print d}'
1b
234
```

未定義変数は、暗黙的に `""` (空文字) や `0` に評価される。
```shell-session:未定義変数は "" or 0
$ awk 'BEGIN{print a " <- a は空文字に評価"; print a + 0 " <- a は0に評価"}'
 <- a は空文字に評価
0 <- a は0に評価
```

未定義変数に `+=` や `++` 等を使うことも可能。

```shell-session:未定義変数へ加算代入
$ awk 'BEGIN{a+=3; print a}'
3
```

コマンドライン引数から `-v` で変数を渡すことが可能。
```shell-session: コマンドライン引数で変数を定義 (-v)
$ awk -v a=1 -v b=hoge 'BEGIN{print a, b}'
1 hoge
```

# 配列/辞書
配列 (`array[key]`) は連想配列である。
`key` は文字列でも数値でもいい。
```shell-session:
$ awk 'BEGIN{a[1]="one"; a["two"]=2; print a[1], a["two"]}'
one 2
```

2次元配列は `,` で実現可能。
```shell-session:
$ awk 'BEGIN{a["hoge",2]="fuga"; print a["hoge",2]}'
fuga
```

仕組み: 配列の添字に `,` が出現したとき、`,` の左右が `SUBSEP` 組み込み変数 (`0x1c`) で文字列連結される。
```shell-session:SUBSEP
$ awk 'BEGIN{printf("%s", SUBSEP)}' | xxd -p # SUBSEP 組み込み変数のデフォルト値は 0x1c
1c

$ awk 'BEGIN{a["hoge",2]="fuga"; print a["hoge" SUBSEP 2]}' # 添字が "hoge" と 2 を SUBSEP で結合されたものとなっている
fuga

$ awk 'BEGIN{SUBSEP="/"; a["hoge",2]="fuga"; print a["hoge/2"]}' # SUBSEP を / に変更してみた
fuga
```

`SUBSEP` は配列の添字以外では使われない。(と筆者は認識している)
`print 1, 2` は `print` 文に `1` と `2` を渡しているのであり、`SUBSEP` は働かない。

`for(key in array)` で配列の全ての key を回す。
```shell-session: for(key in array)
$ awk 'BEGIN{a["piyo"]=1; a[6]="hoge"; a[3]="fuga"; for(i in a){print i, a[i]}}'
6 hoge
piyo 1
3 fuga
```

# 組み込み変数
`$0, $1, ...`: 列の値を格納。`$(exp)` で exp に式を指定できる。代入も可能。
```
$ echo a b c | awk '{print $2}'
b

$ echo a b c | awk '{print $(1+1)}'
b

$ echo a b c | awk '{$2="bbbb"; print $0}'
a bbbb c

$ echo a b c | awk '$0=$2' # {print $2} と同じ
b
```

`NR`: 行番号
```shell-session:NR
$ yes | head -n4 | awk '{print NR, $0}'
1 y
2 y
3 y
4 y
```

NF: 現在行の列数
```shell-session:NF
$ printf 'a b c\nd e\nf' | awk '{print NF, $0}'
3 a b c
2 d e
1 f
```

```shell-session: $NF: 最後の列を表示
$ printf 'a b c\nd e\nf' | awk '{print NF, $NF}'
3 c
2 e
1 f
```

`RS`: レコードの区切りを指定。デフォルトは改行。
```shell-session: RS
$ echo $PATH | awk -v RS=: '{print $0}'
/usr/local/sbin
/usr/local/bin
/usr/sbin
/usr/bin
/sbin
/bin
```

`ORS`: 出力レコードの区切りを指定。デフォルトは改行。
```shell-session: ORS
$ seq 10 | awk -v ORS=: '{print $0}'
1:2:3:4:5:6:7:8:9:10:
```

`FS`: `$1` とか `$2` に分割する区切りを指定。デフォルトは空白やタブ。
正規表現が指定可能。
awk -F オプションでも指定可能。
```shell-session: FS
$ head -n5 /etc/passwd | awk -v FS=: '{print $7, $1}'
/bin/bash root
/usr/sbin/nologin daemon
/usr/sbin/nologin bin
/usr/sbin/nologin sys
/bin/sync sync

$ head -n5 /etc/passwd | awk -F: '{print $7, $1}'
/bin/bash root
/usr/sbin/nologin daemon
/usr/sbin/nologin bin
/usr/sbin/nologin sys
/bin/sync sync

$ echo user@host:/path/to/dir/file | awk -v FS=[@:] '{print $1, $2, $3}' # 正規表現が扱える
user host /path/to/dir/file
```

`OFS`: 出力の区切り文字を指定。
```shell-session: OFS
$ awk -v OFS=/ 'BEGIN{print 1, 2, 3}'
1/2/3
```

SUBSEP: 配列の添字の `[]` の中の `,` で文字列結合するときの区切り文字。デフォルトは `0x1c`。
上述の [配列辞書](#配列辞書) を参照。
```shell-session: SUBSEP
$ awk 'BEGIN{a["hoge",2]="fuga"; print a["hoge" SUBSEP 2]}'
fuga
```

ARGC: 引数のファイル名の個数+1。
ARGV: 引数のファイル名を格納した配列。
```shell-session: ARGC, ARGV
$ awk -v a=hoge 'BEGIN{print ARGC, ARGV[0], ARGV[1], ARGV[2]}' file1 file2
3 awk file1 file2
```

ENVIRON: 環境変数の配列。
```shell-session: ENVIRON
$ awk 'BEGIN{print ENVIRON["USER"]}'
yabeenico
```

# 応用例
## 列を削除
列に代入すればいい
```shell-session: 3列目を削除
$ echo a b c d | awk '{$3=""; print $0}'
a b  d
```

## 行を複製
```shell-session: 行を複製
$ seq 3 | awk '1;1'
1
1
2
2
3
3
```

## 代入演算子 `=` を活用
`=` 演算子は `+` `-` `*` `/` と同様に二項演算子である。
`(2+2)` の評価結果が`4`であるのと同様に、`(a=4)` の評価結果も`4`である。
```shell-session: = 演算子全体は代入値に評価される
$ awk 'BEGIN{print 1 + (a=3)}'
4
```

$0 への代入を条件式にすると `print` と同じことができる。
ただし、その結果 `$0` が偽になったら `print` されないので注意。
```shell-session: $0 に代入すると print と同じことができる
$ echo a b c | awk '$0=$2' # {print $2} と同じ
b
```

## cat と同じことをする
```shell-session: cat
$ awk 1 num.txt num.txt
one   eins   yi  1  0
two   zwei   er  2  1
three drei   san 3  1
four  vier   si  4  2
five  funf   wu  5  3
six   sechs  liu 6  5
seven sieben qi  7  8
eight acht   ba  8  13
nine  neun   jiu 9  21
ten   zehn   shi 10 34
one   eins   yi  1  0
two   zwei   er  2  1
three drei   san 3  1
four  vier   si  4  2
five  funf   wu  5  3
six   sechs  liu 6  5
seven sieben qi  7  8
eight acht   ba  8  13
nine  neun   jiu 9  21
ten   zehn   shi 10 34
```

## 重複削除 (順番保存)
```shell-session:重複削除 (順番保存)
$ printf "a\nb\na\nc" | awk '!a[$0]++' # 3行目の a が消える
a
b
c
```

解説
未定義変数は0に評価されるため、a が未定義のとき `a++` -> `a=a+1` -> `a=1` となる。
また、`a++` 自体は加算前の値に評価され、`++a` は加算後の値に評価される。
(ので本当は `++a` だけが `a=a+1` といえる)
```shell-session: a++, ++a
$ awk 'BEGIN{print a++; print a}'
0
1

$ awk 'BEGIN{print ++a; print a}'
1
1
```

未定義キーを指定された配列も同様の挙動となる。(`array[key]++`)
```shell-session: array[key]++, ++array[key]
$ awk 'BEGIN{print a["hoge"]++; print a["hoge"]; print ++a["fuga"]; print a["fuga"]}'
0
1
1
1
```

つまり、`a[$0]++` で行が出現した回数をカウントできることが分かる。
```shell-session: a[$0]++
$ printf "a\nb\na\nc" | awk '{print a[$0]++, $0}'
0 a
0 b
1 a
0 c
```

否定することで初出時に1となる。
```shell-session: !a[$0]++
$ printf "a\nb\na\nc" | awk '{print !a[$0]++, $0}'
1 a
1 b
0 a
1 c
```

あとはそれを条件式に利用すればいい。(`{print $0}` は省略可能)
```shell-session: 重複削除 (再掲)
$ printf "a\nb\na\nc" | awk '!a[$0]++'
a
b
c
```

`PATH` の重複削除への応用例。
`PATH` は順番の保存が必要なので `sort | uniq` は使えない。
`paste -sd:` で、行を `:` で結合

```bash:PATH の重複削除
$ export PATH=$(printf "$PATH"|awk -vRS=: '!a[$0]++'|paste -sd:)
```

```shell-session: 行を:で結合
$ seq 10 | paste -sd:
1:2:3:4:5:6:7:8:9:10
```

## ログ整形
`/var/log/auth.log` の `Accepted` な鍵の fingerprint を表示する。

```text:/var/log/auth.log
$ head /var/log/auth.log
Nov 13 01:17:01 myhost CRON[2443333]: pam_unix(cron:session): session closed for user root
Nov 13 01:22:25 myhost sshd[2443853]: Accepted publickey for user1 from 192.168.3.2 port 50632 ssh2: RSA SHA256:ID3jU1U/A11prLccA76POqNAq4sW/wflswuZkRbllu1
Nov 13 01:22:25 myhost sshd[2443852]: Accepted publickey for user1 from 192.168.3.2 port 50630 ssh2: RSA SHA256:ID3jU1U/A11prLccA76POqNAq4sW/wflswuZkRbllu1
Nov 13 01:22:25 myhost sshd[2443853]: pam_unix(sshd:session): session opened for user user1 by (uid=0)
Nov 13 01:22:25 myhost sshd[2443852]: pam_unix(sshd:session): session opened for user user1 by (uid=0)
Nov 13 01:22:25 myhost systemd-logind[711]: New session 7150 of user user1.
Nov 13 01:22:25 myhost systemd-logind[711]: New session 7151 of user user1.
Nov 13 01:22:25 myhost systemd: pam_unix(systemd-user:session): session opened for user user1 by (uid=0)
Nov 13 01:22:26 myhost sshd[2444092]: Received disconnect from 192.168.3.2 port 50632:11: disconnected by user
Nov 13 01:22:26 myhost sshd[2444092]: Disconnected from user user1 192.168.3.2 port 50632
```

```bash:/var/log/auth.log の Accepted な fingerprint を表示
$ head /var/log/auth.log | awk '/Accepted/&&$0=$NF'
SHA256:ID3jU1U/A11prLccA76POqNAq4sW/wflswuZkRbllu1
SHA256:ID3jU1U/A11prLccA76POqNAq4sW/wflswuZkRbllu1
```