シェルスクリプトの入力バリデーション設計|型・形式チェック関数をライブラリ化して複数スクリプトで再利用する方法

宮崎智広 この記事の監修:宮崎智広(Linux実務・教育歴20年以上・受講者3,100名超)
HOMELinux技術 リナックスマスター.JP(Linuxマスター.JP)シェルスクリプト > シェルスクリプトの入力バリデーション設計|型・形式チェック関数をライブラリ化して複数スクリプトで再利用する方法
「引数を入れ忘れてスクリプトが暴走した」「数値を期待しているのに文字列が来てエラーで止まった」という経験はないだろうか。
シェルスクリプトは手軽に書けるがゆえに、入力バリデーションを後回しにしたまま本番運用に持ち込まれるケースが多い。引数が空でも処理が進み、想定外のファイルが削除されるまで問題に気づかないのは、バリデーションが設計に組み込まれていないからだ。

この記事では、シェルスクリプトにおける引数・環境変数・ファイルパスの型・形式・範囲チェックを、再利用可能な関数群としてライブラリ化する設計パターンを解説する。RHEL 9.4 / Rocky Linux 9.4 / Ubuntu 24.04 LTS で動作確認済みの実装例を示す。

この記事のポイント

・バリデーション不在のスクリプトは引数ミスで本番データを破壊するリスクがある
・整数チェックは [[ $val =~ ^[0-9]+$ ]] の1行で実装できる
・チェック関数を lib/validate.sh にまとめると複数スクリプトで source 再利用できる
・usage 関数と exit 1 をセットにしてスクリプト自身が使い方を提示する設計が鉄則


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

なぜシェルスクリプトの入力バリデーションが必要か

バリデーションを省略したスクリプトがどう壊れるか、実際に起きやすい障害を3つ示す。

ケース1:引数が空のままrsyncを実行した
バックアップスクリプトで第2引数(バックアップ先)を入れ忘れた。スクリプトは引数チェックなしで実行を続け、rsync -av /data/ "" がカレントディレクトリへの転送と解釈され、大量のファイルが意図しない場所にコピーされた。

ケース2:整数を期待しているのに文字列が来た
世代保持日数を引数で受け取るスクリプトで、誰かが "30days" と入力した。if (( days > 30 )); の arithmetic expansion がエラーになり、スクリプトが途中で停止した。バックアップファイルの削除処理が走らず、ディスクが溢れた。

ケース3:存在しないパスへのmkdir
ログ出力先のディレクトリを環境変数 LOG_DIR から取るスクリプトで、変数が未設定のままだった。mkdir -p はエラーにならず、以降の tee コマンドがルートに近い場所にファイルを作ってしまった。

いずれも「実行前に入力値が正しいか確認する」という1ステップで防げた障害だ。バリデーションは後付けの保険ではなく、スクリプト設計の最初に組み込む骨格だ。

引数の存在チェックとusage関数の設計

まず最も基本的な「引数の個数チェックとusage表示」から設計する。

1. 引数個数のチェック

#!/bin/bash set -euo pipefail usage() { cat >&2 <<'EOF' Usage: SCRIPT_NAME src_dir : バックアップ元ディレクトリ(例: /var/data) dst_dir : バックアップ先ディレクトリ(例: /mnt/backup) retention_days : 世代保持日数(1以上の整数) Example: SCRIPT_NAME /var/data /mnt/backup 30 EOF exit 1 } # 引数が3つ以外のとき即座にusageを表示して終了 [ $# -eq 3 ] || usage src_dir=$1 dst_dir=$2 retention_days=$3

usage() 内の cat >&2 <<'EOF' でヒアドキュメントを標準エラーに出力し、最後に exit 1 で終了する。<<'EOF'(シングルクォートEOF)にすることで、usageメッセージ内でシェル変数が展開されるのを防いでいる。この設計により、引数が足りない場合は必ずusageが表示されてから終了するため、呼び出し元のcronやシェルからも終了コード1で失敗を検知できる。

なお、スクリプト名を動的に表示したい場合は <<EOF(クォートなし)にして $0 または $(basename "$0") を埋め込む。

2. 引数の空文字チェック関数

