LoginSignup
612
632

More than 1 year has passed since last update.

Bash Scriptの作法

Last updated at Posted at 2021-09-19

はじめに

チーム開発でシェルスクリプト(特にBash Script)を書く際に守るべきルールをまとめます。
シェルスクリプトのコードレビューをする際等にお役立てください。

スクリプト名

-区切りの小文字英数字(いわゆるkebab-case)をつける。拡張子は.sh.

OK: deploy-server.sh
NG: deploy_server.sh

これは特に理由があるわけではないので_区切りでもよいし、チーム内にzshfish等の他Shell愛好家がいる場合は拡張子は.bashでも良い。どちらにせよチーム内では統一しておかないと混乱の元となる。

改行コード

LFを使用する。CRLFだと動作しないので書く際に間違うことはないが、Windows上のgitのautocrlf設定によってはご丁寧にCRLFでチェックアウトされてしまう。

避けるためにはgit config --global core.autocrlf inputとするか、.gitattributesをレポジトリにコミットしておく。

.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

ただし、上記の例のような場合は変数置換ではなく、basenamedirnameコマンドの方が可読性が高い。

関数

関数定義

以下の記法を用いる。

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

おわりに

これらのルールは筆者の経験に基づくルールです。
異論反論がありましたらコメントの方に指摘してもらえると助かります。

612
632
13

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
612
632