私家版ShellScript Coding Style


ShellScriptを書くさいに僕が気をつけていることをまとめます。この文書は「シェル芸」や「ワンライナー」といった技芸・難読化には触れません。

ShellScriptを諦めるとき

ShellScriptでプログラムを書く利点はいくつも考えられます。POSIXで定義されたコマンド群のみを使って書けばすべてのUnix上で動作するソフトウェアができます。おおよその伝統的なUnixコマンドはC言語で実装されていますが、それらを呼び出しパイプとリダイレクトで糊付けするShellScriptもまた高速です。手動でおこなっていた作業を自動化するさい、ShellScriptならひとつひとつの手順を上から順に貼り付けていくだけでおおよそは完了してしまいます。

一方ShellScriptで書かれたコードは読みづらく、この一点こそが小規模以上なShellScript実装のソフトウェアを諦める理由でもあります。一度書けばどのような環境でも動くことは、一度書けば二度とコードを読まなくてもいいことと同等ではないのです。保守をしなくてもいい理由にもなりません。使い捨てのコードでない限り、誰かがそのコードを一部分でも読むときがやってくるのです。さらに、その誰かとは二ヶ月後の自分である可能性が大です。

GoogleのShellScriptスタイルガイドを読んでまとめてみた技術系記事はインターネット上で多数公開されています。XML形式の文書を英語で読みたくない方はそれらを参考にしてください。このスタイルガイドでは冒頭に、ShellScriptは小さなユーティリティか簡単なラッパーを書きたいときのみ使うこと("Shell should only be used for small utilities or simple wrapper scripts.")、ShellScriptのコードが100行を超える規模になったのであれば、代わりにPythonで書き直すべきであること("If you are writing a script that is more than 100 lines long, you should probably be writing it in Python instead.")が指摘されています。これは正論です。むしろ僕としては50行を超えた時点でShellScript以外のスクリプト系言語で書きたいところです。

以下の事項のうちひとつでも該当するのであれば、50行を超えた時点で別の言語で実装しましょう。

Bourne Shell

ShellScriptに使う言語はBourne Shellです。Shebangは#!/bin/shです。RHELかUbuntuでしか動かす予定がないのであればBashを使っても構いませんが、後述するPOSIX準拠ではなくなります。

標準でBashがインストールされていないOSも存在することだけは頭の片隅に入れておくといいでしょう。

静的解析ツール ShellCheck

ShellCheckはHaskellで実装された、ShellScriptの静的解析ツールです。基本的にはShellCheckの指示にしたがってコードを書きます。よくある警告は変数を二重引用符で囲わないときに出るSC2086でしょう。どのように修正すればいいかはGitHubのWikiに詳しく書いてあります。警告が出たときはエラーコード(SCxxxx)のページを参照してください。

しかし、なにも考えずなんでもかんでも変数を二重引用符で囲ってしまうと、それはそれでShellScriptが意図しない動作をする可能性もあります。コーディング中の動作確認を頻繁におこないましょう。

#!/bin/sh
# 典型的な例

list="a b c d"

# ループしない
for i in "$list"; do # 変数listを二重引用符で囲ったパターン
    echo "$i"
done
# 出力は
# a b c d

# このコードはスペース区切りでループする。
list="a b c d"
for i in $list; do # 変数listは囲われていない
    echo "$i"
done
# 出力は
# a
# b
# c
# d

インデント・1行あたりの文字数

インデントはスペース4つです。Googleのスタイルでは2つですが、僕はインデントが浅すぎると目で見たときにソースコードの構造がはっきりと分からないので4つにしています。8つは長すぎました。

コードは1行あたり80文字で収めるよう努力します。ここで「努力する」と書いたのは、長い文字列を処理するときにどうしても80文字を超えてしまうこともあるからです。1行に何百文字も書いてしまうと開発者の環境によっては文字が折り返しすぎてコードの見栄えが汚くなることもあります。

