原文: https://google.github.io/styleguide/shellguide.html
Shell Style Guide
Revision 2.02
多くの Google 社員によって執筆、改定そして保守されている。
目次 (Table of Contents)
背景 (Background)
どのシェルを使うか (Which Shell to Use)
Bash は実行が許可された唯一のシェルスクリプト言語である。
実行可能ファイルは #!/bin/bash
と最小限のフラグで始めなければならない。シェルオプションの設定に set
を利用することで、 スクリプトを bash script_name
として呼び出してもその機能を損なわないようにせよ。
全ての実行可能シェルスクリプトを bash
に制限することで、全てのマシンにインストールされた一貫したシェル言語を得る。
これに対する唯一の例外は、コーディング対象によって強制される場合である。この1つの例として、Solaris SVR4 パッケージは、どんなスクリプトにも plain Bourne shell であることを要求する。
いつシェルを使うか (When to use Shell)
シェルは小さな用途やシンプルなラッパースクリプトとして利用されるべきである。
シェルスクリプトは開発言語ではないが、Google 全体で様々な用途の記述に利用される。このスタイルガイドは広く普及させるための提案というよりも使用法の認識である。
一般的なガイドライン:
- もし殆どが他のユーティリティの呼び出しや相対的に小さなデータ操作である場合、シェルはそのタスクに許容可能な選択肢の1つである。
- もし性能に問題があるなら、シェル以外の何かを利用せよ。
- もし長さが100行を超えるスクリプトを書いている、もしくはその制御フローロジックが直進するものでないなら、今すぐに より構造的な言語で書き直すべきである。スクリプトは膨張することを心に留めておけ。もっと後に書き直すのにより多くの時間を消費するのを避けるために早く書き直せ。
- (言語を切り替えるかどうか決めるため等で) コードの複雑さを評価する際は、コードの作者以外にも簡易にメンテナンス可能であるかどうかを考慮せよ。
シェルファイルとインタープリタ呼出 (Shell Files and Interpreter Invocation)
ファイル拡張子 (File Extensions)
実行可能ファイルは、拡張子なし (強く推奨) もしくは .sh
とすべきである。ライブラリは必ず .sh
としなければならないかつ実行可能であってはならない。
実行時はプログラムがどの言語で書かれているかを知る必要がなく、シェルは拡張子を要求しないため、実行可能ファイルには拡張子を付けない方が好ましい。
しかし、ライブラリの場合はどの言語で書かれているかを知ることは重要であり、異なる言語で似たようなライブラリが必要な場合もある。拡張子を付与することで、同じ目的のライブラリファイルの、言語固有の拡張子を除いた部分の名前を同一にできる。
SUID/SGID
SUID と SGID はシェルスクリプトにおいて 禁止 されている。
シェルにはセキュリティの問題が多々あり、それにより SUID/SGIDを許可するのに十分な安全さを確保することはほぼ不可能である。bash は SUID の実行を困難にするが、プラットフォームによってはそれが可能であるため、明示的に禁止している。
権限昇格が必要であれば sudo
を利用せよ。
環境 (Environment)
STDOUT vs STDERR
全てのエラーメッセージは STDERR
に送るべきである。
これは通常の状態と本質的な問題の切り分けを容易にする。
状態情報と共にエラーメッセージを表示する関数の利用が推奨される。
err() {
echo "[$(date +'%Y-%m-%dT%H:%M:%S%z')]: $*" >&2
}
if ! do_something; then
err "Unable to do_something"
exit 1
fi
コメント (Comments)
File Header
ファイルは内容の説明の記述から始めよ。
全てのファイルには内容の簡単な説明を含む top-level comment が記述されていなければならない。copyright と著者情報はオプショナルである。
例:
#!/bin/bash
#
# Perform hot backups of Oracle databases.
関数コメント (Function Comments)
一目瞭然でなくかつ短くもない関数にはコメントを付与しなけなければならない。長さや複雑さに関わらず、ライブラリにおいてはどんな関数にもコメントが必須である。
コードを読まずにコメント (と self-help, もし提供されていれば) を読むことは、他人がプログラムの利用方法を学ぶことや、ライブラリ中の関数の利用を可能にするはずである。
全ての関数コメントは、次の事項により API の意図する動作を説明するべきである:
- 関数の説明
- Globals: 利用、変更されるグローバル変数のリスト。
- Arguments: 受け取る引数。
- Outputs: STDOUT か STDERR への出力。
- Returns: 通常の終了ステータス以外の場合の最終コマンドの返り値。
例:
#######################################
# Cleanup files from the backup directory.
# Globals:
# BACKUP_DIR
# ORACLE_SID
# Arguments:
# None
#######################################
function cleanup() {
...
}
#######################################
# Get configuration directory.
# Globals:
# SOMEDIR
# Arguments:
# None
# Outputs:
# Writes location to stdout
#######################################
function get_dir() {
echo "${SOMEDIR}"
}
#######################################
# Delete a file in a sophisticated manner.
# Arguments:
# File to delete, a path.
# Returns:
# 0 if thing was deleted, non-zero on error.
#######################################
function del_thing() {
rm "$1"
}
コメントの実装 (Implementation Comments)
トリッキーであったり、一目瞭然でない、興味深い、もしくは重要なコードの部分にコメントせよ。
これは Google の一般的なコメントの慣例に従う。全てにはコメントしてはならない。複雑なアルゴリズムが存在したり、通常から外れたことをしている場合に、短いコメントを付与せよ。
TODO コメント (TODO Comments)
一時的であったり、短期的な解決策、概ね良いが完璧でないコードには TODO コメントを利用せよ。
これは C++ Guide の規則と一致する。
TODO
には全て大文字の文字列 TODO
を含めるべきであり、それに続けて名前、メールアドレス、その他 TODO
が示す問題について最も状況にあった個人の識別情報を書く。この主な目的は、要求に応じてより詳細を探すために、検索可能な一貫した TODO
を用意することである。TODO
は参照された人物が問題を修正する確約ではない。そのため TODO
を作成するときは、殆どの場合、あなたの名前を付与せよ。
例:
# TODO(mrmonkey): Handle the unlikely edge cases (bug ####)
フォーマット (Formatting)
既存のファイルを編集しているときはそのスタイルに従う必要があるが、新しいコードには次のスタイルが要求される。
インデント (Indentation)
2つのスペースでインデントせよ。タブではない。
可読性向上のため、ブロック間には空行を入れよ。インデントは2つのスペースである。何をするにしても、タブは利用不可である。既存のファイルでは、既存のインデントを忠実に守れ。
行の長さと長い文字列 (Line Length and Long Strings)
行の最大の長さは80文字である。
もし80文字以上の文字列を記述する必要があるなら、可能であればヒアドキュメントや埋め込み改行で済ませよ。80文字を超える、適切に分割できない文字列リテラルは構わないが、短くする方法を探すことを強く推奨する。
# ヒアドキュメントの利用
cat <<END
I am an exceptionally long
string.
END
# 埋め込み改行
long_string="I am an exceptionally
long string."
パイプライン (Pipelines)
パイプラインは、全体が1行に収まらない場合、1行ごとに分割されるべきである。
もしパイプライン全体が1行に収まるなら、1行で記述するべきである。
そうでなければ、後続するパイプセクションのために改行し、2つのスペースでインデントし、パイプを置く形式でパイプセグメントが行ごとに分割されるべきである。
これは |
によるコマンドの連鎖や、||
や &&
の論理演算子による連結にも適用される。
# 1行に全て収まる場合
command1 | command2
# 長いコマンド
command1 \
| command2 \
| command3 \
| command4
ループ (Loops)
; do
と ; then
は、while
, for
そして if
と同じ行に置け。
シェルのループは少し変わっているが、関数の宣言時におけるカッコの原則に従う。それは: ; then
と ; do
は if/for/while と同じ行に置かれるべきある。 else
は独自の行に置かれるべきであり、閉じ構文も独自の行に置かれるべきである。そしてそれらは開き構文に垂直方向に整列されるべきである。
# 関数内部では、グローバル環境を汚すのを避けるため、
# ループ変数を local として宣言することも検討せよ。
# local dir
for dir in "${dirs_to_cleanup[@]}"; do
if [[ -d "${dir}/${ORACLE_SID}" ]]; then
log_date "Cleaning up old files in ${dir}/${ORACLE_SID}"
rm "${dir}/${ORACLE_SID}/"*
if (( $? != 0 )); then
error_message
fi
else
mkdir -p "${dir}/${ORACLE_SID}"
if (( $? != 0 )); then
error_message
fi
fi
done
case 文 (Case statement)
- 候補は2つのスペースでインデントせよ。
- 1行の候補では、パターンの閉じカッコの後ろ、及び
;;
の前に、1つのスペースが必要。 - 長いもしくは複数コマンドの候補は、パターン、アクション、そして
;;
が複数行に分割されるべきである。
条件式は case
や esac
から1レベルインデントされるべきである。複数行のアクションはさらなるレベルにインデントする。一般的に、条件式はクオートされる必要はない。パターン表現の前に開きカッコがあってはならない。 ;&
や ;;&
の記法は回避せよ。
case "${expression}" in
a)
variable="..."
some_command "${variable}" "${other_expr}" ...
;;
absolute)
actions="relative"
another_command "${actions}" "${other_expr}" ...
;;
*)
error "Unexpected expression '${expression}'"
;;
esac
単純なコマンドは式の可読性が保たれるならば、パターンおよび ;;
と同じ行に配置可能である。これは多くの場合、1文字オプションの処理に適当である。アクションが単一行に収まらない場合、パターンは独自の行に置き、次にアクション、次に ;;
を同様に独自の行に置け。パターンをアクションと同じ行に配置する場合、パターンの閉じカッコの後ろ、及び ;;
の前に、1つのスペースを入れよ。
verbose='false'
aflag=''
bflag=''
files=''
while getopts 'abf:v' flag; do
case "${flag}" in
a) aflag='true' ;;
b) bflag='true' ;;
f) files="${OPTARG}" ;;
v) verbose='true' ;;
*) error "Unexpected option ${flag}" ;;
esac
done
変数展開 (Variable expansion)
重要度順: 見つけたものに対しては一貫性を保て。変数はクオートせよ。$var
よりも ${var}
の方が好ましい。
これらは強く推奨されるガイドラインであるが、必須のレギュレーションではない。ただし、事実、これは推奨であり必須ではないが、これが軽視されたり甘く見られてもいいという意味ではない。
重要度順にリストを示す。
- 見つけたものに対しては一貫性を保て。
- 変数はクオートせよ。後述の クオート (Quoting) を見よ。
- 明示的に必要な場合もしくは深刻な混乱を避ける場合を除いて、シェル特殊変数/位置パラメータ はブレースで区切るな。
その他全ての変数はブレースで区切るのが好ましい。
# *推奨される* ケース
# 特殊変数の好ましいスタイル:
echo "Positional: $1" "$5" "$3"
echo "Specials: !=$!, -=$-, _=$_. ?=$?, #=$# *=$* @=$@ \$=$$ ..."
# ブレースは必須:
echo "many parameters: ${10}"
# ブレースが紛らわしさを回避する:
# 出力は "a0b0c0"
set -- a b c
echo "${1}0${2}0${3}0"
# その他の変数の好ましいスタイル:
echo "PATH=${PATH}, PWD=${PWD}, mine=${some_var}"
while read -r f; do
echo "file=${f}"
done < <(find /tmp)
# 非推奨なケース
# クオートされていない変数、ブレースされていない変数、
# ブレースされた1文字のシェル特殊変数。
echo a=$avar "b=$bvar" "PID=${$}" "${1}"
# 紛らわしい利用方法: これは "${1}0${2}0${3}0" と展開され、
# "${10}${20}${30}" とはならない
set -- a b c
echo "$10$20$30"
NOTE: ブレースを使っている ${var}
はクオートされている形式ではない。"Double quotes"
も使わなければならない。
クオート (Quoting)
- 綿密に、クオートされていない展開が要求される場合や、シェル内部整数 (次の点を見よ) である場合を除き、変数、コマンド置換、スペースやシェルのメタ文字を含む文字列は常にクオートせよ。
- 複数の要素を安全にクオートするために配列を利用せよ。特にコマンドラインフラグの場合。後述の 配列 (Arrays) を見よ。
- 整数として定義されるシェル内部の読み取り専用特殊変数のクオートはオプションである:
$?
,$#
,$$
,$!
(man bash)。一貫性のため、"named" な整数の内部変数はクオートした方がいい。e.g. PPID。 - "words" はクオートした方がいい (コマンドオプションやパス名とは対象的に)。
- 整数 リテラル はクオートしてはならない。
- [[ ... ]] 中のパターンマッチにおけるクオートの規則に注意を払え。後述の Test, [ ... ], and [[ ... ]] を見よ。
- 例えば単純に引数をメッセージの文字列やログに追記するような特別な理由でないならば、
$*
ではなく"$@"
を利用せよ。
# 'Single' クオートは置換の発生を望まないことを示す。
# "Double" クオートは置換が要求/許容されることを示す。
# 簡単な例
# "コマンド置換はクオートせよ"
# "$()" 内に入れ子になったクオートはエスケープ不要であることに注意せよ。
flag="$(some_command and its args "$@" 'quoted separately')"
# "変数のクオート"
echo "${flag}"
# リストとしてクオート展開された配列を利用せよ。
declare -a FLAGS
FLAGS=( --foo --bar='baz' )
readonly FLAGS
mybinary "${FLAGS[@]}"
# 内部整数変数はクオートしなくても良い。
if (( $# > 3 )); then
echo "ppid=${PPID}"
fi
# "整数リテラルはクオートしてはならない"
value=32
# "コマンド置換はクオート" せよ。たとえ整数を期待していても。
number="$(generate_number)"
# "words はクオートしたほうがいい" が、強制ではない。
readonly USE_INTEGER='true'
# "シェルのメタ文字はクオートせよ"
echo 'Hello stranger, and well met. Earn lots of $$$'
echo "Process $$: Done making \$\$\$."
# "コマンドオプションやパス名"
# (ここでは $1 は値を含んでいると仮定する)
grep -li Hugo /dev/null "$1"
# 単純でない例
# "ccs が空かもしれない場合、変数はクオートせよ"
git send-email --to "${reviewers}" ${ccs:+"--cc" "${ccs}"}
# 位置パラメータの予防策: $1がアンセットされている場合への対処
# (訳注: ${1:+"$1"} ではなく $1 とした場合、grep はファイル $1 から読むが、$1 にスペースが含まれていると ng)
# (訳注: ${1:+"$1"} ではなく "$1" とした場合、$1 がセットされていない場合に grep がファイル "" から読む事になり ng)
# (訳注: ${1:+"$1"} として $1 がセットされていない場合: grep はパイプを読むので ok)
# (訳注: ${1:+"$1"} として $1 がセットされている場合: grep はファイル "$1" から読む。クオートしてあるので $1 にスペースが含まれていてもok)
# シングルクオートは正規表現をその通りにできる。
# (訳注: \や$を考慮しなくていい)
grep -cP '([Ss]pecial|\|?characters*)$' ${1:+"$1"}
# 引数渡しについては、
# "$@" はほぼ正しくなるが、
# $* はほぼ正しくならない。
#
# * $* と $@ はスペースで分割するため、
# スペースを含む引数を滅茶苦茶にし、空文字列を破棄する。
# * "$@" は引数をそのまま保持するため、
# 引数が与えられなかった場合は引数を与えない。
# これは引数を渡すほとんどのケースにおいて利用したい動作である。
# * "$*" は1つの引数に展開し、すべての引数が (通常は) スペースで結合される。
# そのため引数が与えられなかった場合は1つの空の文字列が渡される。
# (細かいことについては `man bash` に相談してね ;-)
(set -- 1 "2 two" "3 three tres"; echo $#; set -- "$*"; echo "$#, $@")
(set -- 1 "2 two" "3 three tres"; echo $#; set -- "$@"; echo "$#, $@")
機能とバグ (Features and Bugs)
ShellCheck
ShellCheck プロジェクトはシェルスクリプトについての一般的なバグや警告を識別する。これはシェルスクリプトが大きかろうと小さかろうと全てに適用されることを推奨する。
コマンド置換 (Command Substitution)
backtick ではなく $(command)
を使用せよ。
入れ子になった内側の backtick は \
によるエスケープが求められる。$(command)
の形式なら入れ子になっても変更の必要がなく読みやすい。
例:
# これが好ましい:
var="$(command "$(command1)")"
# これは好ましくない
var="`command \`command1\``"
Test, [ ... ], and [[ ... ]]
[[ ... ]]
は [ ... ]
, test
そして /usr/bin/[
よりも好ましい。
[[ ... ]]
は [[
と ]]
の間でパス名の展開や単語の分割が行われないためエラーを削減する。さらに、[[ ... ]]]
は [...]
とは違い、正規表現マッチングが可能である。
# これは左の文字列が、alnum 文字クラスの後に
# name という文字列が続く構成であることを保証する。
# ここでは右辺はクオートされなくても良いことに注意せよ。
if [[ "filename" =~ ^[[:alnum:]]+name ]]; then
echo "Match"
fi
# これは "f*" そのものにマッチする (このケースではマッチしない)
if [[ "filename" == "f*" ]]; then
echo "Match"
fi
# これは f* がカレントディレクトリのコンテンツに展開されるため、
# "too many arguments" エラーとなる。
if [ "filename" == f* ]; then
echo "Match"
fi
泥沼な詳細については http://tiswww.case.edu/php/chet/bash/FAQ の E14 を見よ。
文字列の test (Testing Strings)
bash では test で空文字列を十分スマートに扱える。そのため、コードの可読性を考えるならば、空/非空の文字列の test もしくは充填文字ではなく空文字列による判定を利用せよ。
# こうせよ:
if [[ "${my_var}" == "some_string" ]]; then
do_something
fi
# -z (文字列長がゼロ) や -n (文字列長が非ゼロ) は
# 空文字列判定において好ましい。
if [[ -z "${my_var}" ]]; then
do_something
fi
# これは OK (空の側には確実にクオートを置け) であるが好ましくはない:
if [[ "${my_var}" == "" ]]; then
do_something
fi
# これも良くない:
if [[ "${my_var}X" == "some_stringX" ]]; then
do_something
fi
何をテストしているかの混乱を避けるため、明示的に -z
や -n
を利用せよ。
# これを使え
if [[ -n "${my_var}" ]]; then
do_something
fi
# こうではない
if [[ "${my_var}" ]]; then
do_something
fi
どちらも同じ働きをするが、明瞭にするため、同値判定には ==
を利用し =
は利用するな。前者は [[
の利用を強制し、後者は代入と紛らわしい。しかしながら、[[ ... ]]
の中での <
と >
の利用には注意せよ。これらは辞書的比較を行う。数値比較を行う場合は (( ... ))
または -lt
や -gt
を利用せよ。
# これを使え
if [[ "${my_var}" == "val" ]]; then
do_something
fi
if (( my_var > 3 )); then
do_something
fi
if [[ "${my_var}" -gt 3 ]]; then
do_something
fi
# こうではなく
if [[ "${my_var}" = "val" ]]; then
do_something
fi
# 恐らく意図しない辞書的比較
if [[ "${my_var}" > 3 ]]; then
# 4ならば真、22ならば偽
do_something
fi
ファイル名のワイルドカード展開 (Wildcard Expansion of Filenames)
ファイル名のワイルドカード展開を行う場合は明示的なパスを利用せよ。
ファイル名は -
から始まる可能性があるため、ワイルドカード展開は *
ではなく ./*
の方がより安全である。
# ディレクトリの中身がこうである場合:
# -f -r somedir somefile
# 誤ってディレクトリ内のほぼ全てを強制的に削除
psa@bilby$ rm -v *
removed directory: `somedir'
removed `somefile'
# 対象的に:
psa@bilby$ rm -v ./*
removed `./-f'
removed `./-r'
rm: cannot remove `./somedir': Is a directory
removed `./somefile'
Eval
eval
は回避されるべきである。
eval は変数への代入に利用される場合に入力コードを難読化し、それらの変数が何であるかの確認を可能にすることなく変数を設定できる。
# これは何を set する?
# 成功したか? 一部それとも全部が?
eval $(set_my_variables)
# 返り値の1つがスペースを含む場合に何が起こる?
variable="$(eval some_function)"
配列 (Arrays)
bash の配列は、クオートの複雑さを回避するため、要素のリストを保存するのに使われるべきである。これは特に引数リストに適用する。配列はより複雑なデータ構造の利用を容易にするために利用されるべきではない (前述の いつシェルを使うか (When to use Shell) を見よ)。
配列は文字列の順序付きコレクションを格納し、コマンドやループに対しては個々の要素に安全に展開される。
単一文字列をコマンドへの複数の引数として利用するのは避けられるべきである。それは必然的に eval
や文字列中のクオートの入れ子を誘発する。これは信頼性や可読性のある結果から遠ざけ、不要な複雑性を誘発する。
# 配列はカッコによって代入され、+=( ... ) によって追加される。
declare -a flags
flags=(--foo --bar='baz')
flags+=(--greeting="Hello ${name}")
mybinary "${flags[@]}"
# 文字列をシーケンスとして用いてはならない。
flags='--foo --bar=baz'
flags+=' --greeting="Hello world"' # これは意図したとおりには動かない。
mybinary ${flags}
# コマンド展開は配列ではなく1つの文字列を返す。
# 配列への代入におけるクオートされていない展開は回避せよ。
# なぜならコマンドの出力が特殊文字やスペースを含む場合に
# 正しく動作しないためである。
# これはリスト出力を1つの文字列に展開し、
# 特殊キーワード展開を行い、スペース分割を行う。
# そうしてようやく単語のリストとなる。
# ls コマンドはユーザのアクティブな環境によって挙動を変更する可能性もある!
declare -a files=($(ls /directory))
# get_arguments は全てを STDOUT に書き出すが、
# 引数のリストとなる前に先と同様の展開処理が走る。
mybinary $(get_arguments)
配列の利点 (Arrays Pros)
- 配列の利用はクオートを錯乱させることなくリストの作成を可能にする。逆に、配列を利用しなければ、文字列中でクオートを入れ子にする誤った試みにつながる。
- 配列は、スペースを含む任意の文字列からなるシーケンス/リストの安全な保存を可能にする。
配列の欠点 (Arrays Cons)
配列の利用によりスクリプトの複雑さが増大するリスクがある。
いつ配列を使うか (Arrays Decision)
配列はリストを安全に作成したり渡したりする場合に利用されるべきである。特に、コマンド引数のセットを構築する時、クオートが錯乱する問題を避ける場合に利用せよ。配列にアクセスするときはクオート展開 - "${array[@]}"
- を利用せよ。しかしながら、もしさらに高度なデータ操作が要求される場合は、そもそもシェルスクリプトの利用は避けるべきである。上記を見よ。
while へのパイプ (Pipes to While)
while
へパイプする場合はプロセス置換か readarray
ビルトイン (bash4+) を優先的に利用せよ。パイプはサブシェルを作るため、パイプライン中における変数の変更は親シェルに伝播しない。
while
へ至るパイプで発生する暗黙的なサブシェルは、追いかけるのが困難な分かりにくいバグを誘引する。
last_line='NULL'
your_command | while read -r line; do
if [[ -n "${line}" ]]; then
last_line="${line}"
fi
done
# これは常に 'NULL' を出力する!
echo "${last_line}"
プロセス置換もサブシェルを作成する。しかしながら、while
(やその他のコマンド) をサブシェル内に置くことなく、サブシェルから while
へのリダイレクトを可能にする。
last_line='NULL'
while read line; do
if [[ -n "${line}" ]]; then
last_line="${line}"
fi
done < <(your_command)
# これは your_command の出力の最後の空行でない行を出力する
echo "${last_line}"
Note: 出力を for ループでイテレートする際は注意せよ。
for var in $(...)
では、出力は行ではなくスペースで分割される。出力が想定外のスペースを含むことがないことが分かっているためにこれが安全であるといえる場合があるが、それが明確でなかったり可読性を改善しない場合は ($(...)
内のコマンドが長い場合など)、while read
ループかreadarray
の方が大抵安全であり明瞭である。
算術演算 (Arithmetic)
let
や $[ ... ]
や expr
ではなく、常に (( ... ))
や $(( ... ))
を利用せよ。
$[ ... ]
構文、 expr
コマンド、let
ビルトインは絶対に利用してはならない。
<
と >
は [[ ... ]]
式内部では数値比較として動作しない (これらは辞書的比較を行う。文字列の test (Testing Strings)を見よ)。全ての算術演算に対しては [[ ... ]]
ではなく、(( ... ))
を優先的に利用せよ。
(( ... ))
を独立した文として利用することは止めた方がいい。利用するならば、その式がゼロと評価されるか注意せよ。
- 特に
set -e
が有効であるとき。例えばset -e; i=0; ((i++))
ではシェルが終了してしまう。
# テキストとして利用される簡単な計算 -
# 文字列内での $(( ... )) の利用には注意せよ。
echo "$(( 2 + 2 )) is 4"
# テストで数値比較を行いたい時
if (( a < b )); then
...
fi
# 変数へ代入される計算
(( i = 10 * j + 400 ))
# この形式は non-portable かつ非推奨
i=$[2 * 10]
# 見た目に反して、'let' は宣言型キーワードではないため、
# クオートされていない代入は glob の単語分割の対象となる。
# 単純化のため、'let' は避け、(( ... )) を利用せよ。
let i="2 + 2"
# expr は外部コマンドでありシェルビルトインではない。
i=$( expr 4 + 4 )
# expr を利用する際もクオートでエラーが誘発されやすい。
i=$( expr 4 '*' 4 )
文法的考慮点を差し置いても、シェルビルトインの算術演算は多くの場合 expr
よりも高速である。
変数を使う際は、$(( ... ))
内部では ${var}
(や $var
) の形式である必要はない。シェルは var
を検索することを理解しており、${ ... }
の省略はコードを分かりやすくする。これは常にブレースを利用せよとした前述のルールとは少々対照的であるため、推奨にとどめておく。
# 注意: 可能なら、変数は integer として、
# そしてなるべくグローバル変数ではなくローカル変数として
# 宣言することを覚えておけ。
local -i hundred=$(( 10 * 10 ))
declare -i five=$(( 10 / 2 ))
# i を 3 インクリメントする。
# 注意:
# - ${i} や $i とは記述していない。
# - (( の後、 )) の前に、スペースを置いている。
(( i += 3 ))
# i を 5 デクリメントする。
(( i -= 5 ))
# 複雑な計算を行う。
# 通常の算術演算子の優先順位が守られていることに注意せよ。
hr=2
min=5
sec=30
echo $(( hr * 3600 + min * 60 + sec )) # 期待通り7530となる
命名規則 (Naming Conventions)
関数名 (Function Names)
小文字、単語の区切りにはアンダースコアを利用せよ。ライブラリは ::
で区切れ。関数名の後のカッコは必須である。キーワード function
はオプションであるが、その使用の有無はプロジェクト全体で一貫させよ。
もし単一の関数を書いている場合、小文字と単語の区切りにはアンダースコアを利用せよ。もしパッケージを書いている場合、パッケージ名は ::
で分離せよ。ブレースは関数名と同じ行に記述しなければならなず (Google における他の言語と同様に)、関数名とカッコの間にスペースがあってはならない。
# 単一の関数
my_func() {
...
}
# パッケージの一部
mypackage::my_func() {
...
}
関数名の後に "()" が存在する場合、function
キーワードは必須ではないが、関数であることを素早く同定することを促進する。
変数名 (Variable Names)
関数名と同様。
ループの変数名は、ループ対象の変数と似た名前にすべきである。
for zone in "${zones[@]}"; do
something_with "${zone}"
done
定数と環境変数 (Constants and Environment Variable Names)
全て大文字、区切りはアンダースコア、ファイルの先頭で宣言する。
定数とエクスポートされる環境変数は大文字で記述されるべきである。
# 定数
readonly PATH_TO_FILES='/some/path'
# 定数と環境変数
declare -xr ORACLE_SID='PROD'
初期設定時に定数になるものもある (例えば getopts によって)。そのため、定数を getopts や条件によって設定することは構わないが、その後、できるだけ早く readonly にすべきである。分かりやすくするため、readonly
や export
が declare
コマンドの代替として推奨される。
VERBOSE='false'
while getopts 'v' flag; do
case "${flag}" in
v) VERBOSE='true' ;;
esac
done
readonly VERBOSE
読み取り専用変数 (Read-only Variables)
読み取り専用であることを保証するため、readonly
や declare -r
を利用せよ。
グローバル変数はシェル全体で使用されるため、それらの利用時にエラーを捕捉することが重要である。読み取り専用を意図した変数を宣言する時は、それを明示的に行え。
zip_version="$(dpkg --status zip | grep Version: | cut -d ' ' -f 2)"
if [[ -z "${zip_version}" ]]; then
error_message
else
readonly zip_version
fi
局所変数を利用せよ (Use Local Variables)
関数専用の変数はlocal
で宣言せよ。宣言と代入は異なる行で行うべきである。
変数宣言を local
で行うことにより、局所変数が、関数とその子の内側のみから見えることを保証せよ。
代入値にコマンド置換が与えられた場合、宣言と代入は異なる文で行われる必要がある。これは local
ビルトインがコマンド置換から終了コードを伝播しないためである。
my_func2() {
local name="$1"
# 宣言と代入の行は分離せよ
local my_var
my_var="$(my_func)"
(( $? == 0 )) || return
...
}
my_func2() {
# こうしてはいけない
# $? は常にゼロである。なぜならこれは my_func ではなく、
# 'local' の終了コードを保持するためである。
local my_var="$(my_func)"
(( $? == 0 )) || return
...
}
関数の位置 (Function Location)
すべての関数は、ファイル中の定数記述部分の直後に配置せよ。実行可能なコードを関数と関数の間に隠すな。そんなことをすれば、コードの追跡が困難になり、結果デバッグ時に予期せぬ不幸を引き起こす。
もし複数の関数があるなら、それらはファイルの先頭に配置せよ。include、set
文そして定数の設定だけは関数の前に宣言されていても構わない。
main
他の関数を少なくとも1つ含むのに十分な長さのスクリプトには main
という関数が必ず必要である。
プログラムの開始位置を見つけるのを容易にするため、メインプログラムは main
という最後尾の関数に配置せよ。これはコードベースの残りの部分と整合性を保つだけでなく、より多くの変数を local
として定義することを可能にする (もしメインコードが関数でない場合、これは不可能である)。ファイルの、コメントでない最後の行は main
の呼び出しであるべきである:
main "$@"
ただの一直線な流れの短いスクリプトでは、明らかに main
は大仰であるため、必須ではない。
コマンド呼び出し (Calling Commands)
返り値判定 (Checking Return Values)
返り値は常に判定せよ。そして有益な返り値を与えよ。
パイプされていないコマンドでは、$?
を利用するか、簡潔にするため、if
文で直接判定せよ。
例:
if ! mv "${file_list[@]}" "${dest_dir}/"; then
echo "Unable to move ${file_list[*]} to ${dest_dir}" >&2
exit 1
fi
# もしくは
mv "${file_list[@]}" "${dest_dir}/"
if (( $? != 0 )); then
echo "Unable to move ${file_list[*]} to ${dest_dir}" >&2
exit 1
fi
bash は PIPESTATUS
変数も保持している。これは全てのパイプの箇所における返り値の判定を可能にする。もしパイプ全体が成功か失敗であるかのみを判定する必要がある場合、次が許容される:
tar -cf - ./* | ( cd "${dir}" && tar -xf - )
if (( PIPESTATUS[0] != 0 || PIPESTATUS[1] != 0 )); then
echo "Unable to tar files to ${dir}" >&2
fi
しかしながら、PIPESTATUS
は他のコマンドの実行により即座に上書きされる。もしパイプのどこでどんなエラーが起こったかによって異なる動作を行う必要がある場合、PIPESTATUS
はコマンド実行後即座に他の変数に代入される必要がある ([
がコマンドであり PIPESTATES
を一掃することを忘れるな)。
tar -cf - ./* | ( cd "${DIR}" && tar -xf - )
return_codes=( "${PIPESTATUS[@]}" )
if (( return_codes[0] != 0 )); then
do_something
fi
if (( return_codes[1] != 0 )); then
do_something_else
fi
ビルトインコマンド vs 外部コマンド (Builtin Commands vs. External Commands)
シェルビルトイン呼び出しか分離プロセス呼び出しかの選択を迫られたら、ビルトインを選択せよ。
より堅牢かつ portable (特に sed
などと比較して) であるため、bash(1) にある 変数展開 機能のようなビルトインの利用が好ましい。
例:
# これが好ましい:
addition=$(( X + Y ))
substitution="${string/#foo/bar}"
# こうではなく:
addition="$(expr "${X}" + "${Y}")"
substitution="$(echo "${string}" | sed -e 's/^foo/bar/')"
結論 (Conclusion)
定石に従い、一貫性を保て。
C++ Guide の最下部の Parting Words 節を2~3分程度で読め。
Revision 2.02