はじめに
2026-09-14、Mac の内蔵SSDを整理していた。先にファイルの一覧(サイズ・更新時刻・パス)をテキストに書き出しておき、~/Downloads の下の画像を、サイズと更新日つきで並べようとした。
grep -E " (Downloads|Movies|Documents/2018)" media.txt | grep -v iCloud写真 \
| while read sz mt path; do
printf "%7.1fM %s %s\n" $((sz/1048576.0)) "$(date -r $mt +%F)" "$path"
done
出てきたのはこれだった。
(eval):1: command not found: date
1.0M Downloads/ChatGPT Image 2026年6月15日 23_53_56.png
(eval):1: command not found: date
0.1M Downloads/名称未設定のデザイン-2.png
(eval):1: command not found: date
0.3M Downloads/Screenshot_20260702-220057.png
サイズとパスは出ている。日付の欄だけが空で、1行ごとに date が見つからないと言われる。同じコマンドの後ろに続けていた集計も、全部同じ形で落ちた。
(eval):1: command not found: awk
(eval):1: command not found: sort
(eval):1: command not found: sed
(eval):1: command not found: ls
(eval):1: command not found: head
path は PATH の別名
原因はループの変数名 path だった。zsh では小文字の path は配列で、大文字の PATH と結びついている。片方に代入すると、もう片方も書き換わる。
print -r -- "path: ${(t)path} / PATH: ${(t)PATH}"
# path: array-tied-special / PATH: scalar-tied-export-special
read sz mt path は1行読むたびに、3つ目の欄(ファイルのパス)を path に入れる。その瞬間に PATH もそのファイルのパスに変わり、/bin も /usr/bin も検索先から消える。
printf と $(( )) の計算は zsh の組み込みなので、PATH が無くても動く。だからサイズとパスは正しく出て、外部コマンドの date だけが消えた。壊れ方が半分だけで、行の形はそれらしく出ていた。
最小の再現
zsh 5.9(macOS 標準)で、設定ファイルを読まずに再現した。
zsh -f -c '
printf "12 2026-09-14 /tmp/a\n" | while read sz mt path; do
print -r -- "PATH=$PATH"
date
awk "BEGIN{print 1}"
done
print -r -- "ループの後 PATH=$PATH"
ls /
'
PATH=/tmp/a
zsh:1: command not found: date
zsh:1: command not found: awk
ループの後 PATH=
zsh:1: command not found: ls
ループの中では PATH が /tmp/a になっている。ループを抜けたあとは空になっていた。zsh の read は入力の終わりで失敗すると、変数を空にして終わる(a=x のあとに空の入力を read a すると、終了コード1で a は空になった)。最後の1回で path が空になり、PATH も一緒に空になる。壊れた PATH はループの後ろまで残る。
同じ1行を bash 3.2(macOS 標準の /bin/bash)で流すと、date はそのまま動く。
printf "12 x /tmp/a\n" | while read sz mt path; do date +%F; done
# 2026-10-12
bash では path はただの変数で、PATH とは関係がない。bash で覚えた書き方を zsh にそのまま持ち込むと、ここで踏む。
関数の中でも消える
local path=... と関数の中に閉じ込めても、その関数の中では同じことが起きる。
f() { local path=/x; date +%F; }
f
# f: command not found: date
date +%F
# 2026-10-12
関数を抜ければ戻る。ただ、関数の中で外部コマンドを呼ぶなら、local を付けても避けられない。
結びついている名前はほかにもある
path と同じ「配列と大文字の変数が結びついた」ものは、手元の zsh 5.9 でほかに3つあった。
print -r -- "fpath: ${(t)fpath} / cdpath: ${(t)cdpath} / manpath: ${(t)manpath}"
# fpath: array-tied-special / cdpath: array-tied-special / manpath: array-tied-special
| 小文字 | 結びついている先 |
|---|---|
path |
PATH(コマンドの検索先) |
fpath |
FPATH(関数の自動読み込み先) |
cdpath |
CDPATH(cd の検索先) |
manpath |
MANPATH(man の検索先) |
実際に壊れるところまで確かめたのは path だけだ。ファイルのパスを受ける変数は、この4つを避けて fname や file にした。直したあとの同じループはこうなる。
printf "12 x /tmp/a\n" | while read sz mt fname; do date +%F; done
# 2026-10-12
パスを受け取る変数だから path と名付けた。名前としては一番素直で、それがそのまま、シェルが自分の道を探すための名前だった。