cdは、ワーキングディレクトリーを変える。
cdについて今更語るべきことはないように思われる。ワーキングディレクトリーという概念が存在する以上cdが必要なことに疑いはなく、cdはやるべきことをやるだけのコマンドではないか。
cdについてよくある理解はこうだ。cd directory
するとdirectory
という名前のディレクトリーをワーキングディレクトリにする。引数を省略するとホームディレクトリをワーキングディレクトリにする。
~$cd boring_job
~/boring_job$ cd
~/$
cdの名前にしたってChange Directoryというひねりのないものだ。
しかし、実はcdには罠がある。
cdがデフォルトで扱うパスは論理パスなのだが、ほとんどのコマンドが扱うパスは物理パスなのだ。具体的な例を見てみよう。
~$ mkdir ~/test/foo ~/test/bar
~$ ln -s ~/test/foo ~/test/bar/foo
~$ cd test/bar/foo
~/test/bar/foo$ ls ..
bar foo
~/test/bar/foo$ cd ..
~/test/bar$ ls
foo
なんと、ls ..
とcd ..; ls
の結果が異なっているではないか。
これはcdやワーキングディレクトリーは論理パスを使うのに対し、通常のプログラムは物理パスを使うために起きる。
ここでは、~/test/bar/foo
は~/test/foo
へのシンボリックリンクだ。~/test/bar/foo
にcdすると、cdは論理パスを扱うので、ワーキングディレクトリは実際に~/test/bar/foo
になる。しかし、物理パスは~/test/foo
である。なぜならばシンボリックリンクだからだ。
ここでls ..
すると何が起きるか。lsは物理パスを扱う。つまり、ここでの..
は、~/test/foo
の親ディレクトリである~/test
と同じ意味だ。この中にはディレクトリfoo
とbar
がある。
ここでcd ..
するとどうなるか。cdは論理パスを扱うので、~/test/bar/foo
の親ディレクトリである~/test/bar
がワーキングディレクトリーとなる。このなかにはシンボリックリンクファイルfoo
がある。
論理パスと物理パスの違いを意識せずにシェルスクリプトを書くと思いもよらぬ不具合を作り込むことがある。
cdには引数として-L
と-P
がある。-L
はcdに論理パスを使わせる通常の動作をし、-P
はcdに物理パスを使わせる。
先程の例を引き続き使うと、
~/test/bar/foo$ cd ..
~/test/bar$ cd foo
~/test/bar/foo$ cd -P ..
~/test$
すでに知ったように、cdはデフォルトで論理パスを扱うので、ワーキングディレクトリーが~/test/bar/foo
のときにcd ..
するということは、/test/bar
がワーキングディレクトリーになる。
一方cd -P ..
すると、物理パスを使うようになるので、物理パス~/test/foo
にとっての親ディレクトリーである~/test
がワーキングディレクトリーになる。
POSIXにはCDPATHという環境変数がある。
とても深いディレクトリパスの下に興味深いディレクトリがある場合を考える。例えばディレクトリーpics
下に画像ファイルをいれていて、その下には更にかわいい画像ファイルを入れるディレクトリーcute
があり、その下に猫画像と犬画像を入れるディレクトリーcats
, dogs
がそれぞれあるとする。
~$ cd pics/cute/
~/pics/cute$ ls
cats dogs
作業中、にわかに猫の画像が見たくなったとする。ワーキングディレクトリーを~/pics/cute/cats
に変えたいが、cd ~/pics/cute/cats
するのは面倒だ。
~$ cd cats
bash: cd: cats: No such file or directory
~$ cd ~/pics/cute/cats
~/pics/cute/cats$
この場合、環境変数CDPATHが使える。
cdは環境変数CDPATHにコロン区切りで記述されたパスと引数のディレクトリ名を連結し、存在するディレクトリパスである場合、そのディレクトリをワーキングディレクトリーとする。
~$ CDPATH=~/pics/cute
~$ cd cats
~/pics/cute/cats$ cd dogs
~/pics/cute/dogs$
CDPATHを使うとワーキングディレクトリーに関わらずよく使うディレクトリ下のディレクトリだけを指定するだけでワーキングディレクトリーを変えることができる。
POSIXには環境変数としてPWDとOLDPWDが規定されている。
環境変数PWDは現在のワーキングディレクトリーのパスが入っている。
環境変数OLDPWDには前回のワーキングディレクトリーンパスが入っている。
~$ echo $PWD
/home/ezoe
~$ cd pics/cute/dogs
~/pics/cute/dogs$ echo $OLDPWD
/home/ezoe
前回のディレクトリに戻る場合cd $OLDPATH
が使えるが、実はcd -
も同様の挙動をすることがPOSIXによって規定されている。
~/pics/cute/dogs$ cd -
~$
もしワーキングディレクトリーに-
という名前のディレクトリーがある場合に衝突する。
~$ mkdir foo foo/-
~$ cd foo
~/foo$ ls
-
~/foo$ cd -
~$
この場合は何らかの方法で-
ではない引数でパスを指定しなければならない。
~/foo$ cd ./-
~/-$
ここまではPOSIXの規格の範囲内のcdをみてきた。cdはシェルの組み込みコマンドとして実装されているので、シェルごとに挙動が異なる。現実ではPOSIXに準拠する以上の高機能なシェルが使われているので、有名なシェルのcdの振る舞いを見ていこう。
bashはBrian FoxによってGNUでBourne shellを代替するために書かれた。1989年にリリースされ、多くの環境でデフォルトのログインシェルとなっている。
bashのcdはPOSIX準拠かつ、以下のような引数を持つ。
cd [-L|[-P [-e]] [-@] [directory]
-e
は-P
と同時に使った場合、ディレクトリーの変更に成功したが現在のワーキングディレクトリーが決定できない場合に、非成功を返す。
「変更に成功したが決定できない場合」というのはとても奇妙な状況だ。具体的には以下のようにして作り出すことができる。
~$ mkdir -p a/b
~$ cd a/b
~/a/b$ rmdir ../b ../../a
~/a/b$ cd ..
cd: error retrieving current directory: getcwd: cannot access parent directories: No such file or directory
ezoe@ezoe-Precision-5530:~/a/b/..$
これは何をしているのかというと~/a/b
というディレクトリを作り、ワーキングディレクトリーにした上で、~/a/b
も~/a
も消している。ワーキングディレクトリーがなぜ削除できるのかと不思議に思うかもしれないが、Linuxカーネルではファイルは参照カウントされていて、最後の参照がcloseされた時点で削除されるから、rmdir ../b ../../a
を実行した時点では、まだディレクトリーは消えていないのだ。なぜならば、bashがopenしているからだ。
cd ..
を実行するとcloseされるのでディレクトリが消える。その結果、ディレクトリを変更に成功したが、現在のワーキングディレクトリーが決定できないという状況になる。
このとき、-e
の有無で挙動が違う。-e
がないと成功する。
~/a/b$ cd -P ..
cd: error retrieving current directory: getcwd: cannot access parent directories: No such file or directory
~/a/b/..$ echo $?
0
-e
をつけると、失敗する。
~/a/b$ cd -Pe ..
cd: error retrieving current directory: getcwd: cannot access parent directories: No such file or directory
~/a/b/..$ echo $?
1
-@
は変わっている。bashのドキュメントでは以下のように書かれている
On systems that support it, the -@ option presents the extended attributes associated with a file as a directory.
対応しているシステムでは、
-@
はファイルに関連付けられているextended attributesをディレクトリーとして表現する。
今の所、対応しているシステムはSolarisしかないようだ。理屈としては、foobarというファイルがありそのなかにfooとbarという名前のextended attributesがある場合、
~$ touch foobar
~$ setfattr -n foo foobar
~$ setfattr -n bar foobar
~$ cd foobar
~/foobar$ ls
bar foo
のように、あたかもファイルをディレクトリ名のように扱ってワーキングディレクトリーとし、そのファイルのextended attributesをファイルのように取り扱うことができる機能のようだ。
Linuxカーネルではextended attributesは極めて限定的なサポートしかされておらず、任意の名前をつけることもままならない。
bashには他にも、ディレクトリースタックという機能がある。pushdで現在のワーキングディレクトリーをスタックに積み、popdで取り出す。
~$ mkdir foo bar
~$ cd foo
~/foo$ pushd ~/bar
~/bar$ popd
~/foo$
現在スタックに積まれているディレクトリーはdirsで確認できる。
~$ mkdir -p foo/bar
~$ pushd foo
~/foo ~
~/foo$ pushd bar
~/foo/bar ~/foo ~
~/foo/bar$ dirs
~/foo/bar ~foo ~
popdに+Nや-Nを指定することで、スタックの上か底からN個のディレクトリーを取り除き変更する。+ならばdirsの出力の左からカウント、-ならば右からカウントということもできる。
~/foo/bar$ dirs
~/foo/bar ~foo ~
~/foo/bar$ popd +2
~$dirs
~
zshはPaul Falstadによって書かれ、1990年にリリースされたシェルだ。2019年にGPLv3嫌いのAppleがbashに変わってデフォルトログインシェルにしたのでbashに次いで利用者が多いシェルだ。
zshは様々な挙動に対してフック関数を登録できる。chpwdという関数を書いておくと、ワーキングディレクトリーが変更された場合に呼び出される。例えばディレクトリーの変更時に自動でlsが実行されるようにしたければ、
chpwd() {
ls
}
と書けばよい。他にもchpwd_functionsという配列もあり、この中に関数を入れておくと呼び出してくれる。
zshのcdはPOSIXに準拠した上で、追加の機能がある。
zshのcdには-q
オプションがあり、これを使うとchpwdとchpwd_functionsによるフック関数の呼び出しを行わない。
zshにはディレクトリーを2つ引数にとるcdがある。
cd old new
これはカレントディレクトリーからoldをnewで置換した上でそのディレクトリーに変更を試みる。
~$ mkdir foo bar
~$ cd foo
~/foo$ cd foo bar
~/bar$
置換は途中でもいいので上の方のディレクトリー名が違って残りが同じディレクトリー構造の場合に便利だ
~$ mkdir foo bar foo/baz bar/baz
~$ cd foo/baz
~/foo/baz$ cd foo bar
~/bar/baz$
-s
は、ディレクトリーにシンボリックリンクが含まれていた場合、ディレクトリーの変更を拒否する。
~$ mkdir foo
~$ ln -s ./foo bar
~$ cd -s bar
cd: not a directory: bar
~$ cd bar
~/bar$
zshもディレクトリースタックをサポートしていて、pushd, popd, dirsがある。さらにcdもpopdとして使うことができる
cd {+|-}n
これにより、-
と同じ問題が起きる。+nや-nといったディレクトリに素直にcdできなくなる。
~$ mkdir +1
~$ cd +1
cd: no such entry in dir stack
~$ cd ./+1
~/+1$
fishはAxel Liljencrantzによって書かれ、2005年にリリースされた。
fishはPOSIX準拠ではない。fishのcdは-Lと-Pをサポートしていない。-はサポートしているが、どうも組み込みコマンドをラップした関数で実装されているらしい。
現在、fishは論理パスを扱うが、かつては物理パスを扱っていたらしい。
cdに-Pがないのは不便だと思うのだが、一体なぜだろう。
fishのcdはワーキングディレクトリーの変更履歴を前後に25件記録していて、prevd, nextdで移動できる。
~> mkdir -p a/b
~> cd a
~/a> cd b
~/a/b> prevd
~/a> prevd
~> nextd
~/a>
cdhは変更履歴のなかでタブ補完をしてくれる。
cdはとてもつまらない機能だと思っていたが、ワーキングディレクトリーの変更はよく行うので、シェルは独自の拡張を追加している。
bashは保守的で無難、zshはフック関数が便利、fishは華やかな履歴機能はあるが、シェルと基本的なことができていない。
https://pubs.opengroup.org/onlinepubs/9699919799/utilities/cd.html
https://www.gnu.org/software/bash/manual/bash.html#index-cd
https://zsh.sourceforge.io/Doc/
https://fishshell.com/docs/current/index.html#navigating-directories