2012.01.01
■[シェルスクリプト] シェルスクリプト内で実行した cd コマンドをターミナルに反映させる方法を教わった
シェルスクリプト内で実行した cd コマンドをターミナルに反映させたい。シェルスクリプト内でふつうに cd しても、子プロセスで cd するだけ。exec cd すればいいだろうと思ったのですがうまくゆかず、はてな人力検索で質問しました。そこで、a-kuma3 さんという方に、最後に exec /bin/bash すればよい という、目から鱗なアイディアを教わったので、自慢しておきます。
検証/理解の確認として、ターミナルで echo $$ し、次のスクリプト dummy.sh を実行し、再度ターミナルで echo $$ してみました。いうなれば、ターミナルで直接向き合っているカレントプロセスをシェルスクリプトのプロセスに差し替える技。なるほど。感動しました。
#!/bin/bash cd /tmp echo $$ exec /bin/bash
mori@x200:~ $ echo $$ 12573 mori@x200:~ $ dummy.sh 12756 mori@x200:/tmp $ echo $$ 12756
追記
この件で、node.js のバージョンを変更するコンソールアプリ nave を思い出しました。nave.sh というシェルスクリプトが切り替え処理を行っています。そこでは exec は付けていないため、コマンド実行前ともシェルスクリプトとも PID は変わりますが、スクリプト内でシェルを起動しています。ネットでの反応は否定的ととれるものばかりで、自分もそう感じていたと記憶しています。個人用途以外で実際に使うとなると、いろいろ検討が必要みたいですね。
かんたんに紹介します。nave での node.js のバージョン変更は、nave use 0.6.0 などと行います。サブコマンド(use など)は次のように解析し、nave use なら、内部的には nave_use() という関数として扱います。
local cmd="$1" shift case $cmd in ls-remote | ls-all) cmd="nave_${cmd/-/_}" ;; install | fetch | use | clean | test | named | \ ls | uninstall | usemain | latest | stable | has | installed ) cmd="nave_$cmd" ;; * ) cmd="nave_help" ;; esac $cmd "$@" && exit 0 || fail "failed somehow"
こちらが本題。nave_use() では "$SHELL" でシェルを起動しています。
nave_use () { ... echo "using $version" if [ $# -gt 1 ]; then shift hash -r PATH="$bin:$PATH" NAVELVL=$lvl NAVE="$version" \ ... NODE_PATH="$lib" \ "$SHELL" -c "$(enquote_all node "$@")" hash -r else hash -r PATH="$bin:$PATH" NAVELVL=$lvl NAVE="$version" \ ... NODE_PATH="$lib" \ "$SHELL" hash -r fi return $? }
追記 2012/01/23
Omicron bash/builtin から引用。なぜ cd がシェルの組み込みコマンドとして実装されているのかについて書いてありました。
組込みコマンド(builtin)は,通常の(外部)コマンドと違って,プロセスを fork & exec せずに, シェルのプロセス内部で実行される.なので,速いという利点もあるが,もし cd や export (csh なら setenv),ulimit などが外部コマンドとして実装されていたら,プロセスが終了した時 点で,変更の影響が消えてしまう(例えば,cd でカレントディレクトリを移動しても,cd が終了 して,シェルに戻ったら元の木阿弥).つまり,これらのコマンドは組込みコマンドとして実装す る必要がある.本質的にはこちらの方が重要である.
