【bash】サーバ作業時のヒューマンエラーを防ぐ

# シェルスクリプトをもっと有効活用しよう
「これからどんなプログラミング言語を学ぶべきか？」という命題はいつの時代もみんなの心を悩ませますね。Python は人気急上昇ですし、Java を愛好する人もまだまだ多いです。”いや、C言語はまず押さえておくべきだ” という意見も根強く聞かれます。
みんなそれぞれ、納得できるんですが、最初の言語として僕が初心者の方に敢えて勧めるのはシェル、例えば bash です。awk や　sed を使えば、コマンドラインでもそこそこの事ができますし、Linux なら設定スクリプトは bash ですから読めるとかなり役立ちます。ちょっとした事なら、Python を使うよりずっと効率的で分かりやすかったりもします。
# 例えばどんな風に使う？
サーバを運用する上で、コマンドラインで作業をすることが多いです。特に注意が必要なのは、実行中のプロセスを終了する作業です。こういう場合は ps コマンドでプロセスIDを調べて、kill コマンドで終了、ということになるんですが、何度もやってると精神的にきついです。特に夜中、草木も眠る丑三つ時あたりになると頭も心も疲れてきて、つい間違ったプロセスIDを kill してしまう、などということになって、目も当てられない状況に追い込まれてしまうと大変です・・・。
こういう場合、ちょっとしたスクリプトを書いて作業を簡素化しておくと、楽になる上 間違いも無くなって得した気分になれます。
# コマンドラインから１行でプロセスを終了させる例
スクリプトを書く前に、実行中のプロセスのIDを取得してkillする、という流れをみてみます。
- 実行環境<br> 
`MacOS 11.0.1`
`GNU bash, version 3.2.57(1)-release`
`awk version 20200816`

ここで、自分で作った "stupid" というプログラムを終了させる場合を考えます。
psコマンドで引数を -ef としてプロセスID を調べ、kill することになります。
まず、`ps -ef` の振る舞いを調べてみます。head で最初の２行を表示してみます。

```bash:
ps -ef | head -n 2 
  UID   PID  PPID   C STIME   TTY           TIME CMD
    0     1     0   0  7:33PM ??         0:30.22 /sbin/launchd
```
最初の項目がユーザID、2番目の項目がプロセスID、最後の項目が実行ファイル名だと分かりますね。
運用者のユーザIDで走っている実行ファイルのプロセスIDを指定して kill すれば良いわけです。
\# 他のユーザが同名で実行しているプロセスをkillしないよう注意が必要です。

例えば、以下のような感じで対象のプロセスIDを調べてみましょう。ちなみに bashの環境変数 $UID に自分のユーザIDが格納されているので、これを利用してみます。

