今回のテーマはごく短いシェルスクリプトだ。「このスクリプトにどんな意味があるのだろう」とか、「役に立つのだろうか」と思う方もいるかもしれない。あなたが作成するシェルスクリプトに直接役立つことはないが、今回のテーマは、今後シェルスクリプトのスキルを上げていきたいと願う人なら、絶対に押さえておきたいものだ。(編集部)
今回は、最初に短いシェルスクリプトを紹介する。このごく短いシェルスクリプトが今回のテーマだ。わけが分からないという人もいるかもしれないが、この部分をしっかり押さえた人と、わけが分からないからと放置した人とでは、後々とてつもなく大きな差が付く。
今回のテーマは、シェルスクリプトマスターになるためには、ぜひとも抑えておきたいポイントなのだ。最初は理解できないところが多いかもしれないが、後々必ず役に立つと約束する。しっかり読み進めていただきたい。
Vim連載の第8回目で作業環境のセットアップについて紹介したが、この話を参考に仮想環境などをセットアップしてあるなら実際に試してみてほしい。Mac OS XやSolarisなどでも同じようなシェルスクリプトを確認できる。UbuntuやFedora、CentOS、openSUSEなどにはこれから紹介するようなスクリプトは存在しないので、何か別の環境で試してほしい。
シェルを使っていればcdコマンドを使わないということはあり得ないと思う。このcdコマンド、FreeBSDやSolaris、Mac OS Xでは組み込みコマンドだけでなく、通常のコマンドとして独立したものも存在する。/usr/bin/cdがそれだ。PC-BSD 9.0には次のようなスクリプトが存在する。
#!/bin/sh # $FreeBSD$ # This file is in the public domain. builtin ${0##*/} ${1+"$@"}
例えば「/usr/bin/cd /tmp」のように実行すると、何事もなかったのように処理が終わる。/usr/bin/cdを実行しても、/usr/bin/cdを実行するために使用したシェルに何か影響があるというわけではない。これは当然の動作だ。なぜこのようなスクリプトが存在しているのか不思議に思うだろう。読みやすくするためにコメントを削除すると次のようになる。
#!/bin/sh builtin ${0##*/} ${1+"$@"}
最初のbuiltinは、特定の環境で発生する問題を避けるためにあとから追加されたものなので(Revision 151635)、実際に機能を実現しているのは次のコードということになる。
#!/bin/sh ${0##*/} ${1+"$@"}
「${0##*/}」は説明しなくても理解できる方が多いかと思う。${0}でコマンド名が渡される。「/usr/bin/cd /tmp」なら「/usr/bin/cd」が入っている。「##*/」で、先頭から始まり、「*/」に最長一致する部分を削除する。その結果、「${0##*/}」では「/usr/bin/cd」が「cd」になる。パスを削除してファイル名やディレクトリ名だけを取り出す方法としてよく使うテクニックだ。
「${1+"$@"}」の部分では頭をひねる方が多いと思う。こう書かなくとも「"$@"」と書けば同じことじゃないかという指摘を受けそうだ。というのも、「${1+"$@"}」で「+」を記述するのは、値がセットされていないかまたはnullの場合にはnullを、そうでない場合には+の後の値を使うようにするという意味がある。つまり「${1+"$@"}」の指定は、引数が何もなければnullであり、そうでなければ「"$@"」となる。そういうことで「"$@"」と同じではないか、というわけだ。
実は過去に、これとは違う動作を見せる実装系を持つ環境があった。そうした環境では引数に何も指定しないと、「"$@"」が「""」を意味していた。引数は存在しないのに、引数は1つあるということになるわけだ。「${1+"$@"}」と記述しておけば、そういった場合も引数は0になる。「${1+"$@"}」は、引数がない場合に「"$@"」が「""」になってしまう環境でもnullとして扱われるように対策を施した結果であるというわけだ。
FreeBSDのashでは「${@+"$@"}」としても同じように動作する。「${1+"$@"}」や「${@+"$@"}」と記述しているスクリプトを時々見かけるのは、こうした理由からである。
つまり/usr/bin/cdは、シェルを起動してその中で組み込みコマンドのcdを実行する、という処理になっている。「/usr/bin/cd /tmp」は、起動されたシェルで「cd /tmp」を実行するという処理になる。考えてみると奇妙な話だ。処理としては成立するが、意味がないように見える。
#!/bin/sh ${0##*/} ${1+"$@"}
実行される組み込みコマンドは「${0##*/}」によって決まっているため、仮にコマンド名がcdではなくaliasであったのなら、このスクリプトは組み込みコマンドであるaliasを実行するものになる。このシェルスクリプトはつまり、コマンドとして実行された処理を、シェルを起動して同じ名前の組み込みコマンドを実行する、という処理をしていることになる。汎用性を持たせながら、こうした意味がない処理をするとはなんとも解せない話だ。
なお、先ほどのスクリプトは、実際には次のように先頭にbuiltinという組み込みコマンドが入っている。これは大文字と小文字を区別しないファイルシステム上で動作することを考慮して、後から追加されたものだ(Revision 151635)。
#!/bin/sh builtin ${0##*/} ${1+"$@"}
Mac OS Xのように大文字と小文字を区別しないファイルシステムでは、最初のシンプルなスクリプトでは処理がうまくいかないことがある。builtin組み込みコマンドを指定して明示的に処理しているのはそのためだ。
スクリプトの内容から推測できるように、/usr/bin/には/usr/bin/cdと同じコマンドがほかにも存在する。意味がないコマンドを用意することを不可解に思う方は少なくないはずだ。例えば、PC-BSD 9.0で次のように/usr/bin/cdを調べると、まったく同じファイルがcdを含めて15個も存在することを確認できる。
% pwd /usr/bin % ls -il cd 1468262 -r-xr-xr-x 15 root wheel 86 Dec 6 07:50 cd % ls -il | grep 1468262 1468262 -r-xr-xr-x 15 root wheel 86 Dec 6 07:50 alias 1468262 -r-xr-xr-x 15 root wheel 86 Dec 6 07:50 bg 1468262 -r-xr-xr-x 15 root wheel 86 Dec 6 07:50 cd 1468262 -r-xr-xr-x 15 root wheel 86 Dec 6 07:50 command 1468262 -r-xr-xr-x 15 root wheel 86 Dec 6 07:50 fc 1468262 -r-xr-xr-x 15 root wheel 86 Dec 6 07:50 fg 1468262 -r-xr-xr-x 15 root wheel 86 Dec 6 07:50 getopts 1468262 -r-xr-xr-x 15 root wheel 86 Dec 6 07:50 hash 1468262 -r-xr-xr-x 15 root wheel 86 Dec 6 07:50 jobs 1468262 -r-xr-xr-x 15 root wheel 86 Dec 6 07:50 read 1468262 -r-xr-xr-x 15 root wheel 86 Dec 6 07:50 type 1468262 -r-xr-xr-x 15 root wheel 86 Dec 6 07:50 ulimit 1468262 -r-xr-xr-x 15 root wheel 86 Dec 6 07:50 umask 1468262 -r-xr-xr-x 15 root wheel 86 Dec 6 07:50 unalias 1468262 -r-xr-xr-x 15 root wheel 86 Dec 6 07:50 wait %
ls(1)コマンドのiオプションはi-node番号を表示させるものだ。i-node番号が同じということは、実体が同じであるということだ。ハードリンクで作成したファイルということになる。実際、これらファイルは単一のファイルに対するリンクである。PC-BSD/FreeBSDなら/usr/src/usr.bin/alias/Makefileにその処理を見ることができる。
# $FreeBSD: releng/9.0/usr.bin/alias/Makefile 207196 2010-04-25 17:38:53Z jilles $ SCRIPTS=generic.sh SCRIPTSNAME=alias NO_OBJ= LINKS= ${BINDIR}/alias ${BINDIR}/bg \ ${BINDIR}/alias ${BINDIR}/cd \ ${BINDIR}/alias ${BINDIR}/command \ ${BINDIR}/alias ${BINDIR}/fc \ ${BINDIR}/alias ${BINDIR}/fg \ ${BINDIR}/alias ${BINDIR}/getopts \ ${BINDIR}/alias ${BINDIR}/hash \ ${BINDIR}/alias ${BINDIR}/jobs \ ${BINDIR}/alias ${BINDIR}/read \ ${BINDIR}/alias ${BINDIR}/type \ ${BINDIR}/alias ${BINDIR}/ulimit \ ${BINDIR}/alias ${BINDIR}/umask \ ${BINDIR}/alias ${BINDIR}/unalias \ ${BINDIR}/alias ${BINDIR}/wait .include
そしてこれはPC-BSD/FreeBSDに限ったことではなく、Mac OS XやSolarisにも存在している。
Copyright © ITmedia, Inc. All Rights Reserved.