こんにちは、ショッピング事業部開発部の吉野です。
Yahoo!ショッピング開発部では新人エンジニア向けにコマンドのソースコードを読むことを奨励しています。
その初期の題材として、lsコマンドがよく挙げられます。
今回は「lsコマンドをハックしてみよう」と題し、lsコマンドについてお話しさせていただきます。
突然ですがエンジニアの皆さん、lsコマンドのソースコードを読んだことはありますか?
読んだことのない方はぜひ一度、目を通しておくことをおすすめします。
意外と知られていませんが、lsはcd,pwdなどのコマンドと違いシェルの組み込みコマンドではありません。
一口にlsと言っても、複数のソースコードが存在します。
代表的なのはGNU版とBSD版です。
一般的に、GNU版はソースコードが読みづらく、BSD版は読みやすい という認識を持たれているようです。
単純にオプションを比べても、GNU版と BSD版は結構違います。
今回は、
Yahoo!ショッピングのほとんどのサーバーはFreeBSDで動いている
BSD版の方が比較的読みやすいと言われている
ということでBSD版をハックしてみたいと思います。
■ソース入手〜コンパイル
※コンパイル環境はFreeBSD6.2です。
まずは、ソースコードを入手しましょう。
http://www.freebsd.org/cgi/cvsweb.cgi/src/bin/ls/
ソースコードを入手したら、適当なディレクトリにまとめます。
$ ls -1
Makefile
cmp.c
extern.h
ls.1
ls.c
ls.h
print.c
util.c
makeします
$ make
cc -O2 -fno-strict-aliasing -pipe -DCOLORLS -c cmp.c
cc -O2 -fno-strict-aliasing -pipe -DCOLORLS -c ls.c
cc -O2 -fno-strict-aliasing -pipe -DCOLORLS -c print.c
cc -O2 -fno-strict-aliasing -pipe -DCOLORLS -c util.c
cc -O2 -fno-strict-aliasing -pipe -DCOLORLS -o ls cmp.o ls.o print.o util.o -lutil -ltermcap
gzip -cn ls.1 > ls.1.gz
「ls」という実行ファイルができていますので、実行してみましょう
$ ./ls -1
Makefile
cmp.c
cmp.o
extern.h
ls*
ls.1
ls.1.gz
ls.c
ls.h
ls.o
print.c
print.o
util.c
util.o
(当たり前ですが)いつも使ってるlsと同じ結果が返ってきます
■ちょっとハック
せっかくなので少しいじってみましょう。
お題
「[ls -l]の出力結果を任意のデリミタで表示するオプションを作成する」まずはオプションを追加しましょう。
lsには既存のオプションがこれだけあります。
usage: ls [-ABCFGHILPRSTUWZabcdfghiklmnopqrstuwx1] [-D format]
デリミタを指定するので -d か -D としたいところですが、すでに使用されていますので
ここは -e [指定デリミタ] にします。
オプションを取得している部分はここです。
ls.c main()
182 while ((ch = getopt(argc, argv,
183 "1ABCD:FGHILPRSTUWZabcdfghiklmnopqrstuwx")) != -1) {
ここに e を追加します。引数を取るので後ろにコロン(:)を付けます
参考:getopt
ls.c main()
182 while ((ch = getopt(argc, argv,
183 "1ABCD:FGHILPRSTUWZabcde:fghiklmnopqrstuwx")) != -1) {
引数を格納する変数 f_delim を宣言します。
ls.c main()
136 int f_label; /* show MAC label */
137 char *f_delim;
138 #ifdef COLORLS
ls.h
57 extern int f_type; /* add type character for non-regular files */
58 extern char *f_delim;
59 #ifdef COLORLS
引数を取得するcase文に e を追加します。
334 case 'Z':
335 f_label = 1;
336 break;
337 case 'e':
338 f_delim = optarg;
339 break;
340 default:
-lオプションと併用されないといけないので、-lオプションが指定されていないときは-eオプションを無効にします。
※usageを出力してエラー終了したほうがよいかもしれませんが、今回は無効にしてしまいます
349 if (!f_listdot && getuid() == (uid_t)0 && !f_noautodot)
350 f_listdot = 1;
351
352 if (!f_longform)
353 f_delim = NULL;
これでオプションが取得できるようになりました。
次は出力結果を作成している部分にデリミタを追加してみましょう。
出力を制御しているのは print.c の printlong 関数にあります。
通常の -l オプションの場合、
1.出力する各項目(ファイル名、ファイルサイズなど)の文字列の最大長を取得し
2.項目の出力時、printfに1.で取得した最大長を最小幅として指定
といった感じの処理をしています。
今回は2.の部分に(ちょっと無理やりですが) f_delim のif文を追加し、デリミタを表示させるように修正します。
print.c printlong()
156 if (f_inode)
157 if (f_delim)
158 (void)printf("%lu%s", (u_long)sp->st_ino, f_delim);
159 else
160 (void)printf("%*lu ", dp->s_inode, (u_long)sp->st_ino);
161 if (f_size)
162 if (f_delim)
163 (void)printf("%jd%s", howmany(sp->st_blocks, blocksize), f_delim);
164 else
165 (void)printf("%*jd ",
166 dp->s_block, howmany(sp->st_blocks, blocksize));
177 if (f_delim)
178 (void)printf("%s%s%u%s%-s%s%-s%s", buf, f_delim,
179 sp->st_nlink, f_delim, np->user, f_delim, np->group, f_delim);
180 else
181 (void)printf("%s %*u %-*s %-*s ", buf, dp->s_nlink,
182 sp->st_nlink, dp->s_user, np->user, dp->s_group,
183 np->group);
184 if (f_flags)
185 if (f_delim)
186 (void)printf("%-s%s", np->flags, f_delim);
187 else
188 (void)printf("%-*s ", dp->s_flags, np->flags);
189 if (f_label)
190 if (f_delim)
191 (void)printf("%-s%s", np->label, f_delim);
192 else
193 (void)printf("%-*s ", dp->s_label, np->label);
202 else if (dp->bcfile)
203 if (f_delim)
204 (void)printf("%s%jd%s", f_delim, sp->st_size, f_delim);
205 else
206 (void)printf("%*s%*jd ",
207 8 - dp->s_size, "", dp->s_size, sp->st_size);
printsize()
632 if (f_delim)
633 (void)printf("%s%s", buf, f_delim);
634 else
635 (void)printf("%5s ", buf);
636 } else
637 if (f_delim)
638 (void)printf("%jd%s", bytes, f_delim);
639 else
640 (void)printf("%*jd ", (u_int)width, bytes);
printtime()
413 if (f_delim)
414 fputs(f_delim, stdout);
415 else
416 fputc(' ', stdout);
※usage()の修正は省略します
これで修正完了です。 コンパイルして実行してみましょう。
$ make
cc -O2 -fno-strict-aliasing -pipe -DCOLORLS -c cmp.c
cc -O2 -fno-strict-aliasing -pipe -DCOLORLS -c ls.c
cc -O2 -fno-strict-aliasing -pipe -DCOLORLS -c print.c
cc -O2 -fno-strict-aliasing -pipe -DCOLORLS -c util.c
cc -O2 -fno-strict-aliasing -pipe -DCOLORLS -o ls cmp.o ls.o print.o util.o -lutil -ltermcap
gzip -cn ls.1 > ls.1.gz
$ ./ls -le ","
total 260
-rw-r--r-- ,1,hoge,users,303,Mar 8 21:42,Makefile
-rw-r--r-- ,1,hoge,users,4803,Mar 8 18:07,cmp.c
-rw-r--r-- ,1,hoge,users,12224,Mar 8 22:02,cmp.o
-rw-r--r-- ,1,hoge,users,2852,Mar 8 18:08,extern.h
-rwxr-xr-x ,1,hoge,users,66366,Mar 8 22:02,ls*
-rw-r--r-- ,1,hoge,users,16646,Mar 8 18:08,ls.1
-rw-r--r-- ,1,hoge,users,6258,Mar 8 22:02,ls.1.gz
-rw-r--r-- ,1,hoge,users,22565,Mar 8 18:57,ls.c
-rw-r--r-- ,1,hoge,users,3357,Mar 8 18:42,ls.h
-rw-r--r-- ,1,hoge,users,41648,Mar 8 22:02,ls.o
-rw-r--r-- ,1,hoge,users,16181,Mar 8 22:02,print.c
-rw-r--r-- ,1,hoge,users,40328,Mar 8 22:02,print.o
-rw-r--r-- ,1,hoge,users,5647,Mar 8 18:08,util.c
-rw-r--r-- ,1,hoge,users,19608,Mar 8 22:02,util.o
おお! 指定したデリミタで表示されるようになりましたね!
これを自分のhomeディレクトリ下のbin/に置いて、環境変数PATHの先頭に付け加えれば
PATH=$HOME/bin:/bin
常に $HOME/bin/ls が実行されるようになります。
lsのソースコードを読むと、ファイル情報の取得に
fts,
stat
を使用していることがわかります。
使い方を覚えれば、自分でカスタマイズした情報を出力したりすることもできますね。
■まとめ
いかがでしたか? 案外簡単ですね。(ハックと呼べるほどではなかったかもしれませんが...)
lsはソースコードの量も少ないので、新人エンジニアの勉強にはちょうどいいと思います。
ソースコードをひととおり読んだ後は、項目を順番指定して出力できるようにしてみたり(cutの-fオプションのような感じ)、いろいろ課題を考えて挑戦してみてください。
GNU版との違いを見比べてみるのも面白いと思います。
lsに限らず、ほとんどのUnixコマンドはソースコードが公開されています。
grepのソースコードなど見てみるのもいいかもしれませんね。
今回のお話は以上です。またお会いしましょう。
2009/05/22追記>>ソースコードリーディング(killコマンド編)はこちら
2009/06/17追記>>
ソースコードリーディング(head,tailコマンド編)はこちら
こちらの記事のご感想を聞かせください。
- 学びがある
- わかりやすい
- 新しい視点
ご感想ありがとうございました