bash の関数で引数を受け取る処理を書いていると、こうした疑問にぶつかります。原因のほとんどは、クォートの有無による位置パラメータの展開の違いを正確に把握していないことです。
この記事では、bash の関数に引数を渡す設計に絞って解説します。$@ と $* の展開の違い(クォートあり・なし両方)から、引数個数が不定な可変長引数の処理パターン、そして配列を関数に渡す値渡しと declare -n によるリファレンス渡しまで、RHEL 9.4 / Ubuntu 24.04 LTS で動作確認した実行例とともに示します。関数の定義構文や return の基本はすでに把握済みの前提で進めます。
この記事のポイント
・"$@" を使えばスペース入り引数も1単語として保持されたまま展開される
・"$*" はIFSの先頭文字で全引数を連結した1つの文字列になる(通常は意図しない動作)
・可変長引数は $# で個数確認・shift でずらし・"$@" ループで全引数を処理する
・配列の受け渡しは declare -n nameref で変数名を渡すリファレンス渡しが確実
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
$@ と $* の違いを正確に押さえる(位置パラメータの一括展開)
bash の関数で引数を渡す方法の中心にあるのが、位置パラメータの一括展開です。$@ と $* はともに「全引数を展開する」特殊パラメータですが、クォートで囲んだときの挙動が根本的に異なります。1. クォートなしの $@ と $*(挙動は同じ)
クォートなしで使うと、$@ と $* はどちらも同じように動作します。IFS(デフォルトはスペース・タブ・改行)に基づいて単語分割が行われ、スペースを含む引数は分割されてしまいます。以下のスクリプトで確認します。
#!/bin/bash show_args() { local i=1 for arg in $@; do echo " arg[$i]: $arg" i=$((i + 1)) done } # スペースを含む引数を渡す show_args "hello world" "foo"
arg[1]: hello arg[2]: world arg[3]: foo
2. "$@" ─ クォートあり(引数を1つずつ分離して展開)
"$@" はダブルクォートで囲んだとき、各引数を個別の単語として展開します。これが関数への引数渡しで正しい選択肢です。#!/bin/bash show_args() { local i=1 for arg in "$@"; do # ダブルクォートで囲む echo " arg[$i]: $arg" i=$((i + 1)) done } show_args "hello world" "foo"
arg[1]: hello world arg[2]: foo
3. "$*" ─ クォートあり(全引数を1つの文字列に連結)
"$*" をダブルクォートで囲むと、全引数を IFS の先頭文字(デフォルトはスペース)で連結した1つの文字列として展開します。#!/bin/bash show_args() { local i=1 for arg in "$*"; do # "$*" は1つの文字列になる echo " arg[$i]: $arg" i=$((i + 1)) done } show_args "hello world" "foo"
arg[1]: hello world foo
4. "$@" と "$*" の使い分け
・"$@" を使う場面:関数の全引数を別の関数やコマンドにそのまま転送するとき。引数の個数と境界を保持したい場合は常に "$@" を使います・"$*" を使う場面:ログ出力など、全引数を1行の文字列として記録したいとき(例:
echo "引数: $*")設計方針としては「迷ったら "$@" 一択」と覚えてください。$* が必要な場面はごく限られています。
可変長引数の設計パターン(引数個数が不定の関数)
実務では、受け取る引数の個数があらかじめ決まらない関数を書く機会が多くあります。ファイルを複数受け取るバックアップ関数や、複数のホストにコマンドを送る関数などがその例です。1. $# で引数の個数を確認する
$# は関数に渡された引数の個数を返します。引数がゼロのときのガード処理や、最低引数数の検証に使います。#!/bin/bash backup_files() { if [ "$#" -eq 0 ]; then echo "エラー: バックアップ対象ファイルを1つ以上指定してください" >&2 return 1 fi echo "バックアップ対象: $# ファイル" for f in "$@"; do echo " 処理中: $f" # cp "$f" /backup/ などの実処理をここに書く done } backup_files /etc/nginx/nginx.conf "/var/log/nginx/access.log"
バックアップ対象: 2 ファイル 処理中: /etc/nginx/nginx.conf 処理中: /var/log/nginx/access.log
2. shift で先頭引数をずらす
引数の先頭に「モード」や「オプション」のような固定引数を1つ置き、残りを可変長引数として処理したい場合、shift を使います。shift を実行すると $1 が取り除かれ、$2 が新しい $1 になります。#!/bin/bash # 第1引数: 出力先ディレクトリ(固定) # 第2引数以降: コピー対象ファイル(可変) copy_files() { local dest="$1" shift # $1 を取り除く。$2 以降が新たな "$@" になる if [ "$#" -eq 0 ]; then echo "エラー: コピー元ファイルを指定してください" >&2 return 1 fi echo "コピー先: $dest 対象: $# ファイル" for src in "$@"; do echo " cp $src -> $dest" done } copy_files /backup/2026-09-09 nginx.conf access.log error.log
コピー先: /backup/2026-09-09 対象: 3 ファイル cp nginx.conf -> /backup/2026-09-09 cp access.log -> /backup/2026-09-09 cp error.log -> /backup/2026-09-09
shift N(N は取り除く個数)で一括してずらせます。3. for ループで全引数を処理するパターン(実務例)
可変長引数を受け取る関数の典型的な実装が「"$@" を for ループで処理する」パターンです。以下はログファイルを複数受け取って各ファイルのエラー行数を集計する例です。#!/bin/bash count_errors() { local total=0 for logfile in "$@"; do if [ ! -f "$logfile" ]; then echo " スキップ(存在しない): $logfile" >&2 continue fi local n n=$(grep -c "ERROR" "$logfile" 2>/dev/null || echo 0) echo " $logfile: ${n} 件" total=$((total + n)) done echo "合計エラー数: $total" } count_errors /var/log/app/app.log /var/log/app/worker.log
/var/log/app/app.log: 3 件 /var/log/app/worker.log: 12 件 合計エラー数: 15
continue でファイルが存在しない場合をスキップしているため、部分的に欠けた入力に対しても堅牢に動作します。配列を関数に渡す設計(値渡しとリファレンス渡し)
bash の関数は配列を引数として直接受け取ることができません。配列を関数に渡すには「値渡し」か「リファレンス渡し」の2つの方法があります。1. 値渡し ─ "${array[@]}" で全要素を展開して渡す
最もシンプルな方法は、配列の全要素を "$@" で展開して渡すことです。関数の中では $1, $2, $3 ... として受け取れます。#!/bin/bash print_list() { echo "受け取った要素数: $#" local i=1 for item in "$@"; do echo " [$i] $item" i=$((i + 1)) done } servers=("web-01" "web-02" "db-01") print_list "${servers[@]}"
受け取った要素数: 3 [1] web-01 [2] web-02 [3] db-01
・メリット:シンプルで、bash のバージョンを問わず動く
・注意点:関数内では元の「配列」としての構造は失われ、位置パラメータとして受け取ることになる。複数の配列を同時に渡すことはできない
2. リファレンス渡し ─ declare -n で変数名を受け取る(bash 4.3 以降)
配列を「配列として」扱いたい場合、変数名(文字列)を関数に渡し、関数内でdeclare -n による nameref(名前参照)を使う方法が確実です。#!/bin/bash # 第1引数: 配列変数名(文字列として渡す) # 第2引数: 出力ラベル print_array() { declare -n arr_ref="$1" # nameref: arr_ref は $1 が指す変数の別名 local label="$2" echo "=== ${label} ===" echo "要素数: ${#arr_ref[@]}" for item in "${arr_ref[@]}"; do echo " - $item" done } hosts=("web-01" "web-02" "db-01") print_array hosts "サーバーリスト" packages=("nginx" "postgresql16" "firewalld") print_array packages "インストール済みパッケージ"
=== サーバーリスト === 要素数: 3 - web-01 - web-02 - db-01 === インストール済みパッケージ === 要素数: 3 - nginx - postgresql16 - firewalld
declare -n arr_ref="$1" は「$1 が指す名前の変数を arr_ref という名前で参照する」という宣言です。複数の配列を同時に渡すことができ、元の配列の要素数や添字も正確に取得できます。・必要バージョン:bash 4.3 以降(RHEL 7 / CentOS 7 以降の bash は 4.3+ のため現場での利用可)
・注意点:nameref の変数名(arr_ref)と引数で渡す変数名が同じだと循環参照エラーになる。意図的に異なる名前を使うこと
実務で役立つ引数設計パターン
1. 引数チェックとデフォルト値の設定
関数を汎用的に作る際は、引数が省略されたときのデフォルト値の設定と、必須引数の検証がセットになります。#!/bin/bash # 使い方: archive_logs [対象ディレクトリ] [保存先] [保存日数] # 例: archive_logs /var/log/nginx /backup/logs 30 archive_logs() { local src="${1:?エラー: 対象ディレクトリを指定してください}" local dest="${2:-/backup/logs}" # 省略時は /backup/logs local days="${3:-30}" # 省略時は30日 echo "対象: $src" echo "保存先: $dest" echo "保存日数: $days 日" } # 必須引数あり、オプション引数は省略 archive_logs /var/log/nginx
対象: /var/log/nginx 保存先: /backup/logs 保存日数: 30 日
${1:?メッセージ} は $1 が未設定または空文字のとき、メッセージを stderr に出力してスクリプトを終了します。必須引数の検証をシンプルに書けるイディオムです。2. 固定引数と可変長引数を組み合わせる設計
「第1引数は操作の種類(固定)、残りはターゲット(可変)」という構成は、ラッパー関数でよく使われます。#!/bin/bash service_control() { local action="${1:?アクションを指定してください (start|stop|status)}" shift if [ "$#" -eq 0 ]; then echo "エラー: サービス名を1つ以上指定してください" >&2 return 1 fi for svc in "$@"; do echo "systemctl $action $svc" # systemctl "$action" "$svc" # 実際に実行する場合はコメントを外す done } service_control start nginx postgresql firewalld
systemctl start nginx systemctl start postgresql systemctl start firewalld
よくあるトラブルと対処
「$@」がクォートなしで使われていて引数が分割される
最もよく見るミスです。既存スクリプトを引き継いだ際には for ループの先頭を確認してください。・NG:
for arg in $@; do(スペース含む引数が分割される)・OK:
for arg in "$@"; do(引数の境界が保持される)shift が多すぎて "$@" が空になる
shift の回数が $# を超えるとエラーになります(bash のバージョンによっては無視されるが移植性がない)。shift 前に $# を確認するか、shift $((n < $# ? n : $#)) のように安全にずらす書き方を使います。# 安全な shift: $# より多くずらさない safe_shift() { local n=${1:-1} shift $((n < $# ? n : $#)) }
declare -n の nameref が循環参照になる
declare -n arr="$1" と書いたとき、$1 の値が「arr」そのものだとエラーになります。・NG:
myfunction arr の中で declare -n arr="$1"(arr → arr の循環)・OK:nameref の変数名にプレフィックスをつける(例:
declare -n _arr_ref="$1")関数内で "$@" の内容が意図と違う
関数を別の関数から呼び出す場合、$@ は「現在の関数の引数」になります。呼び元の引数をそのまま転送するには明示的に "$@" を使って渡す必要があります。outer() { echo "outer の引数: $*" inner "$@" # outer の $@ をそのまま inner に転送 } inner() { echo "inner の引数: $*" } outer "hello world" foo
outer の引数: hello world foo inner の引数: hello world foo
本記事のまとめ
bash の関数に引数を渡す設計のポイントをまとめます。| やりたいこと | 記法・コマンド |
|---|---|
| 全引数を個別に展開する(正しい方法) | "$@" |
| 全引数を1つの文字列として取得する | "$*" |
| 引数の個数を確認する | $# |
| 先頭引数を取り除く | shift(複数個は shift N) |
| 全引数をループ処理する | for arg in "$@"; do ... done |
| 引数省略時のデフォルト値を設定する | ${1:-デフォルト値} |
| 必須引数が未指定なら終了する | ${1:?エラーメッセージ} |
| 配列を値渡しする | myfunc "${array[@]}" |
| 配列をリファレンス渡しする(bash 4.3+) | declare -n ref="$1" |
シェルスクリプト講座を見る >>
3,100名以上が実践した「型」を無料で公開中
プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。
姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら
- 次のページへ:シェルスクリプトのmain関数パターン|関数定義と実処理を分けて読み込み時の暴発を防ぐ構成
- 前のページへ:シェルスクリプトの関数名の衝突を防ぐ設計|プレフィックス命名とdeclare -Fで既存の外部コマンド上書きを避ける
- この記事の属するカテゴリ:シェルスクリプトへ戻る

無料メルマガで学習を続ける
Linuxの実践スキルをメールで毎週お届け。
登録は30秒、解除もいつでも可。
登録無料・いつでも解除できます