シェルスクリプトのリファクタリング設計|if地獄・グローバル変数汚染・コピペ地獄を関数とローカル変数で直す実践パターン

宮崎智広 この記事の監修:宮崎智広(Linux実務・教育歴20年以上・受講者3,100名超)
HOMELinux技術 リナックスマスター.JP(Linuxマスター.JP)シェルスクリプト > シェルスクリプトのリファクタリング設計|if地獄・グローバル変数汚染・コピペ地獄を関数とローカル変数で直す実践パターン
「半年前に自分が書いたシェルスクリプトが読めない」
「ちょっとした修正を加えるたびに別の箇所が壊れる」
「コードをコピペしまくって、同じバグを10箇所直さないといけない」

こういう経験は、Linuxサーバーをある程度触ったことのあるエンジニアなら一度は必ず通る道です。シェルスクリプトは手軽に始められる分、設計を意識しないとあっという間に「読めないコード」になります。

この記事では、現場で実際によく見かける「if地獄」「グローバル変数汚染」「コピペ地獄」の3つのアンチパターンを取り上げ、それぞれを関数・ローカル変数・共通ライブラリで改善するリファクタリング設計を解説します。RHEL 9.4 / Ubuntu 24.04 LTSで動作確認済みです。

この記事のポイント

・if地獄はearly return(早期終了)パターンとガード節で解消できる
・local宣言で変数スコープを関数内に閉じてグローバル汚染を防ぐ
・共通処理はsourceで外部ライブラリ化して複数スクリプトで再利用する
・リファクタリングはshellcheckで問題を可視化してから段階的に進める


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

なぜシェルスクリプトはすぐに「負債」になるのか

シェルスクリプトには「とりあえず動けばOK」の文化が根強くあります。コマンドを順番に並べれば動くため、設計を後回しにしやすいのが構造的な問題です。

現場でよく見かける「負債スクリプト」には、共通した特徴があります。

ifのネストが5段以上ある(読むだけで脳が疲れる)
変数が全てグローバル(関数内の変更が思わぬ箇所に波及する)
同じ処理が3箇所以上コピペされている(バグ修正を全箇所に反映し忘れる)
コメントがない・変数名がa、b、tmp(書いた本人でも読めなくなる)

これらは技術力の問題ではなく、「設計の型」を知らないことが原因です。型を知れば、同じ機能でも「読める・直せる・テストできる」スクリプトに生まれ変わります。

if地獄を関数とearly returnで解消する

1. ネストが深くなる原因

典型的な「if地獄」の例を見てみましょう。ファイルのバックアップ処理で、よくある書き方です。

# NG: ネストが深くて読みにくい backup() { if [ -f "$1" ]; then if [ -d "$BACKUP_DIR" ]; then if cp "$1" "$BACKUP_DIR/"; then if [ -f "$BACKUP_DIR/$(basename "$1")" ]; then echo "バックアップ成功: $1" else echo "コピー後のファイルが見つかりません" >&2 fi else echo "コピーに失敗しました" >&2 fi else echo "バックアップディレクトリが存在しません: $BACKUP_DIR" >&2 fi else echo "ソースファイルが存在しません: $1" >&2 fi }

5段のネストです。どの fi がどの if に対応するのか、追いかけるだけで疲れます。

2. 早期リターン(early return)パターン

早期リターンパターンでは、「NG条件が成立したら即座にreturnして終了する」ことで、ネストを平坦に保ちます。

# OK: early returnで平坦に書く backup() { local src="$1" if [ ! -f "$src" ]; then echo "ソースファイルが存在しません: $src" >&2 return 1 fi if [ ! -d "$BACKUP_DIR" ]; then echo "バックアップディレクトリが存在しません: $BACKUP_DIR" >&2 return 1 fi if ! cp "$src" "$BACKUP_DIR/"; then echo "コピーに失敗しました" >&2 return 1 fi echo "バックアップ成功: $src" return 0 }

ネストが1段に収まりました。「NG条件でreturnする → 末尾で成功処理を書く」という流れが一目で分かります。

実際に実行した例です(RHEL 9.4 実機)。

$ BACKUP_DIR=/tmp/backup $ mkdir -p /tmp/backup $ backup /etc/hostname バックアップ成功: /etc/hostname $ backup /etc/nonexistent ソースファイルが存在しません: /etc/nonexistent $ echo $? 1

3. ガード節で前提条件を宣言的に並べる

前提条件チェックをまとめて「ガード節」として関数の先頭に置くと、「この関数が動作するための条件」が一目で分かります。

