Linux & IT ノート

シェルスクリプトの関数と引数処理:再利用できるスクリプトを書く

管理人 約12分で読めます

前回は変数・条件分岐・ループといった 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 に格納される)

OPTINDgetopts が処理した引数の次のインデックスを指しているため、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 からの exitexit 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 を使ったクリーンアップ処理など、本番で使えるスクリプトを書くために押さえておきたい内容です。