はじめに
最近、シェルスクリプトを書く機会があり、どういう初手をとるのがいいのか、自分なりに整理し直しました。
まず先頭に書くもの
もはや、何番煎じかわからないネタですが、あらためて。
このあたりの記事がよくまとまっています。
結論
# !/usr/bin/env bash
set -euo pipefail
shebang
# !/usr/bin/env bash
bashのバスが、OSなどによって微妙にちがうので、これが一番、そういったものに影響を受けないようです。
作ったシェルスクリプトを、docker container上で動かすなどであれば、portabilityが高い方がいい、というのが、最近の落とし所のようです。
エラーハンドリング
set -euo pipefail
エラー時に即時停止
仕事でシェルスクリプトを書く場合、なんらかのバッチ処理のためのことが多いです。
その場合、想定したファイルが存在しないなど、途中でエラーになった場合は、その時点で処理を止めたいです。
(その状態で後続の処理が続くと、意図しない結果がより広がるからです)
下記を追記することで、
set -e
未定義変数をエラー扱い
変数名のtypoや、値のセットをし忘れた場合に、それを早く検知したいです。
運用上というより、開発時のこういったミスを浮き彫りにするためですね。
# !/usr/bin/env bash
set -e
echo "start"
echo $TEST
echo "end"
$ ./sample.sh
start
end
上記のように、未定義変数を使うと、空の値として扱われます。
変数の使い方次第では、そのまま後続処理がエラーコードを出せずに続いてしまう可能性があります。
この設定で、そういった場合をエラーにできるようになります。
set -u
下記のように、未定義変数を使った箇所で止まるようになります。
$ ./sample.sh
start
./sample.sh: line 7: TEST: unbound variable
パイプしているコマンドにどれかが失敗すれば、エラー扱い
パイプのexit codeは、最後のものになります。
その前が失敗していても、最後のコマンドがエラー扱いにならない場合は、exit codeは0になってしまいます。
$ false | false | false | true
$ echo $?
0
$ true | true | true | false
$ echo $?
1
パイプしているコマンドのどれかでエラーになったら、止めた場合は以下の設定をいれます
set -o pipefail
下記で試してみます。
# !/usr/bin/env bash
set -euo pipefail
echo "start"
true | false | true | true
echo "end"
想定どおりの挙動ですね。
$ ./sample.sh
start
オプション引数の取り扱い
出力先のディレクトリなど、「実際の運用では、とりうる値は1パターンだが、テスト・開発をしやすくするために、値をかえやすくしたい」というものがあります。こういうときは、「引数なしのときは、本番用の値、引数を渡したらそれを使う」ということで、引数の有無やその値で制御できると楽です。
シェルスクリプトの引数のparseのやり方を色々見つけたのですが、なるべくすくない知識と単純な発想でできないかと、思案した結果がこちらです。結局は、自分の要求にあわせた自前での解析が、過不足のない実装になりそうです。
# !/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に該当したら、その次の引数を対応する変数にセット
という方針です。オプション引数を適正に渡したら、変数の値が書き換えられるのが確認できます。
$ ./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}
と、変数の前にエクスクラメーションをいれる書き方)を使っているのが、やや強引なところです。
必須の引数は、最初の方に渡してもらうとすれば、この方法と共存できます。
# !/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}"
$ ./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
これは、必須引数や、オプション引数の順番に制約をつけているので、実装を簡略化できています。
もっと渡す引数の順番に自由度をもたせたないなら、結構がんばった解析が必要です。
参考