引数の個数チェックをパスしても、空文字が渡される場合がある。set -u を有効にしていると未定義変数でエラーになるが、引数に空文字を渡された場合は検知できない。

# 引数の空チェック関数 check_nonempty() { local name=$1 local val=${2:-} if [[ -z $val ]]; then echo "ERROR: $name が空文字です" >&2 exit 1 fi } # 使用例 check_nonempty "src_dir" "$src_dir" check_nonempty "dst_dir" "$dst_dir" check_nonempty "retention_days" "$retention_days"

${2:-} とすることで、set -u 環境でも引数が渡されなかった場合に空文字として安全に処理できる。

型チェックの実装(整数・数値・範囲)

1. 整数チェック

# 整数チェック(負の整数も許可する場合) is_integer() { [[ ${1:-} =~ ^-?[0-9]+$ ]] } # 正の整数のみ許可(0は除く) is_positive_int() { [[ ${1:-} =~ ^[1-9][0-9]*$ ]] } # 使用例:世代保持日数は正の整数でなければならない if ! is_positive_int "$retention_days"; then echo "ERROR: retention_days は1以上の整数で指定してください(指定値: $retention_days)" >&2 exit 1 fi

[[ =~ ]] 内の正規表現はクォートで囲まずに直接記述する。クォートで囲むとリテラル文字列として扱われてしまうため注意が必要だ。正規表現を変数に入れる場合も同様に、変数参照時はクォートしない。

2. 範囲チェック

# 整数の範囲チェック(min以上max以下) is_in_range() { local val=$1 min=$2 max=$3 is_integer "$val" || return 1 (( val >= min && val <= max )) } # 使用例:保持日数は1以上365以下 if ! is_in_range "$retention_days" 1 365; then echo "ERROR: retention_days は 1 以上 365 以下で指定してください(指定値: $retention_days)" >&2 exit 1 fi

3. 数値チェック(小数点を含む場合)

# 小数点を含む数値チェック is_number() { [[ ${1:-} =~ ^-?[0-9]+(\.[0-9]+)?$ ]] } # 文字列長チェック(min_len 以上 max_len 以下) check_str_length() { local name=$1 val=$2 min=$3 max=$4 local len=${#val} if (( len < min || len > max )); then echo "ERROR: $name の長さは ${min}以上${max}以下にしてください(現在: ${len}文字)" >&2 exit 1 fi }

形式チェックの実装(IPアドレス・日付・選択肢)

1. IPアドレス(IPv4)チェック

