シェルスクリプトを書くときの初手のおさらい

# はじめに

最近、シェルスクリプトを書く機会があり、どういう初手をとるのがいいのか、自分なりに整理し直しました。

# まず先頭に書くもの

もはや、何番煎じかわからないネタですが、あらためて。
このあたりの記事がよくまとまっています。

https://ashishb.net/all/the-first-two-statements-of-your-bash-script-should-be/

https://moneyforward.com/engineers_blog/2015/05/21/bash-script-tips/

## 結論

```bash
#!/usr/bin/env bash
set -euo pipefail
```

## shebang

```bash
#!/usr/bin/env bash
```

bashのバスが、OSなどによって微妙にちがうので、これが一番、そういったものに影響を受けないようです。
作ったシェルスクリプトを、docker container上で動かすなどであれば、portabilityが高い方がいい、というのが、最近の落とし所のようです。

https://qiita.com/taiyodayo/items/3b470dbb3b54d09d63e8

https://stackoverflow.com/questions/16365130/what-is-the-difference-between-usr-bin-env-bash-and-usr-bin-bash

https://www.cyberciti.biz/tips/finding-bash-perl-python-portably-using-env.html

## エラーハンドリング

```bash
set -euo pipefail
```

### エラー時に即時停止

仕事でシェルスクリプトを書く場合、なんらかのバッチ処理のためのことが多いです。
その場合、想定したファイルが存在しないなど、途中でエラーになった場合は、その時点で処理を止めたいです。
（その状態で後続の処理が続くと、意図しない結果がより広がるからです）

下記を追記することで、

```
set -e
```

### 未定義変数をエラー扱い

変数名のtypoや、値のセットをし忘れた場合に、それを早く検知したいです。
運用上というより、開発時のこういったミスを浮き彫りにするためですね。

```bash
#!/usr/bin/env bash
set -e 

echo "start"

echo $TEST

echo "end"
```

```terminal
$ ./sample.sh 
start

end
```

上記のように、未定義変数を使うと、空の値として扱われます。
変数の使い方次第では、そのまま後続処理がエラーコードを出せずに続いてしまう可能性があります。

この設定で、そういった場合をエラーにできるようになります。

```bash
set -u
```

下記のように、未定義変数を使った箇所で止まるようになります。

```terminal
$ ./sample.sh  
start
./sample.sh: line 7: TEST: unbound variable
```


### パイプしているコマンドにどれかが失敗すれば、エラー扱い

パイプのexit codeは、最後のものになります。
その前が失敗していても、最後のコマンドがエラー扱いにならない場合は、exit codeは0になってしまいます。

```terminal
$ false | false | false | true
$ echo $?
0

$ true | true | true | false 
$ echo $? 
1  
```

パイプしているコマンドのどれかでエラーになったら、止めた場合は以下の設定をいれます

```bash
set -o pipefail
```

下記で試してみます。

```bash
#!/usr/bin/env bash
set -euo pipefail

echo "start"

true | false | true | true

echo "end"
```

想定どおりの挙動ですね。

```terminal
$ ./sample.sh 
start
```

# オプション引数の取り扱い

出力先のディレクトリなど、「実際の運用では、とりうる値は1パターンだが、テスト・開発をしやすくするために、値をかえやすくしたい」というものがあります。こういうときは、「引数なしのときは、本番用の値、引数を渡したらそれを使う」ということで、引数の有無やその値で制御できると楽です。

シェルスクリプトの引数のparseのやり方を色々見つけたのですが、なるべくすくない知識と単純な発想でできないかと、思案した結果がこちらです。結局は、自分の要求にあわせた自前での解析が、過不足のない実装になりそうです。

https://qiita.com/b4b4r07/items/dcd6be0bb9c9185475bb


```bash
#!/usr/bin/env bash
set -euo pipefail

source_dir=/mnt/nfs001/input
output_dir=/mnt/nfs001/output

for ((i=1; i<$#; i++))
do
  j=$((i+1))
  arg=${!i}
  case "${arg}" in
   "--source-dir" ) source_dir=${!j} ;;
   "--output-dir" ) output_dir=${!j} ;;
  esac
done

echo "source_dir: ${source_dir}"
echo "output_dir: ${output_dir}"

```

* スクリプト内で、変数の初期値をセット
* 引数を順番にチェック
* --XXXに該当したら、その次の引数を対応する変数にセット

という方針です。オプション引数を適正に渡したら、変数の値が書き換えられるのが確認できます。

```terminal
$ ./sample.sh 
source_dir: /mnt/nfs001/input
output_dir: /mnt/nfs001/output

$ ./sample.sh --source-dir /tmp/src                      
source_dir: /tmp/src
output_dir: /mnt/nfs001/output

$ ./sample.sh --output-dir /tmp/dst
source_dir: /mnt/nfs001/input
output_dir: /tmp/dst

$ ./sample.sh --source-dir /tmp/src --output-dir /tmp/dst
source_dir: /tmp/src
output_dir: /tmp/dst

$ ./sample.sh aaa bbb ccc ddd      
source_dir: /mnt/nfs001/input
output_dir: /mnt/nfs001/output
```

i番目、i+1番目の引数の取得するために、bashのindirection機能 (`${!i}` と、変数の前にエクスクラメーションをいれる書き方)を使っているのが、やや強引なところです。

必須の引数は、最初の方に渡してもらうとすれば、この方法と共存できます。

```bash
#!/usr/bin/env bash
set -euo pipefail

keyword=$1
status=$2

source_dir=/mnt/nfs001/input
output_dir=/mnt/nfs001/output

for ((i=3; i<$#; i++))
do
  j=$((i+1))
  arg=${!i}
  case "${arg}" in
   "--source-dir" ) source_dir=${!j} ;;
   "--output-dir" ) output_dir=${!j} ;;
  esac
done

echo "positional arguments"

echo "keyword: ${keyword}"
echo "status: ${status}"

echo " --- options --- "

echo "source_dir: ${source_dir}"
echo "output_dir: ${output_dir}"
``` 

```terminal
$ ./sample.sh                
./sample.sh: line 4: $1: unbound variable

$ ./sample.sh aaa
./sample.sh: line 5: $2: unbound variable


$ ./sample.sh aaa bbb
positional arguments
keyword: aaa
status: bbb
 --- options --- 
source_dir: /mnt/nfs001/input
output_dir: /mnt/nfs001/output

$ ./sample.sh aaa bbb --source-dir /tmp/src --output-dir /tmp/dst
positional arguments
keyword: aaa
status: bbb
 --- options --- 
source_dir: /tmp/src
output_dir: /tmp/dst

```

これは、必須引数や、オプション引数の順番に制約をつけているので、実装を簡略化できています。
もっと渡す引数の順番に自由度をもたせたないなら、結構がんばった解析が必要です。


## 参考

https://maku77.github.io/linux/startup/command-line-params.html

https://monologu.com/increment-variable/

https://tm23forest.com/contents/bash-indirect-expansion

https://stackoverflow.com/questions/8515411/what-is-indirect-expansion-what-does-var-mean

https://chitoku.jp/programming/bash-getopts-long-options