# ガード節パターン: 前提チェックを冒頭に集める process_log() { local log_file="$1" local output_dir="$2" # ガード節(前提条件が満たされていなければ即座に終了) [ -z "$log_file" ] && { echo "ログファイルを指定してください" >&2; return 1; } [ ! -f "$log_file" ] && { echo "ファイルが見つかりません: $log_file" >&2; return 1; } [ ! -d "$output_dir" ] && { echo "出力先ディレクトリが存在しません" >&2; return 1; } # ここまで来れば前提条件はすべて満たされている local out_file="$output_dir/errors_$(date +%Y%m%d).log" grep "ERROR" "$log_file" > "$out_file" echo "エラーログを抽出しました: $(wc -l < "$out_file") 件" }

ガード節は「防御的プログラミング」の一形態で、関数が期待する入力・状態を明示するドキュメントとしても機能します。読んだ瞬間に「この関数はこれを前提にしている」と分かるのが大きな利点です。

グローバル変数汚染をlocal・readonlyで防ぐ

1. グローバル変数がもたらす問題

bashでは、関数内で宣言した変数もデフォルトでグローバルスコープです。これが予期しないバグの温床になります。

# NG: 関数内の変数がグローバルに漏れる count=0 increment() { i=1 # local 宣言なし → グローバル変数として i が生成される count=$((count + i)) } i=100 # 別の場所で i を使っている increment echo "i = $i" # 期待: 100 / 実際: 1 (increment 内で上書きされた)

実行すると:

$ i=100 $ increment $ echo "i = $i" i = 1

関数内の i=1 が外側の i=100 を上書きしてしまいます。この手のバグは関数が増えるほど見つけにくくなります。

2. local宣言で変数スコープを関数内に閉じる

# OK: local で変数スコープを閉じる count=0 increment() { local i=1 # 関数スコープ内だけで有効 count=$((count + i)) } i=100 increment echo "i = $i" # 100 のまま(関数内の local i に影響されない) echo "count = $count" # 1

実行結果:

$ i=100 $ increment $ echo "i = $i" i = 100 $ echo "count = $count" count = 1

関数内で使う変数には必ず local を付ける、と習慣化すれば、変数汚染のバグは大幅に減ります。「変数名が衝突しないか不安で関数を書けない」という状況も解消されます。

3. readonlyとdeclare -rで定数を保護する

スクリプト全体で使う設定値(バックアップ先パス・リトライ回数など)は、定数として保護しましょう。誤って上書きしようとするとエラーを出して検知できます。

#!/bin/bash # 設定値を定数として宣言 readonly BACKUP_DIR="/var/backup" readonly MAX_RETRY=3 declare -r LOG_FILE="/var/log/batch/$(date +%Y%m%d).log" # 誤って上書きしようとするとエラーになる BACKUP_DIR="/tmp/test" # ← これはエラーになる

実行結果(RHEL 9.4 実機):

$ ./script.sh ./script.sh: 行 8: BACKUP_DIR: 読み取り専用の変数です

readonlydeclare -r はどちらも同じ効果ですが、declare は型オプションを組み合わせやすいため(例: declare -ri MAX_RETRY=3 で整数型の定数)、設定値の宣言には declare を使う流派もあります。どちらかに統一することが重要で、混在は避けましょう。

コピペ地獄を共通ライブラリ化で解消する

1. 同じ処理が複数スクリプトに散らばる問題

ログ出力・エラーハンドリング・設定読み込みは、プロジェクト内の全スクリプトで必要になります。コピペで対応するとバグ修正を全件探して直す羽目になります。

# NG: 同じログ関数が backup.sh / deploy.sh / cleanup.sh それぞれに存在する log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" }

このログ関数が3ファイルに存在する場合、タイムスタンプのフォーマットを変えたいだけで3ファイルを編集する必要があります。フォーマット変更の漏れが、ログ集計ツールの解析失敗につながることもあります。

2. sourceコマンドでライブラリを読み込む設計

共通処理を lib/common.sh として切り出し、source コマンドで読み込みます。

# lib/common.sh(共通ライブラリ) # 二重読み込み防止 [ -n "${_COMMON_SH_LOADED:-}" ] && return 0 readonly _COMMON_SH_LOADED=1 readonly LOG_DIR="/var/log/batch" log_info() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] INFO $*" | tee -a "$LOG_DIR/batch.log"; } log_error() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR $*" | tee -a "$LOG_DIR/batch.log" >&2; } die() { log_error "$1" exit "${2:-1}" }

# backup.sh(利用側) #!/bin/bash set -euo pipefail # スクリプトのディレクトリ基準でライブラリを読み込む SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" source "${SCRIPT_DIR}/lib/common.sh" log_info "バックアップ開始" cp /etc/hostname /var/backup/ || die "バックアップに失敗しました" 2 log_info "バックアップ完了"

