シェルスクリプトを書いていて、同じ処理を何度もコピペしてスクリプトが肥大化してしまった経験はありませんか?現場での自動化や保守において 「コピペコードの重複は負け」 です。共通処理を部品化し、メンテナンス性を高める最大の武器が 「関数(Function)」 です。
しかし、Bashの関数は一般的なプログラミング言語(PythonやJavaScriptなど)とは挙動が大きく異なります。特に 「 return コマンドで文字列を返そうとして失敗する」 、 「 local を付け忘れて変数が意図せず上書きされる」 といった罠は、多くのエンジニアが必ず一度はハマる関門です。
この記事では、Bash関数の基本構文・呼び出し方から、引数のスコープ、 return と echo の使い分け、そして変数の局所化( local )まで、実務ですぐに役立つ実践テクニックを網羅して徹底解説します。
【アンサーファースト】実務で即役立つ!Bash関数の理想的テンプレート
まずは「今すぐコピペして安全に使える」実務レベルの関数テンプレートを提示します。引数チェック、ローカル変数の宣言、終了ステータス判定、値の返却がすべて盛り込まれています。
#!/usr/bin/env bash
set -euo pipefail
# ==========================================
# 関数名: calculate_tax
# 概要 : 税抜金額と税率から税込金額を計算して返す
# 引数 : $1 - 金額(数値)
# $2 - 税率(百分率、省略時は 10)
# 戻り値: stdout - 計算結果(税込金額)
# 終了値: 0 - 成功, 1 - 引数不正エラー
# ==========================================
calculate_tax() {
# 1. local宣言でスコープを関数内に限定(グローバル変数汚染を防ぐ)
local price="${1:-}"
local rate="${2:-10}"
local total=0
# 2. 引数のバリデーション(必須チェック & 数値チェック)
if [[ -z "$price" || ! "$price" =~ ^[0-9]+$ ]]; then
echo "エラー: 第1引数に正の整数(金額)を指定してください。" >&2
return 1
fi
if [[ ! "$rate" =~ ^[0-9]+$ ]]; then
echo "エラー: 第2引数に正の整数(税率)を指定してください。" >&2
return 1
fi
# 3. 業務計算の実行
total=$(( price * (100 + rate) / 100 ))
# 4. 計算結果は echo で標準出力に出力(呼び出し元でコマンド置換で受け取る)
echo "$total"
# 5. 正常終了時は return 0(省略しても直前コマンドのステータスが返るが明示が安全)
return 0
}
# --- 関数の呼び出し実例 ---
echo "=== 正常呼び出し ==="
# コマンド置換 $(...) で関数の echo 出力を変数にキャッチ
if result=$(calculate_tax 1000 10); then
echo "計算成功: 税込金額は ${result} 円です"
else
echo "計算に失敗しました"
fi
echo ""
echo "=== エラー呼び出し(引数不足) ==="
if result=$(calculate_tax "abc"); then
echo "計算成功: ${result}"
else
echo "計算エラーを正しく検知しました(終了ステータス: $?)"
fi
実行結果:
=== 正常呼び出し ===
計算成功: 税込金額は 1100 円です
=== エラー呼び出し(引数不足) ===
エラー: 第1引数に正の整数(金額)を指定してください。
計算エラーを正しく検知しました(終了ステータス: 1)
このテンプレートには、実務でトラブルを防ぐための 「5大鉄則」 が凝縮されています:
- 関数内変数はすべて
localで宣言する(グローバル汚染の防止) - 関数の冒頭で引数のバリデーションを実施する(想定外の挙動の防止)
- 計算結果・文字列は
echoで返し、呼び出し元で$(func)で受け取る - エラーメッセージは必ず標準エラー出力(
>&2)へ流す(戻り値汚染の防止) - 成否ステータスは
return 0/return 1で制御する
Bash関数の基本構文と定義方法
Bashで関数を定義する構文には主に2種類の書き方があります。現場のシェルスクリプトでどちらを使うべきか、理由とともに解説します。
基本書式: function構文と簡略構文(name() { … } 推奨)
Bashの関数定義構文には以下のバリエーションが存在します。
| 構文パターン | 記述例 | 互換性・推奨度 | 特徴 |
|---|---|---|---|
| ① 簡略構文(推奨) | my_func() { ... } |
POSIX準拠(最推奨) | すべての標準シェル(sh, dash, zsh等)で動作する。最もシンプル。 |
| ② functionキーワード構文 | function my_func { ... } |
Bash固有(非推奨) | shやPOSIX環境では構文エラーになる。括弧を書かない。 |
| ③ 併用構文 | function my_func() { ... } |
Bash固有(冗長) | 動作はするが function キーワードと () の二重指定で冗長。 |
実務では、他のUnix環境や /bin/sh での実行時にも移植性が高く、記法がスッキリしている ① 簡略構文( func_name() { ... } ) を採用するのが業界の標準ルールです。
# 【推奨】POSIX互換の標準的な関数定義
greet() {
echo "Hello, Bash World!"
}
関数の呼び出し方(括弧は不要、コマンドと同じ形式)
他言語からBashに入った人が最も戸惑うのが、関数の呼び出し方です。Bashの関数呼び出しには greet() のような括弧を付けてはいけません。
# NGな呼び出し例(構文エラーや予期せぬ動作の原因)
greet() # エラー
greet("Taro") # エラー
# OKな呼び出し例(Linuxコマンドと全く同じ形式)
greet
greet "Taro"
Bashにおいて、関数は「自作の内部コマンド」として扱われます。 ls -l /tmp とコマンドを実行するのと同じように、関数名の後ろにスペースを空けて引数を並べます。
注意:関数は呼び出しより「上」で定義する
コンパイル言語(C言語やGoなど)やPythonと異なり、シェルスクリプトはインタープリタが上から1行ずつ順に解釈します。そのため、関数を呼び出すコードよりも前に、関数の定義が記述されていなければなりません。
#!/usr/bin/env bash
# NG: 定義前に呼び出すとコマンド未検出エラーになる
say_hello # bash: say_hello: command not found
say_hello() {
echo "Hello!"
}
# OK: 定義後に呼び出す
say_hello # 正常に出力される
関数への引数の渡し方と受け取り方
関数にパラメータを渡し、関数内部でそれを受け取る仕組みを解説します。
スクリプト引数と関数引数のスコープ(関数内では は関数の第1引数)
Bashでは、コマンドライン引数を受け取る際に $1, $2 などの位置パラメータを使用しますが、関数内部に入ると、位置パラメータは「関数の引数」に自動的に切り替わります。
#!/usr/bin/env bash
show_args() {
echo "関数の第1引数 ($1): $1"
echo "関数の引数の総数 ($#): $#"
}
echo "スクリプト全体の第1引数 ($1): $1"
# 関数に新しい引数を渡して呼び出す
show_args "FunctionArg1" "FunctionArg2"
echo "関数実行後のスクリプトの第1引数 ($1): $1"
実行結果:
$ ./args_demo.sh ScriptArg1
スクリプト全体の第1引数 ($1): ScriptArg1
関数の第1引数 ($1): FunctionArg1
関数の引数の総数 ($#): 2
関数実行後のスクリプトの第1引数 ($1): ScriptArg1
関数内部の $1 は FunctionArg1 に上書きされていますが、関数を抜けるとスクリプト本来の $1 (ScriptArg1)に戻っていることが分かります。
位置パラメータの基礎や特殊変数($#, $@, $*)の詳しい仕様については、関連記事の シェルスクリプトの引数処理まとめ|位置パラメータ・デフォルト値・$@と$*の違いを徹底解説 を参照してください。
複数の引数を渡す・受け取る実例
関数内で受け取った引数は、必ず local 変数に分かりやすい名前を付けて代入するのが安全なコードを書く秘訣です。また、10個以上の引数を扱う場合は ${10} のように波括弧で囲む必要があります。
deploy_app() {
# 意味のある変数名に格納する
local env_name="${1:-}"
local app_version="${2:-}"
local dry_run="${3:-false}"
# 必須チェック
if [[ -z "$env_name" || -z "$app_version" ]]; then
echo "エラー: 環境名とバージョンを指定してください。" >&2
return 1
fi
echo "環境: $env_name に バージョン: $app_version をデプロイします(DryRun: $dry_run)"
}
# 呼び出し
deploy_app "production" "v2.4.0" "true"
配列を関数に渡すテクニック(${array[@]} の展開)
関数に配列を丸ごと渡したい場合、通常は "${array[@]}" を展開して渡します。関数側ではすべての要素を "$@" でループ処理できます。
#!/usr/bin/env bash
# 配列の全要素を出力する関数
process_servers() {
local servers=("$@") # 渡された全引数を新しいローカル配列に再構築
echo "処理対象サーバー数: ${#servers[@]}"
for server in "${servers[@]}"; do
echo " - 接続中: $server"
done
}
target_list=("web01.example.com" "web02.example.com" "db01.example.com")
# 配列を展開して関数に渡す
process_servers "${target_list[@]}"
※Bashでの配列操作の全体像は、【Bash】シェルスクリプトでの配列の使い方まとめ|宣言・追加・ループ処理・要素数を徹底解説 で網羅しています。
ここで必ずハマる!関数の「戻り値」の正しい設計
Bashを学ぶエンジニアが最も頻繁に引っかかるのが、関数の 「戻り値(Return Value)」 です。他言語の感覚で return "Hello" と書くと、思わぬバグに直面します。
【早見表】Bash関数の値返却アプローチ比較
| 手法 | 返せる内容 | 取得方法 | 主な用途 |
|---|---|---|---|
return N |
0〜255 の整数のみ(終了ステータス) | 直後の $? または if func; then |
処理の成功(0)/ 失敗(1〜255)の判定 |
echo "..." + コマンド置換 |
任意の文字列・数値・複数行テキスト | result=$(func) |
計算結果や加工した文字列の受け渡し |
| グローバル変数への代入 | 任意のデータ型 | 変数名を直接参照 | 高速だが副作用が強くバグの温床になりやすい |
※ より高度な関数設計パターン(printf -v による呼び出し元変数への直接格納や local -n による参照渡しなど)については、関連記事の「関数設計パターン集|引数/戻り値の流儀」も併せて参照してください。
returnコマンドの真実(終了ステータス 0〜255 しか返せない)
Bashの return は、関数の終了コード(Exit Status)を返すためのコマンドです。数値(0〜255)しか返せません。
# NG例1: 文字列をreturnしようとすると構文エラー
get_greeting() {
return "Hello" # bash: return: Hello: numeric argument required
}
# NG例2: 256以上の数値を返すと256で割った余り(mod 256)になる罠!
calc_add() {
return 300
}
calc_add
echo "戻り値: $?" # 300 ではなく 「44」 (300 % 256 = 44)が出力される!
特に return 256 を実行すると、256 % 256 = 0 となり、エラーのはずが「成功(0)」と判定されてしまう致命的なバグを引き起こします。計算結果やデータ自体を return で返してはいけません。
文字列や計算結果を返す標準パターン(echo してコマンド置換 $(func) で受け取る)
文字列や計算結果を返したい場合の業界標準パターンは、「関数内で echo(または printf)し、呼び出し元でコマンド置換 $(...) で受け取る」 方法です。
#!/usr/bin/env bash
# 文字列加工関数
sanitize_name() {
local raw_name="$1"
# 小文字変換して空白をハイフンに置換
local clean_name
clean_name=$(echo "$raw_name" | tr '[:upper:]' '[:lower:]' | tr ' ' '-')
# 結果を標準出力に出力する
echo "$clean_name"
}
# 呼び出し: $(...) で結果をキャッチする
user_input="My Test Project"
normalized=$(sanitize_name "$user_input")
echo "正規化されたプロジェクト名: $normalized"
# 出力: my-test-project
成功/失敗(True/False)を判定する関数の書き方
関数の成否を判定したい場合は、return 0(成功)と return 1(失敗)を使います。Bashの if 文はコマンドの終了ステータスをそのまま評価できるため、非常にスマートに記述できます。
# ポートが開いているか確認する関数
is_port_open() {
local host="$1"
local port="$2"
if nc -z -w2 "$host" "$port" 2>/dev/null; then
return 0 # 成功(ポート開放)
else
return 1 # 失敗(接続不可)
fi
}
# if文で直接関数を実行して判定
if is_port_open "127.0.0.1" 80; then
echo "Webサーバーは正常に稼働しています"
else
echo "Webサーバーへの接続に失敗しました"
fi
条件分岐の詳細や test コマンドの記法は、test コマンドの使い方|bashで条件式を評価する方法 も参考にしてください。
変数のスコープ管理(local宣言の必須性)
シェルスクリプトで大規模なバグを引き起こす最大の原因の一つが、変数のスコープ(有効範囲)です。
localを付けないと全変数がグローバルになる危険性
Bashでは、関数内で定義した変数であっても、明示的に local を付けない限りすべてグローバル変数になります。呼び出し元の変数を意図せず上書きしてしまう恐ろしい事故の例を見てみましょう。
#!/usr/bin/env bash
# カウンタ変数 i を使う関数(localを忘れた悪い例)
bad_func() {
for i in {1..3}; do
echo " [bad_func] i = $i"
done
}
# メイン処理のループ
for i in {1..2}; do
echo "[Main Loop] 開始 i = $i"
bad_func
echo "[Main Loop] 終了 i = $i"
done
実行結果:
[Main Loop] 開始 i = 1
[bad_func] i = 1
[bad_func] i = 2
[bad_func] i = 3
[Main Loop] 終了 i = 3
メイン側の i が 1 からスタートしたにもかかわらず、関数内のループで i が 3 に書き換えられたため、メインループが予期せぬ終了を迎えてしまいました。もし while ループでこの現象が起きると、無限ループに陥ってCPUが100%に張り付くことになります。
これを防ぐため、関数内で定義する変数は例外なく local を付与してください。
good_func() {
local i # ローカル変数として宣言
for i in {1..3}; do
echo " [good_func] i = $i"
done
}
読み取り専用変数(readonly / local -r)の活用
変更されては困る定数や設定値には、readonly や local -r を指定します。誤って再代入しようとするとエラーが発生するため、スクリプトの堅牢性が一段と高まります。
setup_config() {
local -r MAX_RETRY=5
local -r TIMEOUT_SEC=30
echo "最大リトライ回数: $MAX_RETRY"
# 再代入を試みるとBashがエラーを吐いてスクリプトを保護
# MAX_RETRY=10 # bash: MAX_RETRY: readonly variable
}
実務で使える頻出パターン集(コピペ用)
現場でそのまま再利用できる実用的な関数パターンを厳選して紹介します。
パターン1: ログレベル・タイムスタンプ付きロガー関数
スクリプトの標準出力を美しく整え、トラブルシューティングを容易にする定番の関数です。
log_info() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [INFO] $*"
}
log_error() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [ERROR] $*" >&2
}
log_info "バッチ処理を開始します(PID: $$)"
log_error "設定ファイルが見つかりません"
パターン2: コマンド・ツールの存在チェック関数
スクリプト実行に必要な外部コマンド(jq, curl, docker など)がインストールされているかを事前に確認します。
check_command() {
local cmd="$1"
if ! command -v "$cmd" >/dev/null 2>&1; then
echo "エラー: 必要なコマンド '$cmd' がインストールされていません。" >&2
return 1
fi
return 0
}
# 必須コマンドの確認
for req_cmd in "curl" "jq" "tar"; do
check_command "$req_cmd" || exit 1
done
パターン3: 一時ファイル作成と安全な自動削除(trap連携)
関数内で作成した作業用一時ディレクトリを、スクリプト終了時に自動で消去する安全設計です。
create_temp_workspace() {
local temp_dir
temp_dir=$(mktemp -d /tmp/script_work.XXXXXX)
# スクリプト終了時(正常終了・エラー終了問わず)に削除
trap 'rm -rf "$temp_dir"' EXIT
echo "$temp_dir"
}
work_dir=$(create_temp_workspace)
echo "作業用一時ディレクトリを作成: $work_dir"
# スクリプトが終了すると自動的に rm -rf される
よくあるエラー・落とし穴と対処法
Bash関数を作成・実行する際によくある落とし穴と、その解決策をまとめました。
落とし穴1: デバッグ用echoが戻り値に混入してしまう
関数内で計算結果を echo して $(func) で受け取る構成にしているとき、途中で echo "デバッグ中..." などを書いてしまうと、そのデバッグ文も戻り値の一部として合体してしまいます。
# NGな例
get_user_id() {
echo "DEBUG: ユーザーIDを検索中..." # これも戻り値に入ってしまう!
echo "10042"
}
uid=$(get_user_id)
echo "取得結果: [$uid]"
# 出力結果: [DEBUG: ユーザーIDを検索中... 10042]
【対処法】 デバッグ出力や案内メッセージは、必ず標準エラー出力( >&2 )にリダイレクトしてください。
# OKな例
get_user_id() {
echo "DEBUG: ユーザーIDを検索中..." >&2 # 標準エラー出力へ流す
echo "10042"
}
uid=$(get_user_id) # 標準出力(10042)のみが格納される
落とし穴2: コマンド置換 $(func) 内での変数変更は親シェルに反映されない
$(func) (コマンド置換)を実行すると、Bashはサブシェル(子プロセス)をフォークして関数を実行します。そのため、関数内でグローバル変数を変更しても、親プロセスの変数は書き換わりません。
COUNT=0
increment() {
COUNT=$((COUNT + 1))
echo "OK"
}
res=$(increment) # サブシェルで実行される
echo "COUNT = $COUNT" # 1 ではなく 「0」 のまま!
【対処法】 値を渡す場合は、コマンド置換経由で受け取るか、サブシェルを使わずに通常呼び出し( increment )を行ってください。
まとめ
Bash関数の基本から実践的な設計手法までを解説しました。実務で関数を作成する際は、以下のポイントを常に意識してください。
- 関数定義は
func_name() { ... }の簡略構文を採用する(POSIX準拠で最も安全) - 関数内のすべての変数は
localを付けて宣言する(グローバル汚染によるバグを根絶) - 関数の戻り値は「成否=
return」「データ=echo+$(...)」と明確に使い分ける returnに指定できるのは 0〜255 の整数のみ(256以上の数値や文字列はNG)- 案内・エラーメッセージは
>&2にリダイレクトして標準出力の戻り値を汚さない
関数を正しく使いこなせるようになると、シェルスクリプトの保守性・再利用性が飛躍的に向上します。ぜひ日常のスクリプト開発で活用してください。

コメント