パイプでコマンドの出力をつなげていくと80文字を超えてしまう場合もあります。そのときは複数行に分けて書きます。

#!/bin/sh
# 例
# これは分割しなくてもいいですが……
grep "Test" "test.txt" | sort | uniq
# 複数行に分ける場合
grep "Test" "test.txt" \
    | sort \
    | uniq

上記の例は各フィルタが短すぎて複数行に分ける利点がないかもしれませんが、ソースコード管理システムで差分をとったとき、どのフィルタをどのように変更したのかわかりやすくなります。

関数名

関数名はコマンド名との混同を避ける目的で接頭辞をつけます。たとえば"fn_", "_", "__"などです。"_"から始まるコマンドは少なくともPOSIXで定義されたコマンド群には存在しません。特定の接頭辞を付けておけば、第三者がコードを読んだとき関数だとすぐに見分けられます。

#!/bin/sh
fn_function()
{
    echo "prefix example"
}

_function()
{
    echo "prefix example"
}

__function()
{
    echo "prefix example"
}

コメント

関数定義のさいは与えられる引数と期待する戻り値についてコメントを書きます。ShellScriptは読みにくいため、このようにコメントを書いておくとコードを読まなくても予期される動作を把握できます。

# _function -- Short description of the function
#
# Args:
#   $1 filter pattern
#
# Return:
#   filtered strings
_function()
{
    # process ...
}

エラー処理

エラー処理は厳格におこないます。ShellScriptは他の高級なプログラミング言語よりも、どの箇所でエラーが発生したのか把握が難しいからです。ファイルを開くとき・ファイルを削除するとき・ディレクトリを移動するとき……などは必ずそのファイルやディレクトリが存在するかをtest(1)で確認します。またコマンドの実行が成功・失敗したかどうかの判定はコマンドの戻り値を使います。

エラーメッセージを表示してexit 1する関数を定義しておくと便利です。

#!/bin/sh
# 現実的ではないが簡単な例

# _bomb -- Show given error message then exit
#
# Args:
#   error messages
#
_bomb()
{
    printf "%s\\n" "$@"
    exit 1
}

dir="./path/to/directory"

test -d "$dir" || _bomb "$dir: Not found"
cd "$dir" || _bomb "cd $dir: failed"
test -f "Makefile" || _bomb "$PWD/Makefile: Not found"
make || _bomb "make: failed"

この_bombという関数名はNetBSDのbuild.shから拝借しました。オリジナルのbombは次のようになっています。

# Copyright (c) 2001-2011 The NetBSD Foundation, Inc.
# All rights reserved.
#
# This is 2-Clause BSD License
bomb()
{
  cat >&2 <<ERRORMESSAGE

ERROR: $@
*** BUILD ABORTED ***
ERRORMESSAGE
  kill ${toppid}    # in case we were invoked from a subshell
  exit 1
}

POSIX

POSIXはUnix OSの標準規格です。POSIXではShellコマンドも定義されています。Utilitiesに書かれているコマンドのみを使ってShellScriptを書けば、POSIX準拠の移植性の高いShellScriptであると言えます。

書き捨てるShellScriptか、実行環境が限定されているShellScriptであればPOSIX準拠を考える必要はありません。しかし小規模かつ複数の環境での実行を想定する場合や、実行環境が商用Unixかつその環境でプログラミングやテストをするのが難しい(本番環境だからなるべく下手なことをしたくない、外部のサービスを契約して1日単位でレンタルする必要があり開発には向かない、……など)場合は、POSIXにしたがってコーディングすることでコードの動作を保証できます。

巨大なソフトウェアをShellScriptで実装しようというモチベーションのひとつには、POSIX準拠の実装による移植性の担保があるように思えます。しかし前述した理由により、優れた移植性による恩恵よりもコードリーディングの難しさによる問題のほうがよほど目立つでしょう。僕は推奨しません。