実行後の /var/log/batch/batch.log:

[2026-09-07 14:23:01] INFO バックアップ開始 [2026-09-07 14:23:01] INFO バックアップ完了

3. 共通ライブラリ設計の指針

ライブラリを作るときに守るべき設計上の指針です。

関数名にプレフィックスを付けて衝突を防ぐ(例: log_info log_error lib_retry
二重読み込み防止のガードを必ず入れる(複数スクリプトがsourceし合う環境で必須)
パスはSCRIPT_DIRからの相対で指定するcd後に実行されても壊れない)
グローバル変数を定義するならreadonly にする(上書き禁止にして他スクリプトへの影響を明示)
ライブラリ自体はshebang行を書かない(source専用ファイルとして扱い、誤って直接実行させない)
現場で通用する安全なLinuxサーバー構築の「型」を体系的に身につけたい方へ、20年以上の現場経験を持つ現役エンジニアが基礎から教えます。
シェルスクリプト講座を見る >>

既存スクリプトを安全にリファクタリングする手順

1. shellcheckで問題を可視化する

リファクタリングを始める前に、まず shellcheck で静的解析を走らせます。設計上の問題を客観的に可視化してからリファクタリングに着手するほうが、作業漏れが減ります。

$ shellcheck backup.sh In backup.sh line 4: DIR=$1 ^--^ SC2034: DIR appears unused. Verify use (or export if used externally). In backup.sh line 7: if [ -f $FILE ]; then ^---^ SC2086: Double quote to prevent globbing and word splitting. In backup.sh line 12: cp $FILE $BACKUP_DIR ^---^ SC2086: Double quote to prevent globbing and word splitting.

これはRHEL 9.4の実機で動作確認した出力例です(shellcheck 0.9.0)。SC2086(引用符なし変数展開)はワードスプリッティングによるバグの典型で、ファイル名にスペースが含まれると動作が壊れます。

shellcheckの警告コードは重大度が高い順に直すのが基本です。SC2086(引用符なし)とSC2048(配列展開)は最優先で直しましょう。

2. ベースラインテストで安全網を張る

リファクタリング前に「現状の動作」を記録しておくことで、リファクタリング後に挙動が変わっていないかを確認できます。

# リファクタリング前: 正常系の期待出力を記録する $ ./backup.sh /etc/hostname 2>&1 バックアップ成功: /etc/hostname $ echo "exit=$?" exit=0 # 異常系も記録する $ ./backup.sh /etc/nonexistent 2>&1 ソースファイルが存在しません: /etc/nonexistent $ echo "exit=$?" exit=1

正常系・異常系それぞれの期待出力と終了コードを手元にメモしておき、リファクタリング後も同じ結果になることを確認します。

3. 小さなコミットで段階的に改善する

「一度に全部直す」は最も失敗しやすいリファクタリング手法です。現場で推奨する順序は以下のとおりです。

Step 1:変数名のリネーム(asrc_file 等)。挙動を変えない安全な変更
Step 2:引用符の追加。$FILE"$FILE"。shellcheckの指摘を1件ずつ潰す
Step 3:関数の抽出。コピペ箇所を特定して共通ライブラリに移す
Step 4:local宣言の追加。関数内のグローバル変数を全てlocal化する
Step 5:early returnの適用。深いネストを平坦化する

各ステップの後にベースラインテストを実行し、挙動が変わっていないことを確認してからGitにコミットします。「全部一気に直す → どこかで壊れる → 何が原因か分からない」という状況を防ぐための分割です。

まとめ:「読める・直せる・テストできる」スクリプトに

この記事で解説した3つのリファクタリングパターンをまとめます。
アンチパターン 解決策 キーワード
if地獄(深いネスト) NG条件を先にチェックしてreturnする early return / ガード節
グローバル変数汚染 関数内変数にlocal・定数にreadonly local / readonly / declare -r
コピペ地獄 共通処理を外部ファイル化してsource source / 共通ライブラリ
リファクタリングは一度にやり切ろうとせず、shellcheckで問題を可視化してから「変数名リネーム → 引用符追加 → 関数抽出 → local化 → early return」の順で段階的に進めることを強くおすすめします。各ステップでベースラインテストを通せば、安全に既存の動作を保ちながら改善できます。

設計の型を身につけたシェルスクリプトは、半年後の自分でも読めます。それが、運用を長く続けられるスクリプトの条件です。
シェルスクリプトを「書ける」から「設計できる」レベルに引き上げたい方へ。20年以上の現場経験をもとに体系的に解説します。
シェルスクリプト講座を見る >>

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

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

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

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

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

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

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

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

この記事を書いた人

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

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

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