はじめに
チーム開発でシェルスクリプト(特にBash Script)を書く際に守るべきルールをまとめます。
シェルスクリプトのコードレビューをする際等にお役立てください。
スクリプト名
-
区切りの小文字英数字(いわゆるkebab-case)をつける。拡張子は.sh
.
OK: deploy-server.sh
NG: deploy_server.sh
これは特に理由があるわけではないので_
区切りでもよいし、チーム内にzsh
やfish
等の他Shell愛好家がいる場合は拡張子は.bash
でも良い。どちらにせよチーム内では統一しておかないと混乱の元となる。
改行コード
LFを使用する。CRLFだと動作しないので書く際に間違うことはないが、Windows上のgitのautocrlf設定によってはご丁寧にCRLFでチェックアウトされてしまう。
避けるためにはgit config --global core.autocrlf input
とするか、.gitattributes
をレポジトリにコミットしておく。
*.sh text eol=lf
Shebang
#!/bin/bash
間違っても#!/bin/sh
としてはいけない。
おまじない
2行目にはこれを書く
set -euxo pipefail
それぞれの意味は以下の通り
-
-e
: エラーが発生したときにスクリプトを中断する。- 途中でエラーにしたくない場合は
set +e
で一時的に解除するか||
で繋げる
- 途中でエラーにしたくない場合は
-
-u
: 未定義変数をエラーにする -
-x
: 実行したコマンドを出力する -
-o pipefail
: パイプで結合したコマンドの途中でエラーが発生したときもスクリプトを中断する
ディレクトリの移動
3行目には原則これを書く
cd "$(dirname "$0")"
こうすることでスクリプト呼び出し時の作業ディレクトリに依存せず実行できる。
ただし、スクリプトが作業ディレクトリに依存する場合は変数に退避しておく。
WORKDIR=$(pwd)
cd "$(dirname "$0")"
変数宣言
変数宣言の右辺は""
あるいは$()
で囲う
MESSAGE="Hello World!"
NOW=$(date)
右辺にスペースが含まれない場合は""
で囲わなくても問題ないのだが、
後の変更によってスペースが含まれるとバグる可能性があるため、常に囲っておいた方が安全。
X="hello date" # Xには"hello date"が格納される
Y=hello date # 一時的な環境変数Yに"hello"を格納してdateコマンドを実行する。(実行後の`Y`は未定義)
グローバル変数には_
区切りの大文字英数字を使う
BRANCH_NAME="topic-foo"
関数内の変数あるいはループ変数は_
区切りの小文字英数字を使う
LOG_FILENAME="script.log"
function hoge() {
local item=$1
echo "hoge is called with item = ${item}" >> "${LOG_FILENAME}"
}
for csv in *.csv; do
hoge "${csv}"
done
変数展開
原則${}
で囲う。例外は$0
$1
$@
$?
等の特殊変数。
RESULT="success"
LOG_DIR="/var/log/hoge"
ARG="$1"
echo "Result is ${RESULT}" | tee "${LOG_DIR}/result.log"
${}
で囲うことで、どこまでが変数名なのか明確になる。また、変数の加工を行う場合の記法と統一感がある。
変数展開は様々な機能がある。仕様を確認すべし。
よく使うものを抜粋しておく
未指定またはNULLの場合にデフォルト値で埋める
GRADLE_HOME="${GRADLE_HOME:-~/.gradle}"
未指定またはNULLの場合にエラーメッセージを表示する
GITLAB_TOKEN="${GITLAB_TOKEN:?Please specify personal access token}"
文字列置換、除去
-
${var#pattern}
でpattern
にマッチするprefixを除去 -
${var%pattern}
でpattern
にマッチするsuffixを除去 -
%
,#
はShortest-match,%%
,##
はLongest-match -
${var/pattern/string}
でpattern
にマッチした部分をstring
に置換
FILE="src/main/java/Example.java"
BASENAME=${FILE##*/} # Example.java
DIRNAME=${FILE%/*} # src/main/java
TESTDIR=${DIRNAME/main/test} # src/test/java
ただし、上記の例のような場合は変数置換ではなく、basename
やdirname
コマンドの方が可読性が高い。
関数
関数定義
以下の記法を用いる。
function 関数名() {
処理
}
function
キーワードがついていた方が関数であることが明確になる
関数名
関数名は_
区切りの小文字英数字を用いる
function login_api() {
...
}
関数内のローカル変数はlocal
をつける
function hoge() {
local item="$1/item"
...
}
local
をつけることで関数呼び出しによって意図せず変数が書き変わるのを防ぐことができる。
コマンド呼び出し
置換の発生する引数は必ず""
で囲む
置換の発生する引数は必ず""
で囲む
置換の発生する引数は必ず""
で囲む
例外はない。どんなに無害そうな変数でも囲んでおいた方が安全。
diff "test-${ID}.out" "test-${ID}.ans"
sudo chown "$(whoami)" /tmp/workdir
長いコマンドは\
で改行を入れる
エディタの推奨幅(80文字-120文字)で表示しきれないコマンドは必ず改行すること。
curl -X POST "${API_ENDPOINT}/users/${USER_ID}/status" \
-F "status=I'm so happy" \
-H "Authorization: Bearer ${ACCESS_TOKEN}"
コマンド展開
$(...)
記法を用いる。バッククオート`...`
はネストが難しいので使わない。
コマンドの引数にするときには""
で囲うことを忘れてはならない。
cd "$(dirname "$0")"
制御構文
ロジックをシンプルに保つ
シェルスクリプトは基本的に一直線のプログラムであることが望ましい。
if文やループがネストするような複雑な制御構造が必要な処理はシェルスクリプトではなくPython等のスクリプト言語を用いて記述すべき。
if文
条件の後にセミコロンを入れる記法で書く。(これはただの好み)
if 条件; then
処理1
else
処理2
fi
while文
ほとんどの場合、入力を1行ごとに処理する場合に使う
入力生成 | while read line; do
処理
done
追記
@Hogeyama さんから上記の書き方には処理部分で行なった変数の変更が外に伝播されないという問題を指摘いただきました。 コメント
処理部分で変数の書き換えを行う場合は以下のようにプロセス置換を用いて渡す必要があります。
while read line; do
処理
done < <(入力生成)
for文
以下の記法を用いる。
for 変数 in 範囲; do
処理
done
範囲
はglob(*.txt
等)か brace expansion ({1..5}
等)。コマンド展開($(...)
)の場合はwhile文で書くことを検討する。
# Discouraged (ファイルパスにスペースが含まれるとバグる)
for file in $(find . -name '*.log'); done
...
done
# Recommended
find . -name '*.log' | while read line; do
...
done
スクリプトの長さ
シェルスクリプトは単体テストが難しい。
長いスクリプトを書いてしまうと一回のテストに時間がかかり、生産性が低くなる。
目安としては100行以内に収めるべきであり、長くても200行を超えたら分割しなければならない。
ログ
シェルスクリプトの実行ログが長くなりすぎるとトラブルシューティングが難しくなる。
目安として標準出力・標準エラーは1000行以下となるようにすべき。
長すぎるログは別ファイルに書き出すようにする。
# Bad
unzip awscliv2.zip
# Good
unzip awscliv2.zip | tee unzip_awscliv2.log | tail -n 10
コマンドライン引数の渡し方
シェルスクリプトでコマンドラインパーサを実装して豊富なオプションを受け付けるのは多くの場合、労力の無駄である。
コマンドラインオプションの代わりに、環境変数で受け渡したが方がシンプルで可読性も高い。
# Discouraged
./doit.sh --token abc123
# Recommended
TOKEN="abc123" ./doit.sh
おわりに
これらのルールは筆者の経験に基づくルールです。
異論反論がありましたらコメントの方に指摘してもらえると助かります。