シェルスクリプトの関数に引数を渡す設計|位置パラメータの一括展開の違いと可変長引数・配列の受け渡し

宮崎智広 この記事の監修:宮崎智広(Linux実務・教育歴20年以上・受講者3,100名超)
HOMELinux技術 リナックスマスター.JP(Linuxマスター.JP)シェルスクリプト > シェルスクリプトの関数に引数を渡す設計|位置パラメータの一括展開の違いと可変長引数・配列の受け渡し
「スペースを含んだファイル名を関数に渡したら引数が途中で分割されてしまった」「$@ と $* はどちらも全引数を展開するのに、なぜ挙動が違うのか」

bash の関数で引数を受け取る処理を書いていると、こうした疑問にぶつかります。原因のほとんどは、クォートの有無による位置パラメータの展開の違いを正確に把握していないことです。

この記事では、bash の関数に引数を渡す設計に絞って解説します。$@ と $* の展開の違い(クォートあり・なし両方)から、引数個数が不定な可変長引数の処理パターン、そして配列を関数に渡す値渡しと declare -n によるリファレンス渡しまで、RHEL 9.4 / Ubuntu 24.04 LTS で動作確認した実行例とともに示します。関数の定義構文や return の基本はすでに把握済みの前提で進めます。

この記事のポイント

・"$@" を使えばスペース入り引数も1単語として保持されたまま展開される
・"$*" はIFSの先頭文字で全引数を連結した1つの文字列になる(通常は意図しない動作)
・可変長引数は $# で個数確認・shift でずらし・"$@" ループで全引数を処理する
・配列の受け渡しは declare -n nameref で変数名を渡すリファレンス渡しが確実


「このままじゃマズい」と感じていませんか?
参考書を開く気力もない、同年代に取り残される不安——
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら

$@ と $* の違いを正確に押さえる(位置パラメータの一括展開)

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

「hello world」が分割されて arg[1] と arg[2] になっています。$* でも全く同じ結果です。

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"

実行結果(RHEL 9.4 / bash 5.1):

arg[1]: hello world arg[2]: foo

「hello world」が1つの引数として保たれています。"$@" は呼び元の引数の境界を正確に維持します。

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

全引数が1つに連結されてしまいました。これは「引数の境界を壊す」動作です。

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 "インストール済みパッケージ"

実行結果(RHEL 9.4 / bash 5.1):

=== サーバーリスト === 要素数: 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

shift の直後に "$#" を確認するパターンが、固定引数と可変長引数の境界を安全に扱う定石です。

よくあるトラブルと対処

「$@」がクォートなしで使われていて引数が分割される

最もよく見るミスです。既存スクリプトを引き継いだ際には 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"
引数の設計次第で、関数の再利用性と堅牢性が大きく変わります。特に "$@" のクォートルールと、配列の nameref パターンは一度押さえると多くの場面で活用できます。シェルスクリプトを使った自動化や運用設計をさらに体系的に学びたい方は、下記の講座もあわせてご覧ください。
現場で通用する安全なLinuxサーバー構築の「型」を体系的に身につけたい方へ、20年以上の運用経験を持つ現役エンジニアが基礎から教えます。
シェルスクリプト講座を見る >>

無料メルマガで学習を続ける

Linuxの実践スキルをメールで毎週お届け。
登録は30秒、解除もいつでも可。

登録無料・いつでも解除できます

暗記不要・1時間後にはサーバーが動く

3,100名以上が実践した「型」を無料で公開中

プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。

姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら

Linux無料マニュアル(図解60P) 名前とメールで30秒登録
宮崎 智広

この記事を書いた人

宮崎 智広(みやざき ともひろ)

株式会社イーネットマーキュリー代表。現役のLinuxサーバー管理者として20年以上の実務経験を持ち、これまでに累計3,100名以上のエンジニアを指導してきたLinux教育のプロフェッショナル。「現場で本当に使える技術」を体系的に伝えることをモットーに、実践型のLinuxセミナーの開催や無料マニュアルの配布を通じてLinux人材の育成に取り組んでいる。

趣味は、キャンプにカメラ、トラウト釣り。好きな食べ物は、ラーメンにお酒。休肝日が作れない、酒量を減らせないのが悩み。最近、ドラマ「フライトエンジェル」を観て涙腺が崩壊しました。