シェルスクリプトの関数と引数処理:再利用できるスクリプトを書く
前回は変数・条件分岐・ループといった bash の基本要素を学びました。今回はその先として、関数と引数処理を扱います。
スクリプトが長くなってくると、同じ処理のコピーが各所に散らばりがちです。関数にまとめておけば、呼び出し側の記述が短くなり、修正も一箇所で済みます。また、引数を受け取れるようにしておけば汎用性が上がり、「ファイル名だけ変えて同じ処理を実行する」といった使い方が自然にできます。
関数を定義する
基本の構文
bash の関数は次の形で定義します。
関数名() {
# 処理
}
function キーワードを付ける書き方もありますが、POSIX 互換性の観点から 名前() の形が一般的です。
#!/usr/bin/env bash
greet() {
echo "Hello, World!"
}
# 呼び出し(コマンドと同じように書くだけ)
greet
関数は定義してから呼び出すのが原則です。ファイルの後半で定義した関数を前半から呼び出したい場合は、main 関数にまとめてファイル末尾で main "$@" と呼ぶパターンがよく使われます。
ローカル変数
関数内で宣言した変数はデフォルトでグローバルスコープになります。他の関数や呼び出し元に意図せず影響を与えないよう、関数内の変数は local で宣言するのが安全です。
count=10 # グローバル変数
increment() {
local count=0 # ローカル変数(関数内だけ有効)
((count++))
echo "関数内の count: $count" # 1
}
increment
echo "グローバルの count: $count" # 10(影響を受けない)
local を付け忘れると関数内での変更がグローバル変数に反映されてしまいます。関数内では local を付ける習慣をつけておきましょう。
関数の引数と戻り値
関数内での引数参照
関数に渡した引数は、スクリプトへの引数と同じ $1、$2、$3 … で参照できます。
greet() {
local name="$1"
local greeting="${2:-Hello}" # 第2引数がなければ "Hello" をデフォルト値にする
echo "${greeting}, ${name}!"
}
greet "Alice" # Hello, Alice!
greet "Bob" "Good morning" # Good morning, Bob!
${2:-Hello} の形はデフォルト値の設定です。:- の右側がフォールバック値になります。引数が省略されたり空文字だった場合に役立ちます。
$@ と $* の違い
複数の引数をまとめて扱うときは $@ か $* を使います。挙動の差は微妙ですが重要です。
show_args() {
echo "引数の数: $#"
for arg in "$@"; do
echo " 引数: $arg"
done
}
show_args "foo" "bar baz" "qux"
# 引数の数: 3
# 引数: foo
# 引数: bar baz ← スペースを含む値が1つの引数として保たれる
# 引数: qux
"$@" はダブルクォートで囲むと各引数が個別に展開され、スペースを含む値も1つの引数として正しく扱われます。"$*" はすべての引数を IFS(デフォルトはスペース)で連結した1つの文字列として展開します。ほぼすべての場面で "$@" を使うのが正解です。
$# は引数の個数を返します。引数チェックに使えます。
戻り値の2種類の方法
bash の関数から値を返す方法は2種類あります。
1. return で終了ステータスを返す
return は 0〜255 の整数値を返します。0 が成功、1 以上が失敗を意味するのは慣習です。$? で呼び出し元から参照できます。
is_even() {
local n="$1"
if [[ $((n % 2)) -eq 0 ]]; then
return 0 # 成功(偶数)
else
return 1 # 失敗(奇数)
fi
}
if is_even 4; then
echo "偶数"
fi
is_even 7
echo "終了ステータス: $?" # 1
2. echo でコマンド置換に渡す
文字列や複雑な値を返したい場合は、関数内で echo して呼び出し元でコマンド置換 $() を使います。
get_timestamp() {
echo "$(date +%Y%m%d_%H%M%S)"
}
ts=$(get_timestamp)
echo "タイムスタンプ: $ts"
この方法は汎用性が高い反面、サブシェルを起動するため、大量に呼び出すと若干のオーバーヘッドがあります。単純な成否判定なら return、値を受け渡したいなら echo+コマンド置換と使い分けましょう。
スクリプト自体の引数処理
位置パラメータの基本
スクリプトに渡した引数は $1、$2、$3 … で参照します。$0 はスクリプト名自体です。
#!/usr/bin/env bash
echo "スクリプト名: $0"
echo "引数の数: $#"
echo "全引数: $@"
echo "1番目: $1"
echo "2番目: $2"
./script.sh foo "bar baz"
# スクリプト名: ./script.sh
# 引数の数: 2
# 全引数: foo bar baz
# 1番目: foo
# 2番目: bar baz
shift で引数をずらす
shift を使うと位置パラメータを1つずつ前にずらせます。$2 が $1 になり、元の $1 は消えます。
#!/usr/bin/env bash
while [[ $# -gt 0 ]]; do
echo "処理中の引数: $1"
shift # 次の引数へ
done
./script.sh a b c
# 処理中の引数: a
# 処理中の引数: b
# 処理中の引数: c
"$@" をループで処理する場合は for arg in "$@" の形でも同じことができますが、shift は引数を消費しながら処理したい場面(後述の getopts の周辺処理など)で役立ちます。
オプション解析:getopts
-f value や -v のようなフラグをスクリプトで受け取るには getopts を使います。
#!/usr/bin/env bash
usage() {
echo "使い方: $0 [-v] [-o 出力ファイル] 入力ファイル"
echo " -v 詳細表示"
echo " -o 出力ファイルを指定"
exit 1
}
verbose=false
output=""
while getopts "vo:" opt; do
case "$opt" in
v)
verbose=true
;;
o)
output="$OPTARG" # -o の引数は $OPTARG に入る
;;
*)
usage
;;
esac
done
# オプション処理済みの引数をスキップ
shift $((OPTIND - 1))
# 残りの位置引数
input="$1"
if [[ -z "$input" ]]; then
echo "エラー: 入力ファイルを指定してください" >&2
usage
fi
echo "入力: $input"
echo "出力: ${output:-(未指定)}"
echo "詳細: $verbose"
getopts "vo:" のオプション文字列の意味は次の通りです。
v…-vフラグ(引数なし)o:…-oフラグ(コロンは引数あり、$OPTARGに格納される)
OPTIND は getopts が処理した引数の次のインデックスを指しているため、shift $((OPTIND - 1)) でオプション処理済み分をスキップし、残りの位置引数を $1 以降で参照できるようにします。
getopts はロング形式(--verbose など)には対応していません。ロング形式が必要なら getopt(外部コマンド、実装が OS によって異なる)か、case 文で手書きするのが現実的です。
case 文による引数分岐
シンプルなサブコマンド形式なら case でも十分です。
#!/usr/bin/env bash
command="$1"
case "$command" in
start)
echo "起動します"
;;
stop)
echo "停止します"
;;
status)
echo "状態を確認します"
;;
*)
echo "不明なコマンド: $command" >&2
echo "使用可能: start | stop | status" >&2
exit 1
;;
esac
usage 関数とヘルプ表示の作法
-h や --help に対応するヘルプ表示は、スクリプトの「仕様書」でもあります。最初に書いておくと設計の整理にもなります。
usage() {
cat <<EOF
使い方: $(basename "$0") [オプション] <引数>
説明:
ファイルを処理するスクリプトです。
オプション:
-v 詳細ログを表示する
-o <ファイル> 出力先ファイルを指定する(デフォルト: output.txt)
-h このヘルプを表示する
例:
$(basename "$0") -v -o result.txt input.txt
EOF
exit 0
}
cat <<EOF ... EOF はヒアドキュメントです。複数行をそのまま出力するのに使います。$(basename "$0") でスクリプト名からパスを除いた名前だけを表示できます。
エラーメッセージは標準エラー出力(>&2)に出力するのが作法です。usage からの exit と exit 0 / exit 1 の使い分け(ヘルプは 0、エラー時は 1)も意識しておきましょう。
実践的なコツ
"$@" は必ずダブルクォートで囲む
$@ と "$@" は挙動が異なります。"$@" は各引数を個別に保ちますが、クォートなしの $@ はスペースで分割されます。引数の受け渡しは常に "$@" で行いましょう。
# 悪い例:スペースを含む引数が分割される
process $@
# 良い例:各引数の完全性が保たれる
process "$@"
引数の個数を必ずチェックする
必須引数が渡されていない場合に適切なエラーメッセージを出して終了する習慣をつけましょう。
if [[ $# -lt 1 ]]; then
echo "エラー: 引数が足りません" >&2
usage
fi
エラーメッセージは stderr へ
echo "エラー: ..." >&2 で標準エラー出力に書くと、パイプラインでスクリプトの出力を別コマンドに渡していても、エラーメッセージがターミナルに表示されます。
まとめ
- 関数は
名前() { ... }で定義し、コマンドと同じ形で呼び出す - 関数内の変数は
localで宣言してグローバルスコープへの漏れを防ぐ - 関数の戻り値は
return(終了ステータス 0〜255)かecho+コマンド置換で返す $1、$2、$#、"$@"で引数を受け取る。"$@"は必ずダブルクォートで囲むgetoptsでフラグ形式のオプションを解析できる。コロン付き(o:)で引数付きオプションshift $((OPTIND - 1))でオプション処理後の位置引数を正しく参照するusage関数を書いてヘルプを整備する。エラー出力は>&2へ
次回は set -euo pipefail を中心に、堅牢なエラー処理を扱います。コマンドが失敗したときにスクリプトを自動停止させる仕組みや、trap を使ったクリーンアップ処理など、本番で使えるスクリプトを書くために押さえておきたい内容です。