Linux でパスの種類を分岐するなら、Bash の条件式を引用して使います。-f は通常ファイル、-d はディレクトリ、-L はシンボリックリンク、-e は参照先をたどった存在確認です。読み書き可能性は -r/-w/-x でその時点の実効アクセスを調べます。ただし検査直後に別プロセスが対象を差し替えられるため、テスト結果は将来の操作を保証しません。最後は open、mkdir、rename など実際の操作の成否を処理します。 確認ポイント:broken symlinkは-Lが真でも-eが偽になり得るため、判定順序を固定します。
symlinkを追跡する前後でpath種別を分類する
[[ -e $path ]]: パスが存在する。symlink は参照先をたどるため、broken symlink では偽。[[ -f $path ]]: 参照先が通常ファイル。通常ファイルを指す symlink でも真になり得る。[[ -d $path ]]: 参照先がディレクトリ。ディレクトリを指す symlink でも真になり得る。[[ -L $path ]]: パス自体が symbolic link。参照先がなくても真。-hも Bash では同じ判定。[[ -r/-w/-x $path ]]: 現在のプロセスから読取・書込・実行可能かを判定。ディレクトリの-xは中を検索・通過する権限。[[ -s $path ]]: 存在してサイズが0より大きい。型判定ではなく内容サイズの補助。
Bash の [[ ... ]] 内では単語分割と pathname expansion は行われませんが、移植性と可読性のため [[ -f "$path" ]] と引用して構いません。POSIX test または [ ... ] では引用が必須で、[ -f $path ] は空白や空値で引数個数が変わります。閉じ角括弧もコマンド引数なので [ -f "$path" ] の空白を省けません。
broken symlink を区別する exact script
symlink では -L を先に判定し、その後 -e で参照先の有無を分けます。-e を最初にして偽なら「missing」と決めるコードは broken link を見落とします。次の使い捨て環境は通常ファイル、ディレクトリ、正常 link、broken link、存在しないパスを作ります。
work=$(mktemp -d) || exit 1
trap 'rm -r -- "$work"' EXIT
printf 'data\n' >"$work/regular file"
mkdir "$work/a directory"
ln -s 'regular file' "$work/good link"
ln -s 'missing target' "$work/broken link"
classify() {
local path=$1
if [[ -L "$path" ]]; then
if [[ -e "$path" ]]; then printf 'symlink-ok\n'; else printf 'symlink-broken\n'; fi
elif [[ -f "$path" ]]; then
printf 'regular-file\n'
elif [[ -d "$path" ]]; then
printf 'directory\n'
elif [[ -e "$path" ]]; then
printf 'other-existing-type\n'
else
printf 'missing\n' >&2
return 3
fi
}
for item in 'regular file' 'a directory' 'good link' 'broken link' 'absent'; do
output=$(classify "$work/$item" 2>&1); rc=$?
printf '%s => %s rc=%d\n' "$item" "$output" "$rc"
done
正確な出力は regular file => regular-file rc=0、a directory => directory rc=0、good link => symlink-ok rc=0、broken link => symlink-broken rc=0、absent => missing rc=3 です。broken link 自体は「存在する directory entry」なのでこの関数では正常に分類し、見つからない入力だけを3にしています。運用要件で broken link も異常にするなら、その分岐を標準エラーへ出して別の非0を return します。
一つのパスが複数テストで真になる
for item in 'regular file' 'good link' 'broken link'; do
p=$work/$item
printf '%s e=%d f=%d d=%d L=%d\n' "$item" \
"$([[ -e "$p" ]]; echo $?)" \
"$([[ -f "$p" ]]; echo $?)" \
"$([[ -d "$p" ]]; echo $?)" \
"$([[ -L "$p" ]]; echo $?)"
done
Bash の終了値は真が0、偽が1なので、結果は regular file が e=0 f=0 d=1 L=1、good link が e=0 f=0 d=1 L=0、broken link が e=1 f=1 d=1 L=0 です。正常 link は -f と -L が両方真です。「link をたどらず通常ファイルだけ」を求めるなら [[ -f $p && ! -L $p ]] と両条件を明示します。
-r、-w、-x は mode bit の表示ではない
アクセス条件式は現在の実効ユーザーが操作できそうかを確認する補助です。所有者・group・other の mode だけでなく ACL、root 権限、capability、読み取り専用 mount などで結果が変わります。特に root でのテスト結果を一般利用者へ外挿しません。通常ファイルの -x は実行許可、ディレクトリの -x は名前検索権限で意味が異なります。
chmod 600 "$work/regular file"
chmod 700 "$work/a directory"
for p in "$work/regular file" "$work/a directory"; do
printf '%s mode=%s r=%s w=%s x=%s\n' "$(basename "$p")" \
"$(stat -c %a -- "$p")" \
"$([[ -r "$p" ]] && echo yes || echo no)" \
"$([[ -w "$p" ]] && echo yes || echo no)" \
"$([[ -x "$p" ]] && echo yes || echo no)"
done
一般利用者が自分で作ったファイルなら regular file mode=600 r=yes w=yes x=no、ディレクトリは a directory mode=700 r=yes w=yes x=yes です。root、ACL、特殊な filesystem では異なるため、ここは固定の普遍出力ではなく「実際の実行主体で yes/no を確認する」手順です。書き込み前は [[ -w $p ]] だけで許可せず、実際の open が失敗した場合のエラーを処理します。存在しない新規ファイルの作成可否はファイル自身でなく親ディレクトリの write と search、および mount 状態に依存します。
stat で metadata と symlink を確認する
stat は種類、mode、owner、size、時刻、inode など filesystem metadata を表示します。GNU 形式指定なら必要項目だけを安定して出せます。既定は symlink 自体、-L/--dereference を付けると参照先を表示します。
stat -c 'name=%n type=%F mode=%a size=%s' -- "$work/regular file"
stat -c 'link=%n type=%F target=%N' -- "$work/good link"
stat -L -c 'follow=%n type=%F size=%s' -- "$work/good link"
stat -L -- "$work/broken link" >/dev/null
printf 'broken follow rc=%d\n' "$?"
通常ファイルは type=regular file mode=600 size=5、link 自体は type=symbolic link と target、dereference 後は type=regular file size=5 です。broken link の stat -L は診断を出して非0になり、最後は通常 broken follow rc=1 です。%N の引用表現は locale・coreutils 版で差があるので機械解析せず、型は %F、mode は %a のように個別指定します。BSD stat は -c 書式が異なるため GNU 固有コマンドとして扱います。
file は内容推定、拡張子判定ではない
file は filesystem test に加えて magic database、内容、文字集合などを調べ、説明または MIME type を推定します。file --brief --mime-type -- path は説明からパス名を外せます。これは「通常ファイルか」の権限分岐より、未知ファイルの内容調査に向きます。推定結果を安全性保証や parser 選択の唯一の根拠にしません。
file --brief --mime-type -- "$work/regular file"
file --brief --mime-type -- "$work/a directory"
file -h --brief --mime-type -- "$work/good link"
代表的な Linux の出力は通常ファイルが text/plain、ディレクトリが inode/directory、-h で link をたどらない good link が inode/symlink です。magic database と file の版で詳細は変わり得ます。-L は link をたどり、-h はたどらない指定です。機密ファイルは file 自身が内容を読む点も考慮します。
find -type で木の中の型を絞る
find "$work" -maxdepth 1 -type f -printf 'f:%f\n' | LC_ALL=C sort
find "$work" -maxdepth 1 -type d -printf 'd:%f\n' | LC_ALL=C sort
find "$work" -maxdepth 1 -type l -printf 'l:%f\n' | LC_ALL=C sort
第1群は f:regular file、第2群は一時ディレクトリ自身と d:a directory、第3群は l:broken link と l:good link です。-maxdepth と -printf は GNU find 拡張です。find は既定で symlink をたどらないため link は -type l、-L を付けると正常 link の参照先型で評価されますが、broken link の扱いが異なります。意図せず木の外へたどらないよう、追跡オプションを明記します。
TOCTOU: 検査と操作の間に対象は変わる
if [[ -f $p ]]; then cat -- "$p"; fi の test と cat は別々の system call です。その間に path が symlink へ交換されたり削除されたりできます。信頼できない共有ディレクトリで「検査したから安全」とはしません。読み取りなら実際の open 失敗を処理し、作成なら排他的作成、ディレクトリなら mkdir 自体の終了状態、置換なら同一 filesystem の atomic rename など、目的の操作を一段で行える API を選びます。高い安全性が必要なプログラムでは path 文字列の再検索を避け、openat 系と file descriptor 後の fstat を使います。
p=$work/new-directory
if mkdir -- "$p"; then
printf 'created directory\n'
elif [[ -d "$p" ]]; then
printf 'already a directory\n'
else
printf 'creation failed and path is not a directory\n' >&2
exit 4
fi
初回は created directory で終了値0、同じコマンドを再実行すれば mkdir はエラーを出した後に already a directory で全体は0です。同名の通常ファイルなら最後のメッセージで4になります。事前の [[ ! -e $p ]] を作成許可にせず、mkdir の結果を主判定にして競合を処理しています。この例で作ったものは一時ディレクトリ配下なので最後の trap が片付けます。
path種別判定の最終照合
- broken symlink を扱う分岐では
-Lを-eより先に確認した。 - 通常ファイルを指す link は
-fと-Lが両方真になると理解し、必要なら両条件で限定した。 - パス変数を引用し、オプション終端
--を受け付けるコマンドでは付けた。 - 権限 test は実行主体・ACL・mount の影響を受ける補助値とし、実操作の失敗を処理した。
- stat は metadata、file は内容推定、find -type は木の絞り込みとして役割を分けた。

コメント