Linuxのシェルスクリプトで関数を定義・呼び出す基本形は、name() { commands; }です。関数はコマンドのまとまりへ名前を付け、通常のコマンドと同じように呼び出せます。Bashでは関数本体が現在のシェル文脈で動くため、引数、終了ステータス、変数のスコープ、作業ディレクトリへの副作用を設計してから共通化することが重要です。本記事は移植しやすい基本形とBash固有機能を区別します。
移植しやすい関数定義の基本形
Bashではfname () compound-commandとfunction fname ...の2形式を認識します。Bash以外のPOSIX系シェルでも使う可能性があるスクリプトでは、function予約語に依存せずname() { ...; }を使います。関数名は英字またはアンダースコアから始まる分かりやすいシェル名にすると、POSIXモードや静的解析とも合わせやすくなります。
print_summary() {
printf 'file=%s status=%s\n' "$1" "$2"
}
print_summary 'report 01.txt' 'ready'
波括弧は予約語なので、関数名や本体との間に空白または改行が必要です。また、閉じ波括弧の前のコマンドリストはセミコロン、&、または改行で終えます。一行へ詰めるとf(){ printf ...; }のように区切り忘れが起きやすいため、保守するスクリプトでは複数行で書く方が確認しやすくなります。
定義を実行してから関数名で呼び出す
関数定義は、その定義へ実行が到達した時点で現在のシェルに登録されます。通常のスクリプトでは、先に関数群を定義し、その後にメイン処理から呼び出します。呼び出しより後にしか定義がないと、その時点ではコマンドとして見つかりません。下部へメイン関数と呼び出しを置く構成なら、依存関係も追いやすくなります。
validate_input() {
test -r "$1"
}
main() {
if validate_input "$1"; then
printf 'readable: %s\n' "$1"
else
printf 'not readable: %s\n' "$1" >&2
return 1
fi
}
main "$@"
例ではスクリプトの引数を引用付き"$@"でmainへ渡します。各引数の境界が保たれるため、空白を含むファイル名も一引数のままです。関数名を変数に入れて外部入力から動的に呼び出す設計は、意図しない関数やコマンドの選択につながるため避け、許可した処理をcaseで明示的に対応付けます。
関数の引数は一時的な位置パラメーターになる
関数の実行中は、呼び出し時の引数が$1、$2、$#、$@などの位置パラメーターになります。関数が戻ると呼び出し元の位置パラメーターが復元され、$0は関数名には変わりません。10番目以降は${10}のように波括弧が必要です。
show_arguments() {
printf 'count=%s\n' "$#"
local index=1
local argument
for argument in "$@"; do
printf 'arg[%s]=%q\n' "$index" "$argument"
((index += 1))
done
}
show_arguments 'alpha beta' '' 'gamma'
この例のlocal、算術コマンド、printf %qはBash固有です。/bin/shで動かす要件があるなら使わず、対象シェルで構文確認と実行テストを行います。引数を単語分割したいという明確な仕様がない限り、$1は"$1"、一覧は"$@"として展開します。
必要な引数を本体の先頭で検証する
未指定の$1をそのままパスやオプションへ使うと、空文字や別の既定動作になり得ます。個数、空文字を許すか、値の形式、パスの許可範囲を先に確認します。オプション風の値をファイル名として渡す可能性があるコマンドでは、対応していれば--でオプション終端を示します。
read_report() {
if (( $# != 1 )); then
printf 'usage: read_report FILE\n' >&2
return 2
fi
local file=$1
if [[ ! -r $file || ! -f $file ]]; then
printf 'not a readable regular file: %s\n' "$file" >&2
return 1
fi
sed -n '1,20p' -- "$file"
}
ここでは関数仕様として2を引数不備、1を読み取り不可に割り当てています。値の意味はプロジェクト内で文書化します。[[ ]]とlocalはBash固有です。シンボリックリンク、ネットワークファイルシステム、検査後の差し替えが問題になる高い権限の処理では、この簡易確認だけをセキュリティ境界にせず、専用APIや最小権限の設計を使います。
localで変数の上書きを減らす
Bashの関数は現在のシェル文脈で動き、通常の変数は呼び出し元と共有されます。内部変数をlocalで宣言すると、関数終了時に外側の同名変数が再び見えるため、意図しない上書きを減らせます。ただしBashのローカル変数は動的スコープで、呼び出した別関数からも見える点に注意します。
format_record() {
local input=$1
local result
if result=$(normalize_record "$input"); then
printf '%s\n' "$result"
else
local status=$?
printf 'normalization failed: status=%s\n' "$status" >&2
return "$status"
fi
}
local result=$(normalize_record ...)のように宣言とコマンド置換を一文へまとめると、local自体の成功値により置換したコマンドの失敗を見落とすことがあります。宣言と代入を分け、代入をifで直接判定します。ローカルにしただけで値が秘密になるわけではなく、set -xや診断出力へ機密値を出さない配慮も必要です。
returnは制御、標準出力はデータに使う
関数の終了ステータスは、最後に実行したコマンドの値、またはreturn nで指定した値です。returnは0から255に収まる制御値に使い、文字列を返す構文ではありません。データは標準出力へ書き、呼び出し側がコマンド置換で取得できます。診断は標準エラーへ分けます。
get_label() {
if (( $# != 1 )); then
printf 'get_label requires one argument\n' >&2
return 2
fi
printf 'item-%s\n' "$1"
}
if label=$(get_label 42); then
printf 'label=%s\n' "$label"
else
status=$?
printf 'get_label failed: status=%s\n' "$status" >&2
fi
コマンド置換は末尾の改行を取り除き、関数をサブシェル環境で実行します。そのため、関数内の変数変更を呼び出し元へ戻す目的には使えません。大量データを変数へ詰め込まず、ストリーム、配列、ファイル記述子、明示した出力先など用途に合う受け渡し方法を選びます。
作業ディレクトリなどの副作用を閉じ込める
通常の関数内でcdすると、成功後の作業ディレクトリは呼び出し元でも変わります。変数代入、umask、シェルオプションなども同様に残る場合があります。副作用が不要な処理は、関数本体を丸括弧のサブシェルにして閉じ込められます。
show_directory() (
if (( $# != 1 )); then
printf 'show_directory requires one path\n' >&2
return 2
fi
cd -- "$1" || return
printf 'directory=%s\n' "$PWD"
printf 'entries=%s\n' "$(find . -maxdepth 1 -mindepth 1 -print | wc -l)"
)
show_directory /srv/reports
この形式ではcdや通常の変数変更が関数終了後に残りません。一方、呼び出し元の変数を更新したい関数には適しません。関数の契約として、標準出力、標準エラー、終了値、変更するグローバル状態を説明します。読み取り専用の例でも、対象ディレクトリの権限とデータ量を確認します。
パイプライン内の関数は別環境になり得る
Bashでは複数要素のパイプライン各段が通常それぞれのサブシェルで実行されます。関数をproducer | collect_valuesの右側に置き、その中で配列やカウンターを更新しても、パイプライン終了後の親シェルへ変更が残らないことがあります。lastpipe設定などで例外はありますが、環境依存の暗黙動作へ頼らない方が安全です。
親シェルへ値を残す必要がある場合は、関数へファイルを直接読ませる、プロセス置換からwhileへ入力する、明示した一時データ経路を使うなど、対象Bashバージョンでテストできる構造にします。単にパイプの左右を入れ替えると終了ステータスやストリームの意味も変わるため、データフローを先に図式化します。
関数ライブラリは信頼できる固定パスから読む
複数スクリプトで共有する関数は別ファイルへ置き、.またはBashのsourceで現在のシェルへ読み込めます。読み込んだファイルのコマンドは現在のシェル権限で実行されるため、外部入力や書き込み可能な検索パスからファイル名を組み立てません。所有者と権限を確認した固定パスを指定します。
# このスクリプトと同じ管理済みディレクトリにあるライブラリを想定
script_dir=$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P) || exit
. "$script_dir/lib/report-functions.sh"
declare -F build_report >/dev/null || {
printf '%s\n' 'required function is missing' >&2
exit 1
}
BASH_SOURCE、declare -FはBash固有です。読み込みファイル内でメイン処理を自動実行せず、関数定義と定数だけを提供すると再利用しやすくなります。関数のexport -fは子Bashの環境へ定義を渡しますが、環境境界を複雑にするため、別プロセスなら通常は検証済みスクリプトを明示的に実行する方が追跡しやすくなります。
名前の衝突と呼び出し先を確認する
関数名が外部コマンドや組み込みコマンドと同じだと、関数が優先されることがあります。type -a nameやdeclare -Fで解決先を確認します。関数内から同名の外部コマンドを意図して呼ぶ場合はcommand name、同名のBash組み込みを呼ぶ場合はbuiltin nameを使えます。曖昧な上書きより、用途を表す固有名を選ぶ方が保守しやすくなります。
確認チェックリスト
- 対象シェルを決め、Bash固有構文と移植可能な構文を区別した。
- 関数を呼び出す前に定義または信頼できるライブラリの読み込みが完了している。
- 引数個数、空文字、値の形式、パス範囲を本体の先頭で検証した。
- 引数の境界を
"$1"と"$@"で保持した。 - 内部変数は必要に応じて
localにし、動的スコープの影響も確認した。 - データは標準出力、診断は標準エラー、成功・失敗は終了ステータスへ分けた。
- 作業ディレクトリ、変数、オプションなど呼び出し元へ残る副作用を文書化した。
- 成功、引数不備、内部コマンド失敗、空出力を個別にテストした。
関数は単なる短縮記法ではなく、小さなコマンドインターフェースです。移植しやすいname() { ...; }から始め、引用した位置パラメーター、明確な終了値、局所変数、副作用の範囲を決めます。Bash固有のlocal、配列、[[ ]]、BASH_SOURCEを使う場合はシバンと要件を合わせ、関数単位の失敗テストを用意すると安全に再利用できます。

コメント