- 技術 💻
シェルスクリプトのファイル存在チェック|-e -f -d の違い一覧
シェルスクリプトのファイル存在チェックとは、[ -e パス ] のような条件式で「そのパスに何かがあるか」を調べることです。 -e は種類を問わず存在すれば真、-f は通常ファイルのとき真、-d はディレクトリのとき真になります。
この記事では、通常ファイル・ディレクトリ・シンボリックリンク・リンク切れ・存在しないパスに対して -e -f -d -L -s -r -w -x が真か偽かを実際に測った一覧表を載せます。あわせて、空ディレクトリの判定、ワイルドカードで1件以上あるかの判定(too many arguments の再現と回避)、スペース入りパスでのクォート忘れ、bash と dash(sh)の差、「無ければ作る」「あれば退避」などの定型文を、bash 5.2.21 と dash 0.5.12(Ubuntu 24.04)の実行結果つきでまとめます。出力中のスクリプト名は check.sh に置き換えて表示しています。
ファイル存在チェックの早見表
演算子の意味
| 演算子 | 真になる条件 | よくある用途 |
|---|---|---|
-e |
存在する(種類は問わない) | ファイルでもディレクトリでもよいから、あるかどうか |
-f |
存在し、通常ファイルである | 読み込む入力ファイルの確認 |
-d |
存在し、ディレクトリである | 出力先フォルダの確認 |
-L(-h も同じ) |
シンボリックリンクである(リンク先が無くても真) | リンク切れの検出 |
-s |
存在し、サイズが 0 より大きい | 空でないファイルの確認 |
-r |
存在し、読み取りできる | 読み込み前の確認 |
-w |
存在し、書き込みできる | 書き込み前の確認 |
-x |
存在し、実行できる(ディレクトリなら中に入れる) | コマンドやスクリプトの確認 |
対象の種類×演算子の実測マトリクス
一般ユーザーで実行した結果です。T が真、― が偽です。bash の [ ]、bash の [[ ]]、dash の [ ] の3通りで測り、すべて同じ結果でした。
| 対象 | -e |
-f |
-d |
-L |
-s |
-r |
-w |
-x |
|---|---|---|---|---|---|---|---|---|
| 通常ファイル(中身あり・権限 644) | T | T | ― | ― | T | T | T | ― |
| 空のファイル(0 バイト) | T | T | ― | ― | ― | T | T | ― |
| 実行権つきスクリプト(権限 755) | T | T | ― | ― | T | T | T | T |
| 権限なしのファイル(権限 000) | T | T | ― | ― | T | ― | ― | ― |
| ディレクトリ(中身あり) | T | ― | T | ― | T | T | T | T |
| 空のディレクトリ | T | ― | T | ― | T | T | T | T |
| シンボリックリンク(ファイルを指す) | T | T | ― | T | T | T | T | ― |
| シンボリックリンク(ディレクトリを指す) | T | ― | T | T | T | T | T | T |
| リンク切れのシンボリックリンク | ― | ― | ― | T | ― | ― | ― | ― |
| 存在しないパス | ― | ― | ― | ― | ― | ― | ― | ― |
すぐ使える書き方
| やりたいこと | 書き方 |
|---|---|
| ファイルがあれば | if [ -f "$file" ]; then ... fi |
| ファイルが無ければ | if [ ! -f "$file" ]; then ... fi |
| ディレクトリが無ければ作る | [ -d "$dir" ] || mkdir -p "$dir"(mkdir -p "$dir" だけでもよい) |
| 無ければ止める | [ -f "$file" ] || { echo "$file が見つかりません" >&2; exit 1; } |
| 空でないファイルか | [ -s "$file" ] |
| ディレクトリが空か | [ -z "$(ls -A "$dir")" ] |
| パターンに合うファイルが1件以上あるか | set -- logs/*.log; [ -e "$1" ] |
| リンク切れか | [ -L "$p" ] && [ ! -e "$p" ] |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「シェルスクリプトのファイル存在チェック|-e -f -d の違い一覧」
基本の書き方:if とファイル存在チェック
#!/bin/sh
file="data/input.csv"
if [ -f "$file" ]; then
echo "$file があります"
else
echo "$file がありません"
fi
押さえる点は3つです。
| ポイント | 理由 |
|---|---|
[ の後ろと ] の前に空白を入れる |
[ は test と同じ働きのコマンドで、] はその最後の引数 |
変数は "$file" とダブルクォートで囲む |
空白入りのパスや空の変数でも正しく動く |
否定は ! を空白で区切って書く |
[ ! -f "$file" ] と ! [ -f "$file" ] のどちらでも同じ結果だった |
test -f "$file" と [ -f "$file" ] は同じ意味です。空白を入れ忘れると、次のように表示されます。
| 書き方 | bash での表示(終了コード) |
|---|---|
[ -f logs/a.log] |
check.sh: line 2: [: missing `]'(2) |
[-f logs/a.log ] |
check.sh: line 2: [-f: command not found(127) |
終了コード 0 が真、1 が偽、2 は書き方の誤りです。 if は「0 かどうか」しか見ないため、2 のときも偽と同じ else 側に進みます。書き方の誤りに気づかないまま「無い」と判定してしまうことがあるので、エラー文が出ていないかを確かめます。
-e・-f・-d の違いと選び方
迷ったら「これから何をするか」で選びます。
| これからすること | 選ぶ演算子 | 理由 |
|---|---|---|
| ファイルを読む(cat、grep、source など) | -f(読めるかまで見るなら -r も) |
ディレクトリを渡すと読めないため、通常ファイルに限定する |
| ディレクトリに移動・書き出す | -d |
同じ名前のファイルがあっても真にならない |
| 同じ名前のものがあったら上書きを避ける | -e |
ファイルでもディレクトリでも、何かあれば真 |
| 空でないことまで確かめる | -s |
0 バイトのファイルは偽になる |
| リンクそのものを調べる | -L |
ほかの演算子はリンク先を見る |
シンボリックリンクに対する動き
マトリクスのとおり、-L 以外の演算子はリンクをたどって、リンク先の状態で判定します。 そのため、ファイルを指すリンクは -f が真、ディレクトリを指すリンクは -d が真になります。
リンク切れ(リンク先が消えている)では、-e も -f も偽で、-L だけが真です。「-e が偽なのに、同じ名前で作ろうとすると File exists と言われる」ときは、リンク切れが残っている可能性があります。
ln -s gone.txt broken
if [ -L broken ] && [ ! -e broken ]; then echo "broken はリンク切れ"; fi
broken はリンク切れ
-r -w -x は「いまの実行ユーザーにとって」の判定
権限 000 のファイルは、一般ユーザーでは -r も -w も偽でした。同じファイルを root で調べると結果が変わります。
| 対象 | 実行ユーザー | -r |
-w |
-x |
|---|---|---|---|---|
| 権限 000 のファイル | 一般ユーザー | ― | ― | ― |
| 権限 000 のファイル | root | T | T | ― |
| 権限 644 のファイル | root | T | T | ― |
root では読み書きの権限チェックを通過するため、-r と -w はほぼ常に真になります。 -x は、どこにも実行権が付いていないファイルでは root でも偽でした。cron や sudo で動かすスクリプトは、手元で試したときと実行ユーザーが違うことがあるので、この差を意識しておくと原因を追いやすくなります。
通常ファイル以外を見分ける演算子
-f が偽になる「通常ファイルではないもの」には、専用の演算子があります(dash と bash で同じ結果)。
| 演算子 | 真になる対象 | 実行結果 |
|---|---|---|
-p |
名前付きパイプ | mkfifo で作った pipe で真 |
-c |
キャラクタデバイス | /dev/null で真 |
-S |
ソケット | 稼働中のデータベースのソケットファイルで真 |
-b |
ブロックデバイス | 未実行(この環境に該当ファイルが無い) |
/dev/null は -e が真、-f と -d は偽でした。「出力先が /dev/null でも動くようにしたい」スクリプトでは、-f ではなく -e で確認すると止まらずに進めます。
クォート忘れで起きること(スペース入りパス・空の変数)
スペース入りのパス
f="my docs/report 2026.txt" # 実在するファイル
g="my docs" # 実在するディレクトリ
[ -f $f ]
[ -f "$f" ]
[ -d $g ]
実行結果(bash):
$ [ -f $f ]
check.sh: line 2: [: too many arguments
-> 終了コード 2
$ [ -f "$f" ]
-> 終了コード 0
$ [ -d $g ]
check.sh: line 2: [: my: binary operator expected
-> 終了コード 2
$ [ -d "$g" ]
-> 終了コード 0
クォートが無いと、シェルは空白で区切って [ -f my docs/report 2026.txt ] と3つの引数に分けてから [ に渡します。引数の数によってメッセージが変わります。
| 展開後の形 | bash のメッセージ | dash のメッセージ |
|---|---|---|
[ -d my docs ](演算子の後ろが2語) |
[: my: binary operator expected | [: my: unexpected operator |
[ -f my docs/report 2026.txt ](3語以上) |
[: too many arguments | [: my: unexpected operator |
空の変数:エラーにならず「真」になる
もう一つ、気づきにくい動きがあります。変数が空のとき、クォートが無いと判定が真になります。
empty=""
[ -f $empty ]
[ -f "$empty" ]
[ -e $undefined_var ]
[ -d $empty ] && echo "ディレクトリがあると判定されてしまう"
$ [ -f $empty ]
-> 終了コード 0
$ [ -f "$empty" ]
-> 終了コード 1
$ [ -e $undefined_var ]
-> 終了コード 0
$ [ -d $empty ] && echo "ディレクトリがあると判定されてしまう"
ディレクトリがあると判定されてしまう
-> 終了コード 0
$empty が消えて [ -f ] になり、これは「-f という文字列が空でないか」という別の判定として扱われるためです。dash でも同じく終了コード 0 でした。エラー文が出ないので、設定の読み込みに漏れがあった場合などに「ある」と判定して先へ進んでしまいます。
対策は、変数を必ず "$var" と囲むことです。 これだけで、空白入りのパスと空の変数の両方に対応できます。
| 書き方 | スペース入りパス | 空の変数 |
|---|---|---|
[ -f $f ] |
エラー(終了コード 2) | 真になってしまう |
[ -f "$f" ] |
正しく判定 | 偽(正しい) |
[[ -f $f ]](bash) |
正しく判定 | 偽(正しい) |
bash の [[ ]] はクォート無しでも単語分割をしないため、どちらも正しく判定されました。ただし [[ ]] は sh(dash)では使えないので、後述の使い分けを確認してください。
ワイルドカードで「1件以上あるか」を判定する
[ -f logs/*.log ] のように、条件式の中に直接ワイルドカードを書くと、一致した件数によって結果が変わります。
(logs/ が空のとき)
$ [ -f logs/*.log ]
-> 終了コード 1
(a.log の1件だけ)
$ [ -f logs/*.log ]
-> 終了コード 0
(a.log, b.log の2件)
$ [ -f logs/*.log ]
check.sh: line 2: [: logs/a.log: binary operator expected
-> 終了コード 2
(a.log, b.log, c.log の3件)
$ [ -f logs/*.log ]
check.sh: line 2: [: too many arguments
-> 終了コード 2
| 一致した件数 | シェルが展開した形 | 結果 |
|---|---|---|
| 0件 | [ -f 'logs/*.log' ](パターンの文字のまま) |
偽(その名前のファイルは無い) |
| 1件 | [ -f logs/a.log ] |
真 |
| 2件 | [ -f logs/a.log logs/b.log ] |
binary operator expected |
| 3件以上 | [ -f logs/a.log logs/b.log logs/c.log ] |
too many arguments |
1件のときだけ動くので、テスト時には気づかず、ファイルが増えた日に止まるという形で表面化します。また、bash の [[ -f logs/*.log ]] は、[[ ]] の中ではワイルドカードを展開しないため、3件あっても偽(終了コード 1)でした。
回避する書き方
| 方法 | 書き方 | 1件以上あるとき | 0件のとき | sh(dash) |
|---|---|---|---|---|
| 位置パラメータに展開して先頭を確認 | set -- logs/*.log; [ -e "$1" ] |
0 | 1 | 動く |
| find で1件見つけたら終了 | [ -n "$(find logs -maxdepth 1 -name '*.log' -print -quit)" ] |
0 | 1 | 動く |
| compgen -G | compgen -G "logs/*.log" > /dev/null |
0 | 1 | bash 専用(dash では compgen: not found) |
| nullglob と配列 | 下のコード | 0 | 1 | bash 専用 |
| ls の終了コード | ls logs/*.log > /dev/null 2>&1 |
0 | 2 | 動く(件数が非常に多いと引数の上限に達することがある) |
sh でも bash でも動く、おすすめの形です。
#!/bin/sh
set -- logs/*.log
if [ -e "$1" ]; then
echo "ログが $# 件あります"
else
echo "ログはありません"
fi
set -- は位置パラメータ($1、$2…)を入れ替えます。一致が0件のときは $1 にパターンの文字列(logs/*.log)がそのまま入るので、-e で実在を確かめています。今回の実行では、3件のときに「件数=3」で終了コード 0、0件のパターンでは終了コード 1 でした。スクリプトの引数を後で使う場合は、関数の中でこの処理を行うと、元の引数を保てます。
bash で件数や一覧も使いたいときは、nullglob と配列が扱いやすくなります。
#!/bin/bash
shopt -s nullglob # 一致0件なら、パターンを空に展開する
files=(logs/*.log)
shopt -u nullglob
echo "${#files[@]} 件"
if [ ${#files[@]} -gt 0 ]; then
for f in "${files[@]}"; do
echo "処理: $f"
done
fi
実行結果(件数の行)は、3件あるとき「3 件」、一致しないパターンに替えたとき「0 件」でした。
ディレクトリが空かどうかを判定する
-d はディレクトリの有無を、-s はファイルのサイズを見ますが、ディレクトリが空かどうかを直接調べる演算子はありません(マトリクスのとおり、空のディレクトリでも -s は真です)。そこで、中身を数える書き方を使います。
3種類のディレクトリで比べました。empty は空、full には a.txt、hidden には隠しファイル .env だけが入っています。
for d in empty full hidden; do
if [ -z "$(ls -A "$d")" ]; then echo "ls -A: $d は空"; else echo "ls -A: $d は空ではない"; fi
done
ls -A: empty は空
ls -A: full は空ではない
ls -A: hidden は空ではない
| 方法 | 書き方 | empty | full | hidden(.env のみ) | sh(dash) |
|---|---|---|---|---|---|
| ls -A | [ -z "$(ls -A "$d")" ] |
空 | 空でない | 空でない | 動く |
| find | [ -z "$(find "$d" -mindepth 1 -maxdepth 1 -print -quit)" ] |
空 | 空でない | 空でない | 動く |
| nullglob と dotglob | 下の関数 | 空 | 空でない | 空でない | bash 専用 |
| ls(-A なし) | [ -z "$(ls "$d")" ] |
空 | 空でない | 空と判定 | 動く |
ls に -A を付けないと、隠しファイルだけのディレクトリを「空」と判定します。 設定ファイル(.env や .gitkeep など)だけが入ったフォルダを空とみなして削除する、といった動きを避けるために、-A を付けるか find を使います。
ファイル数が多いディレクトリでは、1件見つけた時点で終わる find(-print -quit)が向いています。bash だけで完結させたい場合は次の関数が使えます。
#!/bin/bash
is_empty_dir() (
shopt -s nullglob dotglob # ( ) のサブシェル内だけで設定が有効
f=("$1"/*)
[ ${#f[@]} -eq 0 ]
)
if is_empty_dir "work"; then echo "空です"; fi
空のディレクトリだけを探したり、空なら消したりする場合は、専用のコマンドが簡潔です。
$ find . -maxdepth 1 -type d -empty
./empty
$ rmdir full
rmdir: failed to remove 'full': Directory not empty
rmdir は空のときだけ削除するので、「空なら消す」は判定を書かずに rmdir "$dir" 2>/dev/null と書けます。
コピペ用の定型文(sh・bash 共通)
次のスクリプトは dash と bash の両方で実行し、同じ結果になることを確かめています。
#!/bin/sh
# 1) ディレクトリが無ければ作る
dir="out/2026/10"
[ -d "$dir" ] || mkdir -p "$dir"
# 2) ファイルが無ければ初期内容で作る
conf="app.conf"
if [ ! -e "$conf" ]; then
printf 'mode=dev\n' > "$conf"
echo "$conf を作成"
fi
# 3) あれば日時つきの名前に退避してから作り直す
if [ -e "$conf" ]; then
bak="$conf.$(date +%Y%m%d_%H%M%S).bak"
mv -- "$conf" "$bak"
echo "退避: $bak"
fi
printf 'mode=prod\n' > "$conf"
# 4) 無ければメッセージを出して止める
input="input.csv"
if [ ! -f "$input" ]; then
echo "エラー: $input が見つかりません" >&2
exit 1
fi
4) までの実行結果:
app.conf を作成
退避: app.conf.20261003_142833.bak
エラー: input.csv が見つかりません
(終了コード 1)
続けて使える定型文です(こちらも実行して確認)。
# 5) 読めて、かつ空でないか
if [ -r "$f" ] && [ -s "$f" ]; then echo "$f: 読めて中身あり"; else echo "$f: 空か読めない"; fi
# 6) あれば消す(無くてもエラーにしない)
rm -f -- "$f"
# 7) ファイルができるまで待つ(最大5秒)
i=0
until [ -e ready.flag ]; do
i=$((i+1)); [ "$i" -gt 5 ] && { echo "時間切れ"; break; }
sleep 1
done
# 8) コマンドが使えるか
if command -v git >/dev/null 2>&1; then echo "git あり"; fi
# 9) 二重起動を避ける(mkdir は「確認」と「作成」を同時に行う)
if mkdir lock.d 2>/dev/null; then
echo "ロック取得"
# ... 処理 ...
rmdir lock.d
else
echo "すでに実行中"
fi
| 定型文 | 実行結果 | ポイント |
|---|---|---|
| 1) 無ければ作る | out/2026/10 ができた | mkdir -p は既にあってもエラーにならないので、判定を省いてもよい |
| 2) 無ければ初期内容で作る | 1回目だけ作成 | -e を使うと、同名のディレクトリがあるときも上書きを避けられる |
| 5) 読めて空でない | 中身ありと 0 バイトで分岐 | -a でつなぐより && で2つに分けるほうが読みやすい |
| 6) あれば消す | 2回続けても終了コード 0 | rm -f は無くても 0 を返すので、判定が要らない |
| 7) 待つ | 1秒後に作られたファイルを2回目の確認で検出 | 上限回数を決めて、待ち続けない形にする |
| 9) 二重起動を避ける | 1回目「ロック取得」、2回目「すでに実行中」 | 「確認してから作る」の間に別の処理が割り込む余地が無い |
9) の考え方は大切です。[ -e lock ] || touch lock のように確認と作成が別々のコマンドだと、その間に別のプロセスが同じ確認を通過することがあります。 mkdir のように「無ければ作る、あれば止まる」を1回で行うコマンドを使うと、この心配がなくなります。
関数にまとめる:そのまま使えるチェック用ライブラリ
同じ判定を何度も書くなら、関数にまとめると読みやすくなります。次のスクリプトは dash と bash の両方で実行し、出力が一致することを確かめています。
#!/bin/sh
# 無ければメッセージを出して終了
require_file() {
[ -f "$1" ] && [ -r "$1" ] && return 0
echo "エラー: ファイルを読めません: $1" >&2
exit 1
}
# 無ければ作る(同名のファイルがあれば知らせる)
ensure_dir() {
if [ -e "$1" ] && [ ! -d "$1" ]; then
echo "エラー: $1 はディレクトリではありません" >&2
return 1
fi
mkdir -p "$1"
}
# あれば .bak を付けて退避
backup_if_exists() {
[ -e "$1" ] || return 0
mv -- "$1" "$1.bak" && echo "退避: $1 -> $1.bak"
}
ensure_dir "out/logs" && echo "out/logs を用意"
: > memo
ensure_dir "memo"; echo " -> 終了コード $?"
echo v1 > result.txt
backup_if_exists result.txt
backup_if_exists nothing.txt; echo " -> 終了コード $?"
require_file "result.txt.bak" && echo "result.txt.bak は読めます"
require_file "input.csv"
echo "ここには来ません"
実行結果:
out/logs を用意
エラー: memo はディレクトリではありません
-> 終了コード 1
退避: result.txt -> result.txt.bak
-> 終了コード 0
result.txt.bak は読めます
エラー: ファイルを読めません: input.csv
-> スクリプトの終了コード 1
ensure_dir では、-e と -d を組み合わせて「同じ名前のファイルが既にある」場合を見分けています。 -d だけで判定して mkdir に進むと、mkdir 側のエラーになって原因がわかりにくくなるためです。
必要なファイルをまとめて確認する
処理の最初に、必要なファイルを一度に確認しておくと、途中で止まることが減ります。不足しているものを全部表示してから終了する形です。
#!/bin/sh
missing=0
for f in tool/run.sh tool/conf/app.conf tool/conf/db.conf "tool/read me.txt"; do
if [ ! -f "$f" ]; then
echo "不足: $f" >&2
missing=$((missing+1))
fi
done
[ "$missing" -eq 0 ] || { echo "$missing 件のファイルが不足しています" >&2; exit 1; }
不足: tool/conf/db.conf
不足: tool/read me.txt
2 件のファイルが不足しています
-> 終了コード 1
つまずきやすい3つの場面(実行結果つき)
1行書きの条件分岐は if の代わりにならない
1行で書ける [ 条件 ] && 処理 || 別の処理 は便利ですが、条件が真でも、処理が 0 以外で終わると || の側も実行されます。
touch exists.txt
[ -f exists.txt ] && echo "あります" || echo "ありません"
[ -f exists.txt ] && false || echo "ありません(と表示されてしまう)"
if [ -f exists.txt ]; then false; else echo "ありません"; fi
echo "if 文では何も表示されない"
あります
ありません(と表示されてしまう)
if 文では何も表示されない
2行目は、ファイルがあるのに「ありません」側が動いています。真のときと偽のときで別の処理をするなら、if 文で書くのが確実です。&& や || の1行書きは、片方だけの処理(無ければ作る、無ければ止める)に使います。
クォートの中のチルダは展開されない
[ -d "~/work" ] && echo "A: あり" || echo "A: なし"
[ -d ~/work ] && echo "B: あり" || echo "B: なし"
[ -d "$HOME/work" ] && echo "C: あり" || echo "C: なし"
A: なし
B: あり
C: あり
ダブルクォートの中の ~ はホームディレクトリに置き換わらず、「~」という名前のフォルダを探します。クォートと両立させるには "$HOME/work" と書きます。
相対パスは「実行した場所」が基準になる
スクリプトの隣にある設定ファイルを相対パスで確認すると、別の場所から実行したときに見つかりません。
#!/bin/sh
# tool/run.sh (隣に tool/conf/app.conf がある)
script_dir=$(cd "$(dirname "$0")" && pwd)
[ -f conf/app.conf ] && echo "相対パス: あり" || echo "相対パス: なし"
[ -f "$script_dir/conf/app.conf" ] && echo "スクリプト基準: あり" || echo "スクリプト基準: なし"
$ cd tool && sh run.sh
相対パス: あり
スクリプト基準: あり
$ sh tool/run.sh
相対パス: なし
スクリプト基準: あり
cron から動かすときも、カレントディレクトリは手元で試したときと異なります。スクリプトの場所を基準にしたパス(script_dir)で確認すると、どこから実行しても同じ結果になります。
set -e と組み合わせるとき
set -e(コマンドが 0 以外で終わったらスクリプトを止める設定)を使っているスクリプトでは、[ 条件 ] && 処理 の置き場所で動きが変わります。
#!/bin/sh
set -e
[ -f nothing.txt ] && echo "あります"
echo "次の行に進みました"
check() { [ -f nothing.txt ] && echo "あります"; }
check
echo "ここには来ません"
次の行に進みました
-> 終了コード 1
3行目のように && の左側が偽になっただけでは止まりません。一方、同じ式を関数の最後に置くと、関数全体の終了コードが 1 になり、呼び出した行でスクリプトが止まりました(dash・bash とも同じ)。関数の最後で判定を使うときは、if 文にするか、末尾に return 0 を置くと意図どおりに進みます。
角かっこ1つ・2つ・test の違い、bash と dash の差
| 書き方 | 使えるシェル | 単語分割 | ワイルドカード展開 | 向いている場面 |
|---|---|---|---|---|
[ ... ] |
sh(dash)・bash など | する(クォートが必要) | する | どのシェルでも動かしたいスクリプト |
test ... |
同上 | する | する | [ ] と同じ。好みで選ぶ |
[[ ... ]] |
bash・zsh・ksh | しない | しない | bash 専用と決めているスクリプト |
Ubuntu や Debian では /bin/sh が dash を指しています(今回の環境でも /bin/sh は dash へのリンクでした)。そのため、シバン(1行目)が #!/bin/sh のスクリプトや、sh ファイル名 で実行したスクリプトでは [[ ]] が使えません。
$ dash -c '[[ -f /etc/hosts ]]; echo "code $?"'
dash: 1: [[: not found
code 127
終了コード 127 は偽として扱われるため、if の中では「常に else 側に進む」という動きになります。
今回確かめた bash と dash の差
| 項目 | bash 5.2.21 | dash 0.5.12 |
|---|---|---|
-e -f -d -L -s -r -w -x のマトリクス |
上の表のとおり | bash と同じ結果 |
[[ ]] |
使える | [[: not found(終了コード 127) |
compgen -G |
使える | compgen: not found(終了コード 127) |
配列、shopt -s nullglob |
使える | 使えない |
| クォート無しで空白入りパス | binary operator expected/too many arguments | unexpected operator |
] の前の空白忘れ |
[: missing `]' | [: missing ] |
クォート無しで空の変数 [ -f $empty ] |
真(終了コード 0) | 真(終了コード 0) |
[ a -nt b ](a のほうが新しい) |
使える | 使える |
[ a -ef b ](同じ実体) |
使える | 使える |
[ -f a -a -r a ] |
使える | 使える |
set -- パターン; [ -e "$1" ] |
使える | 使える |
演算子そのものの結果は bash と dash で同じでした。差が出るのは [[ ]]、compgen、配列といった bash の拡張機能と、エラーメッセージの文言です。
2つのファイルを比べる演算子
| 演算子 | 真になる条件 | 実行結果 |
|---|---|---|
a -nt b |
a の更新日時が b より新しい | 後から作った c.log と a.log で [ logs/c.log -nt logs/a.log ] が真 |
a -ot b |
a の更新日時が b より前 | 未実行(-nt の逆) |
a -ef b |
a と b が同じ実体(パスの書き方が違っても) | [ logs/a.log -ef ./logs/../logs/a.log ] が真 |
そのほか、-d logs/ のように末尾にスラッシュを付けても真、-f logs/a.log/ のように通常ファイルの末尾にスラッシュを付けると偽でした。bash には -e と同じ意味の -a ファイル もありますが、「かつ」を表す -a と紛らわしいので -e を使うほうが読みやすくなります。
エラーメッセージ別の原因と直し方
| メッセージ | 原因 | 直し方 |
|---|---|---|
| [: too many arguments | 変数に空白が含まれる、またはワイルドカードが3件以上に展開された | 変数を "$var" で囲む。ワイルドカードは set -- や find で判定 |
| [: 文字列: binary operator expected | 演算子の後ろが2語になった(空白入りパス、2件に展開) | 同上 |
| [: 文字列: unexpected operator(dash) | 上の2つと同じ原因 | 同上 |
| [: missing `]' | ] の前に空白が無い |
] の前に空白を入れる |
| [-f: command not found | [ の後ろに空白が無い |
[ の後ろに空白を入れる |
| [[: not found | sh(dash)で [[ ]] を使った |
[ ] に書き換える、または bash で実行する |
| エラーは出ないが、無いのに「ある」と判定される | クォート無しの変数が空だった | "$var" で囲む |
| エラーは出ないが、あるのに「無い」と判定される | リンク切れ、[[ ]] の中のワイルドカード、権限の不足 |
-L で確認、展開方法を見直す、実行ユーザーを確認 |
よくある質問
Q. -e と -f のどちらを使えばよいですか?
A. 「読み込むファイルがあるか」なら -f、「同じ名前のものが既にあるか」なら -e です。-f はディレクトリに対して偽になるので、ファイル名のつもりのパスにフォルダがあった場合も区別できます。
Q. ファイルが存在しないことを確認するには?
A. [ ! -e "$path" ] と書きます。! の前後に空白が必要です。! [ -e "$path" ] でも同じ結果でした。
Q. 複数の条件を組み合わせるには?
A. [ -f "$f" ] && [ -r "$f" ] のように、[ ] を &&(かつ)や ||(または)でつなぎます。[ -f "$f" -a -r "$f" ] という書き方も今回の bash・dash では動きましたが、条件が増えると解釈があいまいになることがあるため、&& で分けるほうが確実です。
Q. 存在を確認してから処理すれば安全ですか?
A. 確認と処理の間に、ほかのプロセスが作成・削除することがあります。mkdir -p、rm -f、定型文 9) の mkdir によるロックのように、確認と操作を1つのコマンドで行える方法があればそちらを選ぶと、判定そのものが不要になります。
まとめ
-eは存在、-fは通常ファイル、-dはディレクトリ。読むなら-f、書き出し先なら-d、重複の確認なら-e-L以外はシンボリックリンクをたどって判定する。リンク切れは-eが偽、-Lだけが真- 変数は必ず
"$var"で囲む。囲まないと、空白入りパスでエラー、空の変数では真になる - 条件式に直接ワイルドカードを書かない。
set -- パターン; [ -e "$1" ]なら sh でも bash でも動く - 空ディレクトリの判定は
ls -Aか find。-Aが無いと隠しファイルを見落とす [[ ]]と compgen、配列は bash 専用。#!/bin/sh のスクリプトでは[ ]を使うmkdir -pやrm -fのように、判定が要らないコマンドを活用する
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「シェルスクリプトのファイル存在チェック|-e -f -d の違い一覧」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。