is_ipv4() { local re='^([0-9]{1,3}\.){3}[0-9]{1,3}$' [[ ${1:-} =~ $re ]] || return 1 # 各オクテットが 0 以上 255 以下かチェック local IFS='.' arr read -r -a arr <<< "$1" local o for o in "${arr[@]}"; do (( o >= 0 && o <= 255 )) || return 1 done } # 使用例 server_ip="192.168.10.256" if ! is_ipv4 "$server_ip"; then echo "ERROR: $server_ip は正しいIPv4アドレスではありません" >&2 exit 1 fi

正規表現だけでは 999.999.999.999 を通してしまうため、オクテットの数値範囲チェックを組み合わせている。実際の実行例を確認する。

# Rocky Linux 9.4 での動作確認 $ is_ipv4 "192.168.1.100" && echo OK || echo NG OK $ is_ipv4 "192.168.10.256" && echo OK || echo NG NG $ is_ipv4 "not-an-ip" && echo OK || echo NG NG

2. 日付形式(YYYY-MM-DD)チェック

is_date_ymd() { local re='^[0-9]{4}-[0-1][0-9]-[0-3][0-9]$' [[ ${1:-} =~ $re ]] || return 1 # dateコマンドで実在する日付か確認(GNU date 必須) date -d "$1" >/dev/null 2>&1 } # 使用例 start_date="2024-02-30" # 2月30日は存在しない if ! is_date_ymd "$start_date"; then echo "ERROR: $start_date は正しい日付(YYYY-MM-DD形式)ではありません" >&2 exit 1 fi

date -d で実在チェックを行うことで、フォーマットは正しくても存在しない日付(2月30日など)を弾ける。date -d は GNU coreutils 提供のオプションのため、macOS では代わりに gdate(Homebrew の gnu-coreutils)を使う。

3. ファイル・ディレクトリチェック

# ディレクトリの存在と読み取り権限チェック check_dir_readable() { local name=$1 path=$2 if [[ ! -d $path ]]; then echo "ERROR: $name ($path) が存在しないかディレクトリではありません" >&2 exit 1 fi if [[ ! -r $path ]]; then echo "ERROR: $name ($path) に読み取り権限がありません" >&2 exit 1 fi } # ディレクトリの書き込み権限チェック check_dir_writable() { local name=$1 path=$2 if [[ ! -d $path ]]; then echo "ERROR: $name ($path) が存在しません" >&2 exit 1 fi if [[ ! -w $path ]]; then echo "ERROR: $name ($path) に書き込み権限がありません" >&2 exit 1 fi } # ファイルの存在と読み取り権限チェック check_file_readable() { local name=$1 path=$2 if [[ ! -f $path ]]; then echo "ERROR: $name ($path) が存在しないかファイルではありません" >&2 exit 1 fi if [[ ! -r $path ]]; then echo "ERROR: $name ($path) に読み取り権限がありません" >&2 exit 1 fi }

4. 選択肢チェック(enumバリデーション)

引数が決まった選択肢のいずれかであることをチェックする。

is_one_of() { local val=$1 shift local choice for choice in "$@"; do [[ $val == "$choice" ]] && return 0 done return 1 } # 使用例:環境はdev/staging/prodのいずれか env_name="$1" if ! is_one_of "$env_name" dev staging prod; then echo "ERROR: 環境は dev / staging / prod のいずれかで指定してください(指定値: $env_name)" >&2 exit 1 fi

バリデーション関数のライブラリ化と再利用設計

バリデーション関数をスクリプトごとにコピーするのは保守性の観点でアンチパターンだ。共通ライブラリファイルを lib/validate.sh にまとめ、複数のスクリプトから source して再利用する設計が現場での鉄則だ。

1. ディレクトリ構成

project/ ├── lib/ │ └── validate.sh # バリデーション関数ライブラリ ├── backup.sh # バックアップスクリプト ├── deploy.sh # デプロイスクリプト └── cleanup.sh # 古いファイル削除スクリプト

2. validate.sh の設計(二重source防止ガード付き)

#!/bin/bash # lib/validate.sh — シェルスクリプト共通バリデーションライブラリ # 二重source防止ガード(同じライブラリを複数回sourceしても安全にする) [[ -n ${_VALIDATE_SH:-} ]] && return 0 _VALIDATE_SH=1 # ----- 文字列系 ----- check_nonempty() { local name=$1 val=${2:-} [[ -n $val ]] && return 0 echo "ERROR: $name が空文字です" >&2; exit 1 } check_str_length() { local name=$1 val=$2 min=$3 max=$4 local len=${#val} (( len >= min && len <= max )) && return 0 echo "ERROR: $name の文字数は ${min}以上${max}以下にしてください(現在: ${len}文字)" >&2; exit 1 } # ----- 数値系 ----- is_integer() { [[ ${1:-} =~ ^-?[0-9]+$ ]]; } is_positive_int() { [[ ${1:-} =~ ^[1-9][0-9]*$ ]]; } is_number() { [[ ${1:-} =~ ^-?[0-9]+(\.[0-9]+)?$ ]]; } is_in_range() { local val=$1 min=$2 max=$3 is_integer "$val" || return 1 (( val >= min && val <= max )) } # ----- 形式系 ----- is_ipv4() { local re='^([0-9]{1,3}\.){3}[0-9]{1,3}$' [[ ${1:-} =~ $re ]] || return 1 local IFS='.' arr read -r -a arr <<< "$1" local o; for o in "${arr[@]}"; do (( o >= 0 && o <= 255 )) || return 1; done } is_date_ymd() { local re='^[0-9]{4}-[0-1][0-9]-[0-3][0-9]$' [[ ${1:-} =~ $re ]] || return 1 date -d "$1" >/dev/null 2>&1 } is_one_of() { local val=$1; shift local c; for c in "$@"; do [[ $val == "$c" ]] && return 0; done; return 1 } # ----- ファイル・ディレクトリ系 ----- check_dir_readable() { local name=$1 path=$2 [[ -d $path ]] || { echo "ERROR: $name ($path) がディレクトリではありません" >&2; exit 1; } [[ -r $path ]] || { echo "ERROR: $name ($path) に読み取り権限がありません" >&2; exit 1; } } check_dir_writable() { local name=$1 path=$2 [[ -d $path ]] || { echo "ERROR: $name ($path) が存在しません" >&2; exit 1; } [[ -w $path ]] || { echo "ERROR: $name ($path) に書き込み権限がありません" >&2; exit 1; } } check_file_readable() { local name=$1 path=$2 [[ -f $path ]] || { echo "ERROR: $name ($path) がファイルではありません" >&2; exit 1; } [[ -r $path ]] || { echo "ERROR: $name ($path) に読み取り権限がありません" >&2; exit 1; } }

3. 各スクリプトからのsource

#!/bin/bash set -euo pipefail # スクリプト自身の場所から相対パスでlib/を解決する SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) # shellcheck source=./lib/validate.sh source "$SCRIPT_DIR/lib/validate.sh" # 以後は validate.sh の関数がそのまま使える is_positive_int "$1" || { echo "ERROR: 第1引数は正の整数で指定してください" >&2; exit 1; }

SCRIPT_DIRcd + pwd で解決することで、どのディレクトリから呼び出されても lib/ を正しく参照できる。シンボリックリンク経由の呼び出しも想定するなら realpath "$(dirname "$0")" を使うとより堅牢だ。

実践例:バックアップスクリプトへのバリデーション組み込み

ここまでの関数を組み合わせた実際のバックアップスクリプト例を示す。実行環境:Rocky Linux 9.4 で動作確認済み。

#!/bin/bash # backup.sh — バリデーション付きrsyncバックアップスクリプト set -euo pipefail SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) source "$SCRIPT_DIR/lib/validate.sh" usage() { cat >&2 <<'EOF' Usage: backup.sh src_dir : バックアップ元ディレクトリ dst_dir : バックアップ先ディレクトリ(書き込み権限が必要) retention_days : 世代保持日数(1以上365以下の整数) Example: backup.sh /var/data /mnt/backup 30 EOF exit 1 } # ---- 引数チェック ---- [ $# -eq 3 ] || usage src_dir=$1 dst_dir=$2 retention_days=$3 # 各引数のバリデーション(本処理より前に全部済ませる) check_nonempty "src_dir" "$src_dir" check_dir_readable "src_dir" "$src_dir" check_nonempty "dst_dir" "$dst_dir" check_dir_writable "dst_dir" "$dst_dir" if ! is_in_range "$retention_days" 1 365; then echo "ERROR: retention_days は 1 以上 365 以下の整数で指定してください(指定値: $retention_days)" >&2 exit 1 fi # ---- バリデーション通過後に本処理を書く ---- timestamp=$(date +%Y%m%d_%H%M%S) backup_dir="$dst_dir/$timestamp" echo "[INFO] バックアップ開始: $src_dir → $backup_dir" rsync -av --link-dest="$dst_dir/latest" "$src_dir/" "$backup_dir/" ln -snf "$backup_dir" "$dst_dir/latest" echo "[INFO] ${retention_days}日超の古いバックアップを削除します" find "$dst_dir" -maxdepth 1 -type d -mtime +"$retention_days" -exec rm -rf {} + echo "[INFO] バックアップ完了"

このスクリプトでは、すべてのバリデーションが validate.sh を source した直後に集中しており、本処理は「バリデーション通過後に書く」という構造になっている。万一 src_dir を誤って空文字で渡しても、rsync の実行前に ERROR を出力して終了する。

実際の実行例(バリデーション失敗時)を確認する。

# 世代保持日数に不正値を渡した場合 $ ./backup.sh /var/data /mnt/backup abc ERROR: retention_days は 1 以上 365 以下の整数で指定してください(指定値: abc) $ echo $? 1 # バックアップ元が存在しない場合 $ ./backup.sh /nonexistent /mnt/backup 30 ERROR: src_dir (/nonexistent) がディレクトリではありません $ echo $? 1 # 引数が不足している場合 $ ./backup.sh /var/data /mnt/backup Usage: backup.sh src_dir : バックアップ元ディレクトリ dst_dir : バックアップ先ディレクトリ(書き込み権限が必要) retention_days : 世代保持日数(1以上365以下の整数) Example: backup.sh /var/data /mnt/backup 30

入力バリデーション設計をさらに深めてシェルスクリプトの設計力を体系的に身につけたい方は、シェルスクリプト実践講座(Linux Master Pro)もご覧ください。現場で使えるシェルスクリプト設計パターンを体系的に学べます。

トラブルシュート:バリデーション実装でよく詰まるポイントと対処

1. sh で実行すると [[ ]] が使えない

[[ =~ ]] はbashの組み込み構文であり、POSIXシェルの sh では動作しない。スクリプトの冒頭に必ず #!/bin/bash(または #!/usr/bin/env bash)を書き、かつ bash script.sh で実行すること。sh script.sh で実行すると [[: command not found エラーが出る。

POSIX互換の sh で動かす必要がある場合は case 文と expr でバリデーションを実装する必要がある。

2. is_ipv4 で IFS がスクリプト全体に影響する問題

is_ipv4 関数内で local IFS='.' としているが、local 宣言を忘れると IFS がグローバルに変更されてしまう。その後の readfor ループで想定外の分割が起きる可能性がある。local IFS='.' と宣言されていることを確認し、関数内で IFS を変更する場合は必ず local をつけること。

3. 整数チェックで数値が大きすぎてオーバーフローする

(( val >= min && val <= max )) のような算術展開は、bashが扱える整数範囲(64bit符号付き整数:-9223372036854775808~9223372036854775807)を超えるとオーバーフローする。ポート番号(0~65535)や年(1900~2100)など、現実的な範囲に収まる値には問題ないが、ファイルサイズなど非常に大きな数値を扱う場合は bc コマンドで比較するか、正規表現で桁数を制限するアプローチを取ること。

4. validate.sh が見つからない(source 失敗)

# エラー例 /path/to/backup.sh: line 7: /path/to/backup.sh/../lib/validate.sh: No such file or directory

SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) が正しく動いていない場合に起きる。$0 がシンボリックリンクの場合、リンク自身のパスを返すため、リンク先のディレクトリとずれる。その場合は SCRIPT_DIR=$(cd "$(dirname "$(realpath "$0")")" && pwd) に変更する。

本記事のまとめ

チェック対象 実装パターン
引数の個数 [ $# -eq N ] || usage
空文字チェック [[ -z ${var:-} ]] で判定 → check_nonempty 関数化
整数チェック [[ $val =~ ^[0-9]+$ ]] → is_integer 関数
範囲チェック (( val >= min && val <= max )) → is_in_range 関数
IPv4アドレス 正規表現 + オクテット数値チェック → is_ipv4 関数
日付形式(YYYY-MM-DD) 正規表現 + date -d で実在確認 → is_date_ymd 関数
選択肢チェック ループで一致確認 → is_one_of 関数
ディレクトリ読み取り [[ -d ]] && [[ -r ]] → check_dir_readable 関数
ディレクトリ書き込み [[ -d ]] && [[ -w ]] → check_dir_writable 関数
ライブラリ化 lib/validate.sh に集約 → 各スクリプトから source で再利用
バリデーションは「念のため」のチェックではなく、スクリプトの安全な実行を保証する設計上の骨格だ。チェック関数を lib/validate.sh にまとめておけば、新しいスクリプトを書くたびに source 1行で使えるようになり、チームや後任者が引き継いだときも同じ品質基準を維持できる。

「バリデーションを済ませてから本処理を書く」という順序を習慣にすれば、引数ミスによる本番データの破壊は二度と起きなくなる。
現場で通用する安全なLinuxサーバー構築の「型」を体系的に身につけたい方へ、20年以上の運用経験を持つ現役エンジニアが基礎から教えます。
シェルスクリプト講座を見る >>

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

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

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

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

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

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

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

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

この記事を書いた人

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

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

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