Linux - awkの仕組みと基本的な使い方

# 1.1 awk の仕組みと基本的な使い方

## awk とは
awkは、Aho, Weinberger, Kernighanによって命名された、報告書を生成するためのフォーマット済みテキスト出力ツールです。現在、GNU/LinuxにおけるAWKは自由ソフトウェア基金会（FSF）によって開発・メンテナンスされており、GNU AWKとも呼ばれています。

## AWKのバージョン
- AWK: 元々はAT & Tの研究所から来たAWKです。
- NAWK: New awkの略で、AT & Tの研究室のAWKのアップグレード版です。
- GAWK: GNU AWKの略で、すべてのGNU/Linuxディストリビューションに搭載されています。AWKおよびNAWKと完全に互換があります。

現在、主流のLinuxディストリビューションではGAWKが使用されています。

```shell
[root@rocky86 ~]# ll `which awk`
lrwxrwxrwx. 1 root root 4 Apr 16 05:17 /usr/bin/awk -> gawk
```

### GNU AWKのユーザーマニュアル
- [GNU AWK マニュアル](https://www.gnu.org/software/gawk/manual/gawk.html)
- [オンラインマニュアルページ](https://man7.org/linux/man-pages/man1/awk.1p.html)

### gawk の機能
gawkは以下の機能を提供するパターンスキャンおよび処理言語です。
- テキスト処理
- フォーマットされたテキストレポートの出力
- 算術計算の実行
- 文字列操作

### 形式
```shell
awk [options] 'program' var=value file…
awk [options] -f programfile var=value file…
```
#### よく使用されるオプション
- `-f progfile`, `--file progfile`: ファイルからプログラムを読み込む
- `-F fs`, `--field-separator fs`: 分離子を指定（デフォルトは空白文字）、複数も指定可
- `-v var=val`, `--assign var=val`: 変数を設定

### プログラムの使い方
プログラムは通常、シングルクォートで囲む必要があり、`BEGIN{}` `[pattern]{COMMAND}` `END{}` の3部分から成り立ちます。これらの部分は存在しなくても構いません。
```shell
awk '' /etc/issue
awk 'BEGIN{print "begin"}' /etc/issue
awk '{print $0}' /etc/issue
awk 'END{print "end"}' /etc/issue
awk 'BEGIN{print "begin"}{print $0}END{print "end"}' /etc/issue
```

### プログラム形式
```
pattern{action statements;..}
pattern # 条件を定義し、条件に合致する記録のみが後続のactionを実行
action statements # データを処理する方法（print, printfなどが一般的）
```

### awk の動作プロセス
1. `BEGIN{action;… }`ブロック内のステートメントを実行します。
2. ファイルまたは標準入力(stdin)から一行を読み取り、`pattern{ action;… }`ブロックを実行します。これを最後の行まで繰り返し実行し、ファイルがすべて読み込まれるまで続けます。
3. 入力ストリームの最後に達した時、`END{action;…}`ブロックを実行します。

### 分割子、フィールド、そして記録
ファイルの各行は記録と称され、指定された分割子によって複数のフィールド（列、フィールド）に分割されます。各フィールドは、$1、$2...$Nで示され、$0はその行全体を表します。

# 1.2 awk の変数

awkでは、変数は内蔵されたものとユーザーが定義するカスタム変数の2種類があります。変数の目的は一度定義することで多用途に渡って利用できる点にあります。

## 1.2.1 内蔵変数
awkのよく使われる内蔵変数を以下に示します。

| 変数名     | 意味                                                                    |
|------------|-------------------------------------------------------------------------|
| `FILENAME` | 現在処理しているファイル名                                              |
| `FS`       | フィールド区切り文字、デフォルトは空白文字、`-F`オプションと同じ       |
| `RS`       | レコード区切り文字、ファイル中のレコードを区切る。デフォルトは改行文字  |
| `OFS`      | 出力フィールド区切り文字、デフォルトは空白文字                          |
| `ORS`      | 出力レコード区切り文字、出力時に改行文字を代用                          |
| `OFMT`     | 数値の出力形式、デフォルトは`%.6g`                                     |
| `NF`       | 現在のレコードのフィールド数（列数）                                   |
| `NR`       | 現在までに処理したレコードの総数（行番号）、1から開始                  |
| `FNR`      | 現在のファイル内のレコード数（行番号）、ファイルが変わるたびにリセット |
| `ARGC`     | コマンドライン引数の数                                                 |
| `ARGV`     | コマンドライン引数配列、`ARGV[0]`はawkプログラム名、その後は各引数     |

### 例：FILENAME
以下のコマンドは、FILENAME変数を使用して現在処理しているファイル名を表示します。ただし、BEGINブロック内では値が取得できません。

```shell
# BEGINブロックではFILENAMEは取得できない
[root@rocky86 ~]# awk 'BEGIN{print FILENAME}' /etc/issue
```
```shell
# BEGIN, COMMAND, ENDブロックを通してFILENAMEの値を確認
[root@rocky86 ~]# awk 'BEGIN{print FILENAME}{print "test"}END{print FILENAME}' /etc/issue
test
test
test
test
/etc/issue
```

### 例：FS
フィールド区切り文字として":"を設定し、`/etc/passwd` ファイルから情報を表示します。

```shell
# 変数FSを用いてフィールド区切り文字を設定
[root@rocky86 ~]# awk -v FS=":" 'BEGIN{print FS}{print $1,$1}' /etc/passwd
:
root root
bin bin
daemon daemon
```
```shell
# フィールド区切り文字を出力にも使用
[root@rocky86 ~]# awk -v FS=":" 'BEGIN{print FS}{print $1FS$1}' /etc/passwd
:
root:root
bin:bin
daemon:daemon
```
```shell
# -F オプションを使ってフィールド区切り文字を指定
[root@rocky86 ~]# awk -F: 'BEGIN{print FS}{print $1FS$1}' /etc/passwd
:
root:root
bin:bin
daemon:daemon
```
```shell
# シェル変数からFSを設定
[root@rocky86 ~]# str=":";awk -v FS=$str 'BEGIN{print FS}{print $1FS$1}' /etc/passwd
:
root:root
bin:bin
daemon:daemon
```
```shell
# -F と FS 変数を同時に使用する場合、後者が先行した設定を上書き
[root@rocky86 ~]# awk -v FS=":" -F";" 'BEGIN{print FS}' /etc/passwd
;
[root@rocky86 ~]# awk -F";" -v FS=":" 'BEGIN{print FS}' /etc/passwd
:
```
### 例：RS (レコード区切り文字)
レコードの区切り文字を変更することで、テキストデータの構造を柔軟に解析できます。

```shell
[root@rocky86 ~]# awk -v RS=";" '{print $0}' test.txt 
a b c
1 2 3
x y z
```

### 例：OFS (出力フィールド区切り文字)
出力時のフィールド区切り文字を定義することで、結果のフォーマットをコントロールします。

```shell
[root@rocky86 ~]# awk -v FS=":" -v OFS="---" '{print $1,$3}' /etc/passwd
root---0
bin---1
daemon---2
adm---3
```

### 例：ORS (出力レコード区切り文字)
出力時のレコード区切り文字を設定することで、出力するテキストの構造を変更できます。

```shell
[root@rocky86 ~]# awk -v ORS="---" '{print $0}' test.txt
123---456---789---
```

### 例：OFMT (数値の出力形式)
数値の表示形式を指定することによって、数値データの読みやすさや精度を制御します。

```shell
[root@rocky86 ~]# awk 'BEGIN{PI=3.1415926;print PI;OFMT="%.1g";print PI;OFMT="%.2g";print PI;OFMT="%.8g";print PI;OFMT="%.8f";print PI}'
3.14159
3
3.1
3.1415926
3.14159260
```

### 例：NF (フィールド数)
現在のレコードのフィールド数を取得し、動的なフィールド参照が可能です。

```shell
[root@rocky86 ~]# awk -v FS=":" '{print NF}' /etc/passwd
7
7
7
```

### 例：NR と FNR（レコード番号）
ファイルを横断したレコード番号（NR）とファイル特有のレコード番号（FNR）を利用して、データの流れを追跡します。

```shell
[root@rocky86 ~]# awk '{print NR,FNR,$0}' /etc/issue /etc/redhat-release 
1 1 Welcome to magedu! \d
2 2 \S
3 3 Kernel \r on an \m
4 4
5 1 Rocky Linux release 8.6 (Green Obsidian)
```

### 例：ARGC と ARGV（引数の数と引数配列）
コマンドラインからの入力引数の数と内容を確認し、スクリプトの挙動を条件付きで変更することができます。

```shell
[root@rocky86 ~]# awk 'BEGIN{print ARGC,"---",ARGV[0],"---",ARGV[1],"---",ARGV[2],"---",ARGV[3]}' /etc/issue /etc/os-release
3 --- awk --- /etc/issue --- /etc/os-release ---
```
### 1.3 動作 print

#### 形式
```plaintext
print item1, item2, ...
```

#### 説明
- 項目間にはコンマ（`,`）が置かれ、出力する項目は文字列、数値、現在のレコードのフィールド、変数、またはawkの表現式が可能です。
- 項目を省略すると、`print $0`と同等です。
- 固定文字列はダブルクォーテーション（`" "`）で囲みますが、変数や数値には必要ありません。

#### 例

```shell
# 「hello world」と表示する
[root@rocky86 ~]# awk 'BEGIN{print "hello world"}'
hello world

# 「hello world」を3回表示する
[root@rocky86 ~]# seq 3 | awk '{print "hello world"}'
hello world
hello world
hello world

# 入力された各行を表示する
[root@rocky86 ~]# seq 3 | awk '{print $0}'
1
2
3

# 数値計算の結果を表示する
[root@rocky86 ~]# awk 'BEGIN{print 2*3}'
6

# /etc/issueの内容を表示する
[root@rocky86 ~]# awk '{print }' /etc/issue
Welcom to magedu! \d
\S
Kernel \r on an \m

# 変数とフィールドを組み合わせて表示する
[root@rocky86 ~]# awk -v var1=123 -F: '{print var1,$1,$3}' /etc/passwd
123 root 0
123 bin 1
123 daemon 2
123 adm 3

# タブ区切りで変数とフィールドを表示する
[root@rocky86 ~]# awk -v var1=123 -F: '{print var1,$1"\t"$3}' /etc/passwd
123 root	0
123 bin	1
123 daemon	2
```

# 1.2 例

### 1.2.1「host_list.log」ファイルからドメイン名前のホスト名を抽出してファイルに書き戻す
`.mooreyxia.com`より前にあるホスト名を`host_list.log`ファイルから抽出し、その結果を元のファイルに上書き保存します。

#### 具体的な内容
```
1 www.mooreyxia.com
2 blog.mooreyxia.com
3 study.mooreyxia.com
4 linux.mooreyxia.com
5 python.mooreyxia.com
......
999 study.mooreyxia.com
```

#### 解決策
`awk` を使って、各行を空白で分割し、ドメイン名を取得後、更に `.` で分割することでホスト名（例：`www`）を抽出します。その後、抽出したホスト名を新しいファイルに書き出し、最終的に `mv` コマンドを使って元のファイルと置き換えます。

#### 実行方法

```shell
[root@rocky86 ~]# awk -v x=100 'BEGIN{print x}{print x+100}' /etc/hosts
100
200
200
[root@rocky86 ~]# awk 'BEGIN{print x}{print x+100}' x=200 /etc/hosts
300
300
[root@rocky86 ~]# cat test2.awk 
#!/bin/awk -f
{if($3 >=min && $3<=max)print $1,$3} 
[root@rocky86 ~]# chmod +x test2.awk
[root@rocky86 ~]# ./test2.awk -F: min=100 max=200 /etc/passwd
systemd-resolve 193
rtkit 172
pulse 171
qemu 107
usbmuxd 113
abrt 173
```

### 1.2.2「/etc/fstab」ファイル中の各ファイルシステムタイプの出現回数を統計

#### 目的
「/etc/fstab」ファイルに記載されている各ファイルシステムタイプの出現回数をカウントします。

#### 解決策と手順
ファイル `fstab` 内の各ファイルシステムタイプを識別してカウントするには `awk` コマンドを使用します。このコマンドは、ファイルシステムタイプが記載されている列（3列目）を特定し、それが何回現れるかをカウントします。

#### 実行方法
1. 全てのUUIDで始まる行について3番目の列（ファイルシステムタイプ）をカウントし、その結果を表示する方法：

    ```shell
    [root@rocky86 ~]# awk -F' +' '/^UUID/{fs[$3]++}END{for(i in fs){print i, fs[i]}}' /etc/fstab
    swap 1
    ext4 3
    ```

    このコマンドは、フィールド区切りとして1つ以上のスペースを指定し、各UUIDで始まる行の3番目のフィールド（ファイルシステムタイプ）をカウントします。最終的に、ファイルシステムタイプとその出現回数を表示します。

2. `awk` でファイルシステムタイプを抽出し、その後 `uniq` コマンドを利用して出現回数をカウントする方法：

    ```shell
    [root@rocky86 ~]# awk -F' +' '/^UUID/{print $3}' /etc/fstab | uniq -c
          3 ext4
          1 swap
    ```

    こちらの方法は、まず `awk` を使って各UUID行からファイルシステムタイプを抽出し、その出力を `uniq -c` にパイプします。`uniq -c` は隣接する行が同じ場合にカウントを行うため、並びが保証されている必要があります。この例では元のファイルがファイルシステムタイプによって順不同でないことが前提です。

どちらの方法も `/etc/fstab` において各ファイルシステムタイプの使用頻度を把握するのに有効です。

### 1.2.3 文字列 Yd$C@M05MB%9&Bdh7dq+YVixp3vpw 内のすべての数値を抽出
```shell
[root@rocky86 ~]# echo 'Yd$C@M05MB%9&Bdh7dq+YVixp3vpw' | awk '{gsub(/[^0-9]/,"");print $0}'
05973
[root@rocky86 ~]# echo 'Yd$C@M05MB%9&Bdh7dq+YVixp3vpw' |awk -F "" '{for(i=1;i<=NF;i++){if ($i ~ /[[:digit:]]/){str=$i; str1=(str1 str)}};print 
str1}'
[root@rocky86 ~]# echo 'Yd$C@M05MB%9&Bdh7dq+YVixp3vpw' | awk -F'[^0-9]' '{for(i=1;i<=NF;i++){printf "%s",$i }}'
05973
```

### 1.2.4 ファイルrandom.txtには、合計5000個のランダムな整数が記録されています。保存形式は100、50、35、89...の中で最大の整数と最小の整数を取り出し
 ```shell
[root@rocky86 ~]# str="";for((i=1;i<=5000;i++));do if [ $i -ne 5000 ];then 
str+="$RANDOM,";else str+=$RANDOM;fi;done;echo $str > random.txt

[root@rocky86 ~]# awk -F, '{max=$1;min=$1;for(i=1;i<=NF;i++){if($i>max)
{max=$i}else{if($i<min){min=$i}}}}END{print "最大值："max,"最小值："min}' 
random.txt
```

### 1.2.5 特定の IP の同時接続数が 100 を超えたときに監視し、ファイアウォール コマンドを呼び出して対応する IP をブロックし、頻度を 5 分ごとに監視します。 ファイアウォール コマンドは次のとおりです。 iptables -A INPUT -s IP -j REJECT
 ```shell
[root@rocky86 ~]# ss -nt | awk -F " +|:" 'NR!=1{ip[$(NF-2)]++}END{for(i in ip)
{if(ip[i]>100){system("iptables -A INPUT -s "i" -j REJECT")}}}'
```