awkにハマってしまったかもしれない件

# はじめに


こんばんは！

[MYJLab Advent Calendar 2023](https://qiita.com/advent-calendar/2023/myjlab)の11日目を担当するセイです！

今夜じゃねーよ！って思う人もいると思いますが、今書いてる現時点は夜だからこんばんはなんだよ。

最近は暇になってもおかしくは無い時期なんですが、いくらやってもタスクが終わらない毎日を送りつけています。

自分に「お前毎日毎日何やってこんなに忙しいんの」 問いかけ続けていますが、 何かはやっているはずです。

毎日行ったタスクを可視化できるツールがあったらいいなぁと思います。

などと、ダラダラ愚痴を吐いてきましたが、今回のテーマもこれと全く関係ないです。

# 今回のテーマ


突然ですが、皆さんCLI(コマンドラインインターフェイス、Command Line Interface)は使っていますか？ Macって言うとターミナルですね。

CLIの操作を簡単にするために、たくさんのツールが 作られています。

CLIを使いこなせるとすごく便利でかっこいいですが、なかなか習得するのにコストがかかります。

自分も、このCLIツールの使い方、覚えたら絶対便利だけどめんどくさそうて言って逃げ続けていました。

`awk`がまさにその一つでした。

でも、最近は宮治先生のTAがきっかけで`awk`にハマったのも、自分の使用例をもとに 紹介しようと思います！

# 基礎編 ~今回出てくるコマンドも軽く紹介~

## awk

今回のメインですね！

AWK（オーク）は、プログラミング言語の一つ。 テキストファイル、特に空白類（スペースの他、タブなど）やカンマなどで区切られたデータファイルの処理を念頭に置いた仕様となっています。(Wikipediaより)

そう、実を言うと、awkはプログラミング言語なんです。

特に、CLIでの入力に対して、こみいった処理ができます！

awkはパイプラインで渡された入力を処理して出力の使われ方が多いので、このセクションでは、macのホームディレクトリで`ls -l`コマンドの出力を渡して処理してみます。

パイプラインとか、標準入力とか、標準出力とかの名詞もちょろっと説明しようと思ったが、余裕がなかったので、分からない人は調べてね。

```bash
❯ ls -l
total 80
drwx------@   6 chengchen  staff    192 Nov 10 17:12 Applications
drwx------@   4 chengchen  staff    128 Nov 24 21:24 Desktop
drwx------+  10 chengchen  staff    320 Oct  8 14:39 Documents
drwx------@ 212 chengchen  staff   6784 Dec  9 17:44 Downloads
drwx------@ 117 chengchen  staff   3744 Dec  7 18:05 Library
drwx------+  20 chengchen  staff    640 Oct  8 15:54 Movies
drwx------+   5 chengchen  staff    160 Oct 24  2022 Music
drwx------+   5 chengchen  staff    160 Oct 24  2022 Pictures
drwxr-xr-x+   4 chengchen  staff    128 Feb 20  2022 Public
drwxr-xr-x+   3 chengchen  staff     96 Mar 11  2022 Sites
drwxr-xr-x@   5 chengchen  staff    160 Nov 24 12:38 important
drwxr-xr-x    3 chengchen  staff     96 Nov  8 18:26 scikit_learn_data
-rw-r--r--@   1 chengchen  staff  39693 Dec  8 13:19 vs-picgo-data.json
```


### Hello, world!

プログラミング言語なので、まずはHello, world!を出力してみましょう。

```bash 
❯ ls -l | awk '{print "Hello, world!"}'
Hello, world!
Hello, world!
Hello, world!
Hello, world!
Hello, world!
Hello, world!
Hello, world!
Hello, world!
Hello, world!
Hello, world!
Hello, world!
Hello, world!
Hello, world!
Hello, world!
```

awkの基本的な構文は、`{}`で囲まれた中に、`条件 {処理}`という形で書きます。

渡すパラメータは、がならずシングルクォーテーション、`''`で囲みましょう。理由は今回は割愛しますが、ダブルクォーテーションで囲むと挙動がおかしくなります。

構文として、`print␣"文字列"`と書くと、文字列を出力します。

何回も出力されているのは、`awk`はデフォルトで、入力を行ごとに処理して出力するからです。


### NR

awkには、`NR`という特殊変数があります。

`NR`は、現在の行番号を表します。

たとえば、`print␣NR,"Hello, world!"`の構文を使うと、行番号と一緒に出力できます。

`print`は、`,`で区切られた引数をスペースで区切って出力する。

```bash
❯ ls -l | awk '{print NR,"Hello, world!"}'
1 Hello, world!
2 Hello, world!
3 Hello, world!
4 Hello, world!
5 Hello, world!
6 Hello, world!
7 Hello, world!
8 Hello, world!
9 Hello, world!
10 Hello, world!
11 Hello, world!
12 Hello, world!
13 Hello, world!
14 Hello, world!
```

### NF, $0, $1 ...

awkには、`NF`という特殊変数があります。

`NF`は、現在の行のフィールド数を表します。

フィールドというのは、行をスペースで区切ったもののことです。

例えば、以下の例だと`total 80`の行はフィールド数が2で、

`drwx------@   6 chengchen  staff    192 Nov 10 17:12 Applications`の行はフィールド数が9です。

```bash
❯ ls -l | awk '{print NF, $0}'
2 total 80
9 drwx------@   6 chengchen  staff    192 Nov 10 17:12 Applications
9 drwx------@   4 chengchen  staff    128 Nov 24 21:24 Desktop
9 drwx------+  10 chengchen  staff    320 Oct  8 14:39 Documents
9 drwx------@ 212 chengchen  staff   6784 Dec  9 17:44 Downloads
9 drwx------@ 117 chengchen  staff   3744 Dec  7 18:05 Library
9 drwx------+  20 chengchen  staff    640 Oct  8 15:54 Movies
9 drwx------+   5 chengchen  staff    160 Oct 24  2022 Music
9 drwx------+   5 chengchen  staff    160 Oct 24  2022 Pictures
9 drwxr-xr-x+   4 chengchen  staff    128 Feb 20  2022 Public
9 drwxr-xr-x+   3 chengchen  staff     96 Mar 11  2022 Sites
9 drwxr-xr-x@   5 chengchen  staff    160 Nov 24 12:38 important
9 drwxr-xr-x    3 chengchen  staff     96 Nov  8 18:26 scikit_learn_data
9 -rw-r--r--@   1 chengchen  staff  39693 Dec  8 13:19 vs-picgo-data.json
```

上のコマンドには、`$0`という特殊変数がありますが、

`$0`は、現在の行全体を表します。

`$1`は、現在の行の1番目のフィールドを表します。

`$2`は、現在の行の2番目のフィールドを表します。

`$3`は、現在の行の3番目のフィールドを表します。

...

例えば、

```bash 
❯ ls -l | awk '{print $1}'
total
drwx------@
drwx------@
drwx------+
drwx------@
drwx------@
drwx------+
drwx------+
drwx------+
drwxr-xr-x+
drwxr-xr-x+
drwxr-xr-x@
drwxr-xr-x
-rw-r--r--@
❯ ls -l | awk '{print $6}'

Nov
Nov
Oct
Dec
Dec
Oct
Oct
Oct
Feb
Mar
Nov
Nov
Dec
```

1行の最後のフィールドを出力するには、`$NF`と書きます。

```bash
❯ ls -l | awk '{print $NF}'
80
Applications
Desktop
Documents
Downloads
Library
Movies
Music
Pictures
Public
Sites
important
scikit_learn_data
vs-picgo-data.json
```

後ろから2番目のフィールドを出力するには、`$(NF-1)`と書きます。

```bash
❯ ls -l | awk '{print $(NF-1)}'
total
17:12
21:24
14:39
17:44
18:05
15:54
2022
2022
2022
2022
12:38
18:26
13:19
```

### 条件指定
`ls -l`みたいのコマンドは、1行目の`total 80`とかは邪魔だったりしますので、それを除外することができます。

```bash
❯ ls -l | awk 'NR>1{print NR,$0}'
2 drwx------@   6 chengchen  staff    192 Nov 10 17:12 Applications
3 drwx------@   4 chengchen  staff    128 Nov 24 21:24 Desktop
4 drwx------+  10 chengchen  staff    320 Oct  8 14:39 Documents
5 drwx------@ 212 chengchen  staff   6784 Dec  9 17:44 Downloads
6 drwx------@ 117 chengchen  staff   3744 Dec  7 18:05 Library
7 drwx------+  20 chengchen  staff    640 Oct  8 15:54 Movies
8 drwx------+   5 chengchen  staff    160 Oct 24  2022 Music
9 drwx------+   5 chengchen  staff    160 Oct 24  2022 Pictures
10 drwxr-xr-x+   4 chengchen  staff    128 Feb 20  2022 Public
11 drwxr-xr-x+   3 chengchen  staff     96 Mar 11  2022 Sites
12 drwxr-xr-x@   5 chengchen  staff    160 Nov 24 12:38 important
13 drwxr-xr-x    3 chengchen  staff     96 Nov  8 18:26 scikit_learn_data
14 -rw-r--r--@   1 chengchen  staff  39693 Dec  8 13:19 vs-picgo-data.json
```



### printf

awkには、`printf`という関数があります。

他の言語のformatted print機能を同じだったりします。

たとえば、`printf`を使って、`ls -l`の出力を整形してみましょう。

```bash
❯ ls -l | awk 'NR>1{printf "%02d %s\n",NR,$0}'
02 drwx------@   6 chengchen  staff    192 Nov 10 17:12 Applications
03 drwx------@   4 chengchen  staff    128 Nov 24 21:24 Desktop
04 drwx------+  10 chengchen  staff    320 Oct  8 14:39 Documents
05 drwx------@ 212 chengchen  staff   6784 Dec  9 17:44 Downloads
06 drwx------@ 117 chengchen  staff   3744 Dec  7 18:05 Library
07 drwx------+  20 chengchen  staff    640 Oct  8 15:54 Movies
08 drwx------+   5 chengchen  staff    160 Oct 24  2022 Music
09 drwx------+   5 chengchen  staff    160 Oct 24  2022 Pictures
10 drwxr-xr-x+   4 chengchen  staff    128 Feb 20  2022 Public
11 drwxr-xr-x+   3 chengchen  staff     96 Mar 11  2022 Sites
12 drwxr-xr-x@   5 chengchen  staff    160 Nov 24 12:38 important
13 drwxr-xr-x    3 chengchen  staff     96 Nov  8 18:26 scikit_learn_data
14 -rw-r--r--@   1 chengchen  staff  39693 Dec  8 13:19 vs-picgo-data.json
```

`%02d␣%s\n`は、2桁の数字を0埋めして出力して、スペースを空けて、文字列を出力して、改行するという意味です。

`printf`はデフォルトで最後に改行を入れないので、`\n`を入れて改行しています。


### `awk`の最後に

`awk`はちゃんとしてプログラミング言語なので、一般の言語ように実はif文for文や各種組み込み関数もありますので、いくらでもはまれます。



## ps

`ps`コマンドは、実行中のプロセスのリストを表示するLinuxコマンドです。(ChatGPTより)

**例：**

我が家のopenwrtルータにpsを実行すると以下のようになる。

150行ぐらい出てくるのでこれは一部です。

```bash
❯ ps
  PID USER       VSZ STAT COMMAND
 2454 logd       992 S    /sbin/logd -S 64
 2508 root      1464 S    /sbin/rpcd -s /var/run/ubus/ubus.sock -t 30
 2581 root      3688 S    /usr/sbin/snmpd -Lf /dev/null -f -r
 3024 root      1376 S    /sbin/netifd
 3091 root      1100 S    /usr/sbin/odhcpd
 3286 root      4764 S    /usr/sbin/uhttpd -f -h /www -r OpenWrt -x /cgi-bin -u /ubus -t 3600 -T 30 -k 20 -A 1 -n 50 -N 100 -R -p 0.0.0.0:5000 -p [::]:50
 ...
```


## gerp

`grep`コマンドは、ファイル内で特定の文字列を検索するためのコマンドです。(ChatGPTより)

**例：**

さっきのpsコマンドを出力に、"docker"の文字列を探したい時に`ps | grep docker` を使うとdockerを含む行しか出力されなくなる。

```bash
❯ ps | grep docker
 6428 root      738m S    /usr/bin/dockerd --config-file=/tmp/dockerd/daemon.json
 6785 root      740m S    containerd --config /var/run/docker/containerd/containerd.toml --log-level warn
14278 root      688m S    /usr/bin/docker-proxy -proto tcp -host-ip 0.0.0.0 -host-port 23333 -container-ip 172.19.0.2 -container-port 80
14287 root      688m S    /usr/bin/docker-proxy -proto tcp -host-ip :: -host-port 23333 -container-ip 172.19.0.2 -container-port 80
29210 root      1120 R    grep docker
```


## kill

killコマンドは、プロセスを終了させるためのコマンドです。特定のプロセスID（PID）を使ってプロセスを終了します。例えば、kill 1234はPIDが1234のプロセスを終了させます。(ChatGPTより)
厳密にいうと、プロセスにシグナル(指令)を送るコマンドです。PIDはpsコマンドとかで調べられます。

**例：**

上の例で、dockerdのプロセスを終了させたい時に`kill 6428`を実行すると、dockerのプロセスが終了します。
(dockerのdaemonプロセスを終了する方法としてとても推奨はできませんが)

```bash
❯ kill 6428
(正しく終了した場合は何も出力されない)
```

`-9`オプションをつけると、強制終了します。

```bash
❯ kill -9 6428
(正しく終了した場合は何も出力されない)
```

## xargs

xargsコマンドは、標準入力からデータを読み取り、それを引数として他のコマンドに渡すために使用されます。これにより、一連のデータを一度に複数のコマンドに渡して処理することができます。例えば、`echo "1 2 3" | xargs touch`は、ファイル1, 2, 3を作成します。(ChatGPTより)

**例：**
```bash
❯ ls -l
total 0 (ファイルがない)
❯ echo "1 2 3" | xargs touch
❯ ls -l
total 0
-rw-r--r--  1 chengchen  staff  0 Dec 12 13:35 1
-rw-r--r--  1 chengchen  staff  0 Dec 12 13:35 2
-rw-r--r--  1 chengchen  staff  0 Dec 12 13:35 3
```


# 応用編

ここでは、僕が実際にawkを使っている場面を紹介します。

## 応用1: awkを使って、特定のプロセスを終了させる

### シチュエーション
僕は自分のopenwrtルータに、vscodeのremote-sshで接続して設定をいじったっりすることがあります。

vscodeのremote-sshで接続していると、vscode-serverというプロセスがルータ上で立ち上がります。

普通なら、remote-sshを終了すれば、vscode-serverも終了しますが、このルータの環境では、vscodeを終了してもvscode-serverが終了せずに、プロセスがずっと残り続けて、非常にcpuリソースを食いつぶします。

なので、vscodeプロセスを強制終了させたいです。

### 解決策
そのためのコマンドが

```bash
❯ ps | grep /root/.vscode | awk '{print $1}' | xargs kill -9
```

流れは、psでvscodeのプロセスを見つけて、PIDがけを抽出して、kill -9で強制終了させます。

まずは`ps | grep /root/.vscode`でvscodeのプロセスを見つけます。

```bash
❯ ps | grep /root/.vscode
21572 root      1120 S    sh /root/.vscode-server/bin/af28b32d7e553898b2a91af498b1fb666fdebe0c/bin/code-server --start-server --host
21582 root      292m S    /root/.vscode-server/bin/af28b32d7e553898b2a91af498b1fb666fdebe0c/node /root/.vscode-server/bin/af28b32d7e
22386 root      304m R    /root/.vscode-server/bin/af28b32d7e553898b2a91af498b1fb666fdebe0c/node --dns-result-order=ipv4first /root/
22397 root      255m S    /root/.vscode-server/bin/af28b32d7e553898b2a91af498b1fb666fdebe0c/node /root/.vscode-server/bin/af28b32d7e
22429 root      1120 S    grep /root/.vscode
```

次に`awk 'print $1'`で1番目でのフィールドのPIDを取り出す。

```bash
ps | grep /root/.vscode | awk '{print $1}'
21572
21582
22386
22397
22447
```

最後に、`xargs`で得られたPIDを全部`kill`に渡せて、vscodeのプロセスを終了させることができます。

## 応用2: awkを使って、ファイル名を一括で変更する
### シチュエーション

僕はこんな感じに、毎月TASAの勤務表を作成する必要がある。

```bash
❯ tree
.
└── 2023.11
    ├── SA勤務報告書_インフラ_11月.xlsx
    ├── TA勤務報告書_インフラ_11月.xlsx
    ├── TA勤務報告書_情報科学総合演習B_11月.xlsx
    └── TA勤務報告書_機械学習_11月.xlsx

2 directories, 4 files
```

これを毎月毎月ファイル名を変更するのはめんどくさいので、一括で変更したい。

### 解決策

そのためのコマンドが

```bash
ls | awk '{ print $0; sub("11", "12"); print $0}' | xargs -n2 mv
```

流れは、`ls`でファイル名を取得して、`awk`で元ファイル名と新しいファイル名をprintして、`xargs`で`mv`に渡して、ファイル名を変更します。

まずは`ls`でファイル名を取得します。

```bash
❯ ls
SA勤務報告書_インフラ_11月.xlsx                        TA勤務報告書_情報科学総合演習B_11月.xlsx
TA勤務報告書_インフラ_11月.xlsx                        TA勤務報告書_機械学習_11月.xlsx
```

次に、`awk '{ print $0; sub("11", "12"); print $0}'`で元ファイル名と新しいファイル名をprintします。

```bash
❯ ls | awk '{ print $0; sub("11", "12"); print $0}'
SA勤務報告書_インフラ_11月.xlsx
SA勤務報告書_インフラ_12月.xlsx
TA勤務報告書_インフラ_11月.xlsx
TA勤務報告書_インフラ_12月.xlsx
TA勤務報告書_情報科学総合演習B_11月.xlsx
TA勤務報告書_情報科学総合演習B_12月.xlsx
TA勤務報告書_機械学習_11月.xlsx
TA勤務報告書_機械学習_12月.xlsx
```

ここの文法を説明すると、複数のコマンドを実行する時は、`;`で区切ります。最初に`print $0`でファイル名を出力して、`sub("11", "12")`でファイル名(`$0`)の11を12に置換して、`print $0`で新しいファイル名を出力します。


次は、`xargs -n2 mv`で、`mv`に渡して、ファイル名を変更しますが、`-n2`のオプションを説明すると、`xargs`はデフォルトで、渡された引数を1つずつ渡しますが、`-n2`をつけると、2つずつ渡します。

つまり、こうなる
  
```bash
❯ ls | awk '{ print $0; sub("11", "12"); print $0}' | xargs -n2 echo
SA勤務報告書_インフラ_11月.xlsx SA勤務報告書_インフラ_12月.xlsx
TA勤務報告書_インフラ_11月.xlsx TA勤務報告書_インフラ_12月.xlsx
TA勤務報告書_情報科学総合演習B_11月.xlsx TA勤務報告書_情報科学総合演習B_12月.xlsx
TA勤務報告書_機械学習_11月.xlsx TA勤務報告書_機械学習_12月.xlsx
```

以上にように、受けたパラメータに渡し`mv`で実行すると、ファイル名が変更されます。

# 最後に

awkは、CLIのツールの中でも、かなり強力なツールです。

こうやって少しつづ使い方を覚えていくと、つよつよエンジニアの道を歩めるかもしれません。

では皆だん、良いCLIライフを！