2009年3月23日

Yahoo!ショッピング

lsコマンドをハックしてみよう

  • このエントリーをはてなブックマークに追加

こんにちは、ショッピング事業部開発部の吉野です。

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コマンド編)はこちら

Yahoo! JAPANでは情報技術を駆使して人々や社会の課題を一緒に解決していける方を募集しています。詳しくは採用情報をご覧ください。

  • このエントリーをはてなブックマークに追加