```
ps -ef | awk '{if ($1 == '${UID}') {print $0} | grep stupid | grep -v grep 
```
やってることは、
① ps -ef でプロセス一覧を表示
② awk を使って最初の項目が自分のユーザIDと同じ行を取り出す
　　＃ bash の変数と awk内の変数は区別する必要があるため、表記法に注意[
&emsp;&emsp;&emsp;参考：shellスクリプト変数をawkに渡す](https://qiita.com/shibu_t/items/f09dd63236474d2f5d27)
③ grep で、終了対象の実行ファイルの名前の行を取り出す
③’（ grep -v で、③ で一緒に抽出されてしまう自分自身を除外する）

これで当該プロセスに関する行を抽出できます。出力結果はこんな感じ。

```bash:
  502  4225  2319   0 12:23PM ttys002    0:00.00  ./stupid
```
ここで、２番目の項目がプロセスIDでしたね。この場合は 4225 です。この部分を取り出して kill すれば良いですね。awk で取り出します。

```
ps -ef | awk '{if ($1 == '${UID}') {print $0} | grep stupid | grep -v grep | awk '{print $2}'
```

この実行結果で表示されるプロセスIDをkillすれば良いわけです。次はここを自動化します。

bash では、（一連の）コマンドをバックスラッシュで囲むと、その結果を返してくれます。よって、プロセスID を間違えず kill するには、以下の通りです。１行でできますね。

```
kill `ps -ef | awk '{if ($1 == '${UID}') { print $0 }}' | grep $1 | grep -v grep | awk '{print $2}'`
```

# 指定した名前のプロセスを kill するスクリプト
ただし、都度これを打ち込むのはかえって面倒です。この流れをうまくスクリプトにして、あとは例外処理、エラー処理などを施しておきます。これで後々楽になるしミスも減ります。

ここでは、kill すべきプロセスの名前を引数にとって実行し、以下の流れで処理していきます。実際に kill が発動するのは ⑤ だけで、あとは例外処理です。面倒ですが、一度作っておけば、何度でも使えるので、よしとしてください・・・

① 引数の個数が１つでなければ、使用法を表示し終了。
② 引数で指定した名前のプロセスが2つ以上あればそれを表示して終了
③ 引数で指定した名前のプロセスが存在しなければ、その旨表示して終了
④ 引数で指定した名前のプロセスの概要を表示し、killするか確認
⑤ ’y'キーが押されれば kill を実行
⑥ その他のキーが押されれば、何もせず終了

ソースは以下のような感じです。

```bash:killprocess.sh
#!/bin/bash

# 引数の個数が1つでなければ、使用法を表示し終了
if [ "$#" != 1 ];then
    echo
    echo "Usage: $0 processname"
    echo
    exit 1
fi

# ps コマンドで指定の行を取り込み、PSRESULT という変数に格納
PSRESULT=`ps -ef | awk '{if ($1 == '${UID}') { print $0 }}' | grep $1 | grep -v grep |grep -v $0`

# 引数で指定した名前のプロセスがいくつあるか数え、PSNUMという変数に格納
PSNUM=`ps -ef | awk '{if ($1 == '${UID}') { print $0 }}' | grep $1 | grep -v grep |grep -v $0|wc -l`

# 後々のために、psコマンドのヘッダを PSHEADER という変数に格納
PSHEADER=`ps -ef | head -n 1`

# 引数で指定した名前のプロセスが2つ以上あればそれを表示して終了
if [ "$PSNUM" -gt  1 ];then
    echo 
    echo "More than 2 \"$1\" are running as bellow."
    echo
    echo $PSHEADER
    ps -ef | awk '{if ($1 == '${UID}') { print $0 }}' | grep $1 | grep -v grep |grep -v $0
    echo
    echo "You should select one of them."
    echo
    exit 1
fi

# 引数で指定した名前のプロセスが存在しなければ、その旨表示して終了
if [ -n "$PSRESULT" ];then 
    PID=`echo $PSRESULT|awk '{print $2}'`
else
    echo
    echo "No such Process. Cannot kill."
    echo
    exit 1
fi

# 引数で指定した名前のプロセスの概要を表示し、killするか確認
# 'y'キーが押されればプロセスをkill を実行
# その他のキーが押されれば、何もせず終了 
echo
echo  " Do you want to kill the process bellow?"
echo
echo $PSHEADER
echo $PSRESULT
echo
read -p "input (y/n):" input

if [ "$input" = "y" ];then 
    kill $PID
    if [ $? == 0 ];then 
        echo
        echo " \"$1\" is successfully killed"
        echo
        exit 0
    else
        echo
        echo "Failed..."
        echo
        exit 1
    fi

else
    echo
    echo "Did nothing."
    echo
    exit 1
fi

```
こんな感じでしょうか。
実際に "stupid" というプロセスを kill するには、

```
chmod u+x killprocess.sh
./killprocess.sh stupid
```
などとすれば良いでしょう。
うまくいけば、こういう感じで "stupid" を kill することができます。

```
 Do you want to kill the process bellow?

UID PID PPID C STIME TTY TIME CMD
502 5482 2182 0 5:28PM ttys000 0:00.00 stupid

input (y/n):y

 "stupid" is successfully killed

```
セキュリティ上、このスクリプトは作業が済んだらサーバから消しておくことも必要でしょうね。
# まとめ
ssh でサーバに乗り込んで作業する際、十分な注意が必要です。たとえ手順書を作っていても、実際の作業時には想定外のインシデントが起きるのは当然と考えておかないといけません。やはり、できるところは事前にスクリプトを作っておいて、作業の効率化、信頼度向上を図ることはなんだか良いんじゃあないかと思います。