これだけ覚えておけばOK！シェルスクリプトで冪等性を担保するためのTips集

「シェルスクリプトって冪等性ないじゃないですか」
そんなことをよく耳にします。しかし、**if文を入れるだけ**で**簡単に冪等性を担保したシェルスクリプト**を書くことができます。
今回は、私が使う条件式を紹介します。基本的な形式なものなので、組み合わせると幅が広がりますよっ！

# ファイル/フォルダ関係
### ファイルの存在確認

* `-e 'ファイル名'で`「ファイル名」が**存在しているかチェック**します。

```sh
if [ -e 'ファイル名' ]; then
  # ファイル/フォルダが存在していれば、ファイル/フォルダを削除するなど
  rm ファイル名
fi
```

* 先頭に`!`をつけることで、**存在していないことをチェック**します。

```sh
if [ ! -e 'ファイル名' ]; then
  # そのファイル/フォルダが存在していなければ、ファイル/フォルダを作成するなど
  touch ファイル名
fi
```

### ファイル内の特定文字列の存在確認

* grepコマンドを使って、ファイルに特定の文字列が含まれていることを確認します。

```sh
if [ grep -q '検索文字列' ファイル名 ]; then
  # 検索文字列が含まれていれば、処理を行う（置換処理とか）
fi
```

* 先頭に`!`（否定）をつけることで存在していないことをチェックします。

```sh
if [ ! grep -q '検索文字列' ファイル名 ]; then
  # 検索文字列が含まれていなければ、処理を行う（追記処理とか）
fi
```

# インストール系
## インストール済みか確認（１）
* コマンドを実行し、実行結果`$?`が`127`を返すことを確認する。 

`$?`は、直前に実行したコマンドのExit Codeが格納されています。
また、Exit Code`127`は**command not found**を表します。

```sh
コマンド名 -v > /dev/null 2>&1　# コマンド名 --versionなど 
if [ $? -eq 127 ]; then
  # コマンドをインストールする処理とか
fi
```

※コマンドによって、`-v`/`--version`が使えないものがあります。（`--help`など変更を加えないコマンドを実行することをオススメします。）

`> /dev/null 2>&1 `は、コンソールには結果が表示されなくなります。
エラーメッセージを表示する必要もないので、ここでは握りつぶします。

* コマンドを実行し、実行結果`$?`が`0`を返すことを確認します。

Exit Code`0`は正常に終了したことを表します。

```sh
コマンド名 -v > /dev/null 2>&1　# コマンド名 --versionなど 
if [ $? -eq 0 ]; then
  # コマンドをアンインストールする処理とか、依存ライブラリをインストールするとか・・・
fi
```

## インストール済みか確認（２）

* コマンドのバイナリファイルが存在しているか確認する。

```sh
if [ -e 'コマンドの場所' ]; then #sshコマンドであれば、/usr/bin/ssh
  # コマンドをインストールする処理とか
fi
```

インストールした状態で`which コマンド名`を実行すると、どこに存在しているか表示されます。

```sh
$ which コマンド名

# 例
$ which ssh
===========
/usr/bin/ssh
===========
```


* また、先頭に`!`をつけることで、**存在していないことをチェック**します。

```sh
if [ ! -e 'コマンドの場所' ]; then
  # コマンドをアンインストールする処理とか、依存ライブラリをインストールするとか・・・
fi
```

# インストール済みか確認（３）
* コマンドのタイプを調べる。
紹介した方法の中で**最もスマートな方法**です。

`type`コマンドを利用し、コマンドに関する情報を表示します。

```sh
if type docker >/dev/null 2>&1; then
  # コマンドをアンインストールする処理とか、依存ライブラリをインストールするとか・・・
fi
```

* 先頭に`!`をつけることで、**存在していないことをチェック**します。

```sh:
if ! type docker >/dev/null 2>&1; then
 # コマンドをインストールする処理とか
fi
```

~~個人的には、インストール済みか確認（１）方法をオススメします。~~
個人的には、**インストール済みか確認（３）をオススメ**します。
各コマンドのオプションを知る必要もなく、アップデートによってインストール場所が変化した場合も調べることができます。

インストール済みか確認（１）では、コメントで頂いた通り、コマンドのオプションを知る必要があります。
また、インストール済みか確認（2）では、コマンド毎にどこにインストールされるか確認しなければなりません。
さらに、アップデートによってインストール場所が変化する可能性もあります。



# 特定のサービス
### SSHの成功/失敗確認

* SSHが成功したか確認を行う

`$?`が255を返す時、SSHが失敗した場合です。
つまり、`$?`が`255`でない時が成功となります。

```sh
ssh x.x.x.x > /dev/null 2>&1
if [ $? -ne 255 ]; then    # 失敗の場合 if [ $? -eq 255 ]; then
   # sshが成功/失敗した時の処理
fi
```

### mysqlに特定のデータが含まれているか確認

```sh
# 例
# mysql -u foo -p bar -e "show databases\G" | grep development > /dev/null 2>&1
mysql -u ユーザ名 -p パスワード -e "SQL\G" | grep 検索したい文字列 > /dev/null 2>&1
if [ $? -ne 0 ]; then  # 含まれていない場合 if [ $? -eq 0 ]; then
  # db:migrateしてみたり、特定のデータ入れてみたり・・・削除してみたり
fi
```

