lsっぽいコマンドを作る

作成:

UNIX 環境のコマンドラインを触ったことがある人ならどんな人でも使ったことがあるコマンドの一つ、 lsっぽいコマンドラインプログラムを作りながら、そのために必要な要素について解説していく。 あくまで「っぽい」であり、本物と全く同じものを作るわけではないので注意。

説明に使用するプログラムコードについては GitHub で公開している。 ソースコードの全文をよく見たい、ダウンロードしたいなどの場合はこちらを参照してほしい。

今回は ls6.c を利用した説明になる。

ロングフォーマットの表示

前回に引き続きロングフォーマット出力に対応させていく。 前回はモード文字列の表示ができたので、 今回は残りの要素、ハードリンク数、uid/gid、サイズ、タイムスタンプの表示ができるようにする。 以下の赤字の部分に相当する情報だ。

-rwxrwxr-x  1 ryosuke ryosuke 8672 12月 15 20:32 a.out

ユーザ名やグループ名の名前解決については次回にまわし、今回はそれぞれを指し示す uid / gid までの表示とする。 日付についても ls の場合少し特殊な表示のさせ方をしているのだが、 それについても次回にまわし、今回は通常の日付表示とする。

情報の取得

必要な情報の取得方法についてはすでに説明済みである。

#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
struct stat {
    dev_t st_dev;             /* ファイルがあるデバイスのID  */
    ino_t st_ino;             /* inode番号 */
    nlink_t st_nlink;         /* ハードリンクの数 */
    mode_t st_mode;           /* ファイルのモード  */
    uid_t st_uid;             /* ファイル所有者のユーザID */
    gid_t st_gid;             /* ファイルのグループのグループID*/
    dev_t st_rdev;            /* デバイス番号(デバイスファイルの場合) */
    off_t st_size;            /* ファイルサイズ(バイト単位) */
    blksize_t st_blksize;     /* I/Oにおけるブロックサイズ  */
    blkcnt_t st_blocks;       /* 割り当てられた512Bブロックの数 */
    struct timespec st_atim;  /* 最終アクセス時刻 */
    struct timespec st_mtim;  /* 最終変更時刻 */
    struct timespec st_ctim;  /* 最終状態変更時刻 */
};
int stat(const char *pathname, struct stat *buf);
int lstat(const char *pathname, struct stat *buf);

stat で取得可能なのでこれを順次表示するだけで良い。

情報の表示

前回追加した以下の処理を

if (long_format) {
  char mode_str[11];
  get_mode_string(dent_stat.st_mode, mode_str);
  printf("%s ", mode_str);
}

以下のように変更する。

if (long_format) {
  char buf[20];
  get_mode_string(dent_stat.st_mode, buf);
  printf("%s ", buf);
  printf("%3d ", (int)dent_stat.st_nlink);
  printf("%4d %4d ", dent_stat.st_uid, dent_stat.st_gid);
  if (S_ISCHR(dent_stat.st_mode) || S_ISBLK(dent_stat.st_mode)) {
    printf("%4d,%4d ", major(dent_stat.st_rdev), minor(dent_stat.st_rdev));
  } else {
    printf("%9ld ", dent_stat.st_size);
  }
  strftime(buf, sizeof(buf), "%F %T", localtime(&dent_stat.st_mtim.tv_sec));
  printf("%s ", buf);
}

ロングフォーマットの特性上、各項目の縦位置を揃えて表示させたいところだ。 本物の ls は、一旦全項目について各カラムの最大桁を調べてから出力するようになっており、 かなり特殊な値を持つものがあっても縦位置が揃うように作られている。 そのような工夫を取り入れるのも良いが、 今回は簡易的に概ねこのぐらいの桁数があれば良いという決め打ちでの出力としている。

順に説明していこう。

printf("%3d ", (int)dent_stat.st_nlink);

ハードリンク数についてはそのまま10進数で出力する。 通常のファイルは別途ハードリンクを持っていないかぎり 1 だが、 ディレクトリについては親ディレクトリからの参照に加えて、 そのディレクトリの中からは、.で参照できるため、最低でも 2 となる。 さらにサブディレクトリをもつ場合は、そのサブディレクトリから..で参照できるため、 サブディレクトリの数だけハードリンクは増える。 そのため、比較的多くのサブディレクトリを持つものを想定し 3 桁とした。

printf("%4d %4d ", dent_stat.st_uid, dent_stat.st_gid);

次に、 uid gid の順で表示する。 これらについて 4 桁程度までの数値で表現することが多いので 4 桁とした。 本物の ls では -n オプションを指定すると、 これと同じように名前解決前の数値を表示させることができる。

if (S_ISCHR(dent_stat.st_mode) || S_ISBLK(dent_stat.st_mode)) {
  printf("%4d,%4d ", major(dent_stat.st_rdev), minor(dent_stat.st_rdev));
} else {
  printf("%9ld ", dent_stat.st_size);
}

次はサイズだが、表示しようとしているファイルがデバイスファイルの場合は、 サイズではなく、そのデバイスのシステムにおける識別番号であるメジャー番号と、マイナー番号を表示する。 サイズを表示してはいけないわけではないが、デバイスファイルのサイズ情報は 0 である。 逆にデバイスファイル以外ではメジャー番号やマイナー番号を取り出しても 0 となっている。

struct stat のフィールドとしては dev_t 型の一つの値だが、 ここからメジャー番号とマイナー番号を取り出す関数が用意されている。

#include <sys/types.h>
unsigned int major(dev_t dev);
unsigned int minor(dev_t dev);

メジャー番号とマイナー番号はそれぞれ 4 桁ずつ、サイズは 9 桁として表示させる。

strftime(buf, sizeof(buf), "%F %T", localtime(&dent_stat.st_mtim.tv_sec));
printf("%s ", buf);

最後は時刻情報、時刻情報は 3 種類あるが、最終変更時刻を表示する。 struct timespec 型であり、ナノ秒単位までを表現できるが、 通常欲しいのは日付レベルの情報なので、秒単位の値のみを使用する。 これはエポック秒で表現される値なので 時間情報の取得 time() で紹介しているように strftime を使って時刻表記に変換している。 "%F %T" というフォーマット指定をすると、 YYYY/MM/DD HH:mm:ssという形式になる。


以上の対応を行うことで以下のような表示ができるようになる。

$ ./ls6 -l ~/lstest/
srwxrwxr-x   1 1000 1000         0 2015-12-15 20:11:03 socket
prw-rw-r--   1 1000 1000         0 2015-12-15 20:01:34 fifo
-rw-rw-r--   1 1000 1000         0 2015-12-15 19:57:30 file
drwxrwxr-x   2 1000 1000      4096 2016-01-09 19:14:18 dir
-rwxrwxr-x   1 1000 1000      8672 2015-12-15 20:32:48 a.out
lrwxrwxrwx   1 1000 1000         4 2015-12-27 21:37:12 link

「それっぽく」なってきたのではないだろうか。