66
62

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

GoogleのShell Style Guideの邦訳

Last updated at Posted at 2021-03-27

原文: https://google.github.io/styleguide/shellguide.html

Shell Style Guide

Revision 2.02
多くの Google 社員によって執筆、改定そして保守されている。

目次 (Table of Contents)

Section Contents
背景 (Background) どのシェルを使うか (Which Shell to Use) - いつシェルを使うか (When to use Shell)
シェルファイルとインタープリタ呼出 (Shell Files and Interpreter Invocation) ファイル拡張子 (File Extensions) - SUID/SGID
環境 (Environment) STDOUT vs STDERR
コメント (Comments) File Header - 関数コメント (Function Comments) - コメントの実装 (Implementation Comments) - TODO コメント (TODO Comments)
フォーマット (Formatting) インデント (Indentation) - 行の長さと長い文字列 (Line Length and Long Strings) - パイプライン (Pipelines) - ループ (Loops) - case 文 (Case statement) - 変数展開 (Variable expansion) - クオート (Quoting)
機能とバグ (Features and Bugs) ShellCheck - コマンド置換 (Command Substitution) - Test - 文字列の test (Testing Strings) - ファイル名のワイルドカード展開 (Wildcard Expansion of Filenames) - Eval - 配列 (Arrays) - 配列の利点 (Arrays Pros) - 配列の欠点 (Arrays Cons) - いつ配列を使うか (Arrays Decision) - while へのパイプ (Pipes to While) - 算術演算 (Arithmetic)
命名規則 (Naming Conventions) 関数名 (Function Names) - 変数名 (Variable Names)
定数と環境変数 (Constants and Environment Variable Names) 読み取り専用変数 (Read-only Variables) - 局所変数を利用せよ (Use Local Variables) - 関数の位置 (Function Location) - main
コマンド呼び出し (Calling Commands) 返り値判定 (Checking Return Values) - ビルトインコマンド vs 外部コマンド (Builtin Commands vs. External Commands)
結論 (Conclusion)

背景 (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つのスペースが必要。
  • 長いもしくは複数コマンドの候補は、パターン、アクション、そして ;; が複数行に分割されるべきである。

条件式は caseesac から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 にすべきである。分かりやすくするため、readonlyexportdeclare コマンドの代替として推奨される。

VERBOSE='false'
while getopts 'v' flag; do
  case "${flag}" in
    v) VERBOSE='true' ;;
  esac
done
readonly VERBOSE

読み取り専用変数 (Read-only Variables)

読み取り専用であることを保証するため、readonlydeclare -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

66
62
1

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
66
62

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?