シェルスクリプトのプロジェクトが育ってくると、こうした奇妙なバグに悩まされることがある。調べてみると、自分が付けた関数名が既存の外部コマンドや bash 組み込みと衝突していた、あるいは複数のライブラリ間で同名の関数が互いに上書きし合っていた、という原因が多い。
この記事では、bash 関数名の衝突がなぜ起きるのかという仕組みの解説から、declare -F による棚卸し・type コマンドによる事前確認・プレフィックス命名規約・readonly -f による保護まで、衝突を根本から防ぐ実践的な設計パターンを順番に解説する。
この記事のポイント
・bash は「関数 → 組み込み → 外部コマンド」の順で名前を解決するため、関数名が既存名と同じだと外部コマンドが呼ばれなくなる
・declare -F で定義済みの全関数を一覧し、type -t で衝突リスクを事前に確認できる
・プロジェクト識別子をプレフィックスに使う命名規約(例: bk_log)が衝突を根本から防ぐ
・readonly -f で公開済みの関数をロックし、source 読み込み時の意図しない上書きをブロックできる
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
なぜ bash 関数名が既存コマンドと衝突するのか
bash が名前を解決するとき、以下の優先順位で探索する。# bash の名前解決優先順位(上が高い) # 1. エイリアス (alias) # 2. シェル関数 (function) ← 自分で定義した関数 # 3. シェル組み込み (builtin) ← cd, echo, printf, read など # 4. 外部コマンド (PATH 上のファイル) ← /bin/ls, /usr/bin/date など
ls や date という名前の関数を定義すると、それ以降そのシェルセッション(および source で読み込んだすべての子スクリプト)で外部コマンドの ls や date が呼ばれなくなる。1. 衝突が引き起こす問題(実例)
次のコードは現場でよく見かける事故パターンだ。# 【事故例1】ls という名前で「常に -la を付けたい」と思って関数を定義 ls() { ls -la --color=auto "$@" # ← ls が ls を呼ぶ無限再帰になる } # 実行すると bash がスタックオーバーフローで強制終了する
# 【事故例2】ライブラリスクリプト lib_a.sh に date という名前の関数を定義 date() { echo "$(command date '+%Y%m%d')" # 引数なしで日付文字列だけ返す } # main.sh で source して呼び出す source ./lib_a.sh BACKUP_FILE="/backup/data_$(date '+%Y-%m-%d').tar.gz" # ↑ date 関数が呼ばれるため、'+%Y-%m-%d' は単なる引数として無視され # "data_20260908.tar.gz" ではなく "data_20260908.tar.gz" という # フォーマット崩れが起きる(または意図しない挙動になる)
2. 複数ライブラリ間での上書き衝突
外部コマンドだけでなく、ライブラリ同士の命名衝突も同様の問題を起こす。# lib_a.sh: 汎用名を使ったライブラリ log() { echo "[INFO] $*"; } # lib_b.sh: 別の開発者が書いた別ライブラリ(同名で定義) log() { echo "[DEBUG] $*"; } # main.sh source ./lib_a.sh source ./lib_b.sh log "バックアップ完了" # → "[DEBUG] バックアップ完了" と出力される # lib_b.sh の log が lib_a.sh の log を上書きした
declare -F で定義済み関数を棚卸しする
関数名の衝突を防ぐ第一歩は、「今シェルに何という名前の関数が定義されているか」を把握することだ。そのためのコマンドがdeclare -F だ。1. declare -F の基本使い方
# 定義済みのシェル関数名を一覧する declare -F # 関数名だけを抽出する場合 declare -F | awk '{print $3}'
$ source ./lib/bk_core.sh $ declare -F declare -f _bk_build_tar_cmd declare -f _bk_rotate_old declare -f _bk_validate_args declare -f bk_cleanup declare -f bk_full declare -f bk_incremental declare -f bk_log declare -f bk_status
declare -F の出力は「declare -f 関数名」の形式で並ぶ。関数名だけ取り出したい場合は awk '{print $3}' で3列目を抽出すればよい。2. declare -f 関数名 で定義内容を確認する
declare -f(小文字 f)に関数名を指定すると、その関数の定義内容がそのまま出力される。未知のライブラリを読み込んだ後に「この関数は何をしているか」を確認するのに使える。$ declare -f bk_log bk_log () { local level="$1" local msg="$2" echo "[$level] $(date '+%Y-%m-%d %T') $msg" }
declare -F 関数名(大文字 F)の戻り値で判定できる。# 関数が定義されているか確認する(0=定義済み、1=未定義) declare -F bk_log >&/dev/null && echo "定義済み" || echo "未定義"
type コマンドで衝突リスクを事前に確認する
declare -F はシェル関数の棚卸しツールだが、「これから定義しようとする名前が既存の何かと衝突しないか」を確認するには type コマンドを使う。1. type -t で名前の種別を調べる
# type -t は名前の種別を alias/builtin/function/file のいずれかで返す # 何も見つからなければ何も出力せず終了コード 1 を返す $ type -t ls alias $ type -t cd builtin $ type -t date file $ type date date is /usr/bin/date $ type -t bk_log function $ type -t nonexistent_name (何も出力されない・exit 1)
種別ごとの意味をまとめると次のようになる。
| type -t の出力 | 意味 | 定義すると |
|---|---|---|
| alias | エイリアスとして定義済み | alias より関数が優先される |
| builtin | bash 組み込みコマンド | 組み込みが関数に隠される |
| file | PATH 上の外部コマンド | 外部コマンドが関数に隠される |
| function | 既にシェル関数として定義済み | 既存の関数が上書きされる |
| (空) | 未定義・衝突なし | 安全に定義できる |
2. 衝突チェックを関数化して再利用する
ライブラリスクリプトの冒頭でこのチェックを自動化しておくと、source した時点でミスを検出できる。# _lib_assert_safe_name: 指定した名前が既存の builtin/file と衝突しないか確認する # 引数: 関数名 # 戻り値: 0=安全、1=衝突あり(エラーを stderr に出力) _lib_assert_safe_name() { local name="$1" local kind kind=$(type -t "$name" 2>/dev/null) case "$kind" in builtin|file|alias) printf "ERROR: '%s' は既に '%s' として存在します。関数名を変更してください。\n" \ "$name" "$kind" >&2 return 1 ;; *) return 0 ;; # 未定義または既存の function: 呼び出し元で判断 esac } # ライブラリの先頭でチェックしてから定義する _lib_assert_safe_name "bk_log" || exit 1 bk_log() { local level="$1" local msg="$2" echo "[$level] $(date '+%Y-%m-%d %T') $msg" }
シェルスクリプト講座を見る >>
プレフィックス命名規約で衝突を根本から防ぐ
衝突チェックは「後からミスを検出する」仕組みだ。しかし「そもそも衝突しない名前を付ける」設計の方が根本的な解決になる。それがプレフィックス命名規約だ。1. プロジェクト識別子をプレフィックスに使う
関数名の先頭にプロジェクト名や機能ブロックの略称を付けることで、他のコマンドや別ライブラリとの衝突をほぼゼロにできる。# 【悪い例】汎用的な名前は衝突しやすい log() { echo "[INFO] $*"; } backup() { tar czf "/backup/$(date +%Y%m%d).tar.gz" "$@"; } check() { df -h; } cleanup() { rm -f /tmp/work.*; } # 【良い例】プロジェクト識別子(2~4文字)をプレフィックスに使う # bk = backup project の略称 bk_log() { echo "[INFO] $*"; } bk_backup() { tar czf "/backup/$(date +%Y%m%d).tar.gz" "$@"; } bk_check() { df -h; } bk_cleanup() { rm -f /tmp/work.*; }
・プロジェクト名の略称を使う(backup → bk、deploy → dp、monitor → mn など)
・2~4文字程度にしてタイプコストを最小化する
・既存の外部コマンドや bash 組み込みと同じにならない文字列を選ぶ
・チーム内でプレフィックス一覧を管理し、新規ライブラリ追加時に割り当てる
2. アンダースコア始まりで内部専用関数を区別する
ライブラリの外から呼ばれる「公開関数」と、ライブラリ内部だけで使う「内部ヘルパー関数」を命名で区別すると、可読性と安全性が上がる。# 公開関数: プレフィックス付き(外部スクリプトから呼び出してよい) bk_run_full() { _bk_validate_dir "$1" && _bk_do_tar "$1"; } bk_run_incremental() { _bk_validate_dir "$1" && _bk_do_rsync "$1"; } bk_get_status() { _bk_read_last_log; } # 内部ヘルパー: _ 始まり(同一ライブラリファイル内だけで使う) _bk_validate_dir() { [[ -d "$1" ]] || { echo "ERROR: $1 が存在しません" >&2; return 1; }; } _bk_do_tar() { tar czf "/backup/$(date +%Y%m%d_%H%M%S).tar.gz" "$1"; } _bk_do_rsync() { rsync -a --link-dest=/backup/latest "$1" "/backup/$(date +%Y%m%d)/"; } _bk_read_last_log() { tail -1 /var/log/bk_backup.log 2>/dev/null; }
3. 特に避けるべき関数名の例
bash の組み込みコマンドと外部コマンドの両方に存在するため、特に衝突リスクが高い名前を挙げる。| 避けるべき関数名 | 理由 |
|---|---|
log |
汎用名・複数ライブラリで重複しやすい |
date |
/usr/bin/date(外部コマンド)を隠す |
ls |
/bin/ls を隠す・エイリアスと衝突する |
echo |
bash 組み込みを隠す(printf に影響が出ることも) |
test |
bash 組み込みの test コマンドを隠す |
run / exec / init |
汎用的すぎて複数ライブラリで重複しやすい |
cleanup / check / backup |
同上 |
readonly -f で意図しない上書きをブロックする
プレフィックス命名規約だけでは防げない問題がある。「同じプレフィックスを使った別のライブラリが source され、後から定義を上書きしてしまう」ケースだ。これを防ぐのがreadonly -f だ。1. readonly -f の基本的な使い方
bk_log() { local level="$1" local msg="$2" echo "[$level] $(date '+%Y-%m-%d %T') $msg" } # 関数を定義した直後に readonly -f でロックする readonly -f bk_log # これ以降、bk_log を再定義しようとするとエラーになる bk_log() { echo "上書き"; }
$ source ./lib/bk_core.sh $ bk_log() { echo "上書き"; } bash: bk_log: readonly function
readonly -f は「その関数名への再代入を bash が拒否する」仕組みだ。source で新しいライブラリを読み込んでも、ロック済みの関数は書き換えられない。2. ライブラリ末尾でまとめてロックするパターン
関数を1つずつreadonly -f するのは記述が冗長になる。ライブラリ末尾でプレフィックスによる絞り込みをしてまとめてロックする書き方が実用的だ。# bk_core.sh の末尾に追加する # bk_ で始まる全公開関数を readonly -f でロック while IFS= read -r fn_name; do readonly -f "$fn_name" done < <(declare -F | awk '$3 ~ /^bk_/ {print $3}')
3. すでに readonly な関数を確認する
readonly -f が適用済みかどうかは declare -f -p 関数名 で確認できる。# readonly -f が適用されているかを確認する # readonly な関数には "-fr" フラグが付く(f=function, r=readonly) $ declare -p -f bk_log declare -fr bk_log
declare -fr と表示されれば readonly が有効だ。-f だけなら readonly は未設定の状態になる。実務パターン:複数ライブラリを安全に読み込む設計
バックアップスクリプトを例に、プレフィックス命名・declare -F・readonly -f を組み合わせた実務パターンを示す。1. ライブラリファイルの構成
lib/ ├── bk_log.sh # ログ関数(bk_log, bk_log_error) ├── bk_backup.sh # バックアップ本体(bk_full, bk_incremental) └── bk_notify.sh # 通知関数(bk_notify_slack, bk_notify_mail)
readonly -f によるロックを実施する(前述のパターンを使う)。2. エントリポイントスクリプトでの安全な読み込み
#!/bin/bash set -euo pipefail LIB_DIR="$(cd "$(dirname "$0")/lib" && pwd)" # ライブラリをまとめて読み込む for lib_file in "${LIB_DIR}"/bk_*.sh; do if [[ ! -f "$lib_file" ]]; then echo "ERROR: ライブラリが見つかりません: $lib_file" >&2 exit 1 fi # shellcheck source=/dev/null source "$lib_file" done # 読み込み後に定義済み関数を確認(デバッグ時に有用) # declare -F | awk '$3 ~ /^bk_/ {print $3}' # エントリポイント bk_log "INFO" "バックアップ開始" bk_full "/var/data" || { bk_log "ERROR" "バックアップ失敗"; exit 1; } bk_notify_slack "バックアップ完了: $(date '+%Y-%m-%d %T')" bk_log "INFO" "バックアップ完了"
3. 読み込み後に衝突の有無を機械的に確認する
CI/CD に組み込む場合は、ライブラリ読み込み後に既知の外部コマンド名と衝突していないかを自動チェックするスクリプトを加えると安全だ。# 衝突チェックスクリプト(CI で実行する) DANGER_NAMES=( ls cp mv rm mkdir find grep sed awk date printf echo read test true false cat head tail sort uniq wc cut log backup check cleanup run exec init ) for name in "${DANGER_NAMES[@]}"; do if declare -F "$name" >&/dev/null; then echo "WARN: 危険な関数名が定義されています: $name" >&2 fi done
make lint や GitHub Actions の linting ステップに追加しておけば、事故の多くを自動検知できる。よくあるエラーと対処法
1. 「bash: 関数名: readonly function」が出る
readonly -f でロックした関数を再定義しようとすると発生する。意図せず同名の関数を定義しているスクリプトや読み込んでいるライブラリを特定する。# エラー例 $ source ./lib_b.sh bash: bk_log: readonly function # 対処: どこで bk_log が再定義されているかを確認する grep -rn "bk_log\s*()" ./lib_b.sh # または、lib_b.sh を source する前に readonly フラグを確認する declare -p -f bk_log 2>/dev/null # "declare -fr bk_log" と表示されれば readonly が有効
readonly -f が適用されている関数を上書きする正当な理由がある場合(テスト時のモック差し替えなど)は、新しいシェルプロセスを起動して readonly の影響を受けない環境で実行するのが正解だ。readonly は子プロセスには引き継がれない。2. 意図せず定義された衝突関数を削除したい
unset -f で関数の定義を削除できる。ただし readonly -f が適用されていると unset -f も失敗する。# 関数の定義を削除する unset -f ls # 関数として定義された ls を削除 unset -f date # 関数として定義された date を削除 # 削除後に確認する type -t ls # alias (元のエイリアスに戻る) type -t date # file (外部コマンドが使えるようになる) # readonly が適用された関数は unset -f できない $ unset -f bk_log bash: unset: bk_log: cannot unset: readonly function # → サブシェル(bash -c や スクリプト実行)で処理するしかない
3. declare -F で大量の関数が表示されて把握しきれない
インタラクティブシェルでは~/.bashrc や各種ツール(git, nvm, rbenv など)が大量の関数を定義していることがある。自分のプロジェクトの関数だけを絞り込むにはプレフィックスでフィルタリングする。# bk_ プレフィックスの関数だけを一覧する declare -F | awk '$3 ~ /^bk_/ {print $3}' # _ 始まりの内部関数を除いた公開関数だけを一覧する declare -F | awk '$3 ~ /^bk_[^_]/ {print $3}'
まとめ
bash 関数名の衝突防止は「名前解決の優先順位を知り、宣言前に確認し、命名規約でリスクを根絶し、readonly で保護する」という4層の設計で実現できる。| やりたいこと | コマンド/方法 |
|---|---|
| 定義済み関数を一覧する | declare -F |
| 関数名だけ抽出する | declare -F | awk '{print $3}' |
| 関数の定義内容を確認する | declare -f 関数名 |
| 名前の種別を確認する | type -t 名前 |
| 関数が定義済みか確認する | declare -F 関数名 >&/dev/null |
| 関数を再定義不可にする | readonly -f 関数名 |
| readonly 適用済みか確認する | declare -p -f 関数名(-fr なら適用済み) |
| 衝突を根本から防ぐ命名 | プレフィックス_関数名(例: bk_log) |
| 内部専用関数を区別する | _プレフィックス_関数名(例: _bk_validate) |
シェルスクリプト講座を見る >>
3,100名以上が実践した「型」を無料で公開中
プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。
姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら
- 前のページへ:シェルスクリプトのリファクタリング設計|if地獄・グローバル変数汚染・コピペ地獄を関数とローカル変数で直す実践パターン
- この記事の属するカテゴリ:シェルスクリプトへ戻る

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