### gitで編集中のファイルが存在するか確認

* `git diff --exit-code --quiet`コマンドを使って、編集中のファイルの存在確認をします。


`git diff`とは、ワーキングツリーとインデックスの差分を表示するコマンドです。
`--exit-code`オプションは、差分が見つかった場合に終了コード1を返し、`--quiet`オプションは、差分非表示にします。
つまり、差分を表示せず、Exit Codeだけ返します。

* 0: 正常終了
* 1: 異常終了

```sh:
git diff --exit-code --quiet
if [ $? -eq 1 ]; then
  # 編集中のファイルが存在していた時の処理
  git stash   # 例
fi
```

* 編集中のファイルが存在していなければ処理を行う

```sh
if [ $? -eq 0 ]; then   # または if [ $? -ne 1 ]; then
  # 編集中のファイルが存在しない時の処理
fi
```

# プロセス
### プロセス起動確認（１）

* プロセスが動いていることを確認する

```sh
ps aux | grep [m]ysql # 確認したいプロセス名
if [ $? -eq 0 ]; then
  # プロセスの停止など
fi
```

* プロセスが停止していることを確認する

```sh
ps aux | grep [m]ysql # 確認したいプロセス名
if [ $? -ne 0 ]; then
  # プロセスの起動など
fi
```

プロセスを確認する際に`[m]ysql`としているのは、grepをしているプロセスを除くためです。
では、実際に比較してみましょう。

```sh
$ ps aux | grep mysql
=====================
user           33003   0.0  0.0  2435864    784 s000  S+    9:50AM   0:00.00 grep mysql      ← grepをしているプロセス（mysqlのプロセスではないので不要）
user           32995   0.0  2.7  3086248 458860 s000  S     9:50AM   0:00.49 /usr/local/Cellar/mysql56/5.6.29/bin/mysqld --basedir=/usr/local/Cellar/mysql56/5.6.29 --datadir=/usr/local/var/mysql --plugin-dir=/usr/local/Cellar/mysql56/5.6.29/lib/plugin --log-error=/usr/local/var/mysql/L2753.local.err --pid-file=/usr/local/var/mysql/L2753.local.pid
user           32899   0.0  0.0  2446700   1284 s000  S     9:50AM   0:00.02 /bin/sh /usr/local/Cellar/mysql56/5.6.29/bin/mysqld_safe --datadir=/usr/local/var/mysql --pid-file=/usr/local/var/mysql/L2753.local.pid
=====================

$ ps aux | grep [m]ysql
=====================
user           32995   0.0  2.7  3086248 458860 s000  S     9:50AM   0:00.50 /usr/local/Cellar/mysql56/5.6.29/bin/mysqld --basedir=/usr/local/Cellar/mysql56/5.6.29 --datadir=/usr/local/var/mysql --plugin-dir=/usr/local/Cellar/mysql56/5.6.29/lib/plugin --log-error=/usr/local/var/mysql/L2753.local.err --pid-file=/usr/local/var/mysql/L2753.local.pid
user           32899   0.0  0.0  2446700   1284 s000  S     9:50AM   0:00.02 /bin/sh /usr/local/Cellar/mysql56/5.6.29/bin/mysqld_safe --datadir=/usr/local/var/mysql --pid-file=/usr/local/var/mysql/L2753.local.pid
=====================
```

こうすることで実際に動いているプロセスのみ抽出することができます。

また、上記の例は`grep`の実装に依存しているため、もし使用できない場合は以下のコマンドを利用してください。

```sh
$ ps -ef | grep mysql | grep -v grep
```


### プロセス起動確認（2）

* プロセスが動いていることを確認する

serviceコマンドのステータス確認を実行した際に`$?`が0の時、プロセスが動いていることを表します。

```sh
sudo service mysql status > /dev/null 2>&1
if [ $? -eq 0 ]; then
  # プロセスの停止など
fi
```

* プロセスが停止していることを確認する

また、`$?`が3のとき停止していることを表します。

```sh
sudo service mysql status > /dev/null 2>&1
if [ $? -eq 3 ]; then
  # プロセスの起動
fi
```

ここら辺はまぁお好みで・・・。
私は、プロセスを見ることが多いです。

# その他
### 何かの起動を待つ

* 起動が終わるまで、次の処理ができない時に使う小技です。

```sh
# 例：dockerのmysqlが起動するまで待つ
while :
do
  docker-compose ps | grep mysql
  if [ $? -eq 0 ]; then
    break  # 起動したら抜ける
  fi
  echo mysqlのdockerコンテナが起動するまで待ちます（30秒）
  sleep 30
done
```

### インストールが成功するまで、リトライする

* ネットワークやサーバが不安定でインストールが失敗しやすい時に使う。

```sh
while :
do
  # インストールしたい何か
  npm install -g gulp
  if [ $? -eq 0 ]; then
    break # 成功したらループを抜ける
  fi
done
```

# まとめ
私がシェルスクリプトで冪等性を担保するために使う小技を紹介しました。
なんだかんだシェルスクリプトは学習コストが低い・・・。廃らない技術だと思っています。

プロビジョニングにもシェルスクリプトは向いてますよ！！！というまとめでした！