lsっぽいコマンドを作る

作成:

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

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

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

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

さて、前回まででおおよそロングフォーマット表示される要素は揃ってきた。 不足しているものとして、ユーザ名とグループ名の表示だ。 また、日付の表示も少し異なっている。

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

-rwxrwxr-x   1 1000 1000      8672 2015-12-15 20:32:48 a.out

今回はここを対応していくこととする。

ユーザ名・グループ名の名前解決

uid / gid はわかっているので、その uid / gid に該当する名前を調べる必要がある。 uid からそのユーザの情報を引き出す関数として以下がある。 詳細は MAN を参照

#include <sys/types.h>
#include <pwd.h>
struct passwd *getpwuid(uid_t uid);
struct passwd {
    char   *pw_name;       /* ユーザ名 */
    char   *pw_passwd;     /* ユーザのパスワード */
    uid_t   pw_uid;        /* ユーザ ID */
    gid_t   pw_gid;        /* グループ ID */
    char   *pw_gecos;      /* ユーザ情報 */
    char   *pw_dir;        /* ホームディレクトリ */
    char   *pw_shell;      /* シェルプログラム */
};

この関数を利用し、ユーザ名を調べることができる。 ここにグループIDも含まれているが、これはあくまでこのユーザの所属グループであって、 ファイルのグループIDとは関係ないので気をつけよう。

この関数を利用して、指定した uid のユーザ名を表示する以下の関数を作る。 該当するUIDのユーザが存在しない可能性もあるので、その場合はuidの数値を表示するようにする。 名前については必要な桁数をどれだけ取れば十分かというのは難しいが、 あまり大きく摂り過ぎても出力が不格好になるので、 デフォルトで用意されているユーザ名が概ね収まる長さということで 8 桁にしている。

static void print_user(uid_t uid) {
  struct passwd *passwd = getpwuid(uid);
  if (passwd != NULL) {
    printf("%8s ", passwd->pw_name);
  } else {
    printf("%8d ", uid);
  }
}

続いて同様に gid からそのグループの情報を引き出す関数として以下がある。 詳細は MAN を参照

#include <sys/types.h>
#include <pwd.h>
struct group *getgrgid(gid_t gid);
struct group {
    char   *gr_name;       /* グループ名 */
    char   *gr_passwd;     /* グループのパスワード */
    gid_t   gr_gid;        /* グループ ID */
    char  **gr_mem;        /* グループのメンバ */
};

この関数を利用し、ユーザ名と同様に gid から該当するグループ名を表示する関数を作る。

static void print_group(gid_t gid) {
  struct group *group = getgrgid(gid);
  if (group != NULL) {
    printf("%8s ", group->gr_name);
  } else {
    printf("%8d ", gid);
  }
}

これらを前回単純に uid / gid をprintfで表示していたところで実行するようにすれば対応完了である。

日時のフォーマット

表示する日時情報は前回までの 2015-12-15 20:32:48 と十分な情報量の出力となっているが、 本物の ls では少し変わった出力の仕方をしている。

-rw-rw-r-- 1 ryosuke ryosuke    0  3月 11 13:00 a
-rw-rw-r-- 1 ryosuke ryosuke    0  3月 11  2015 b

と、あるファイルは、月日と時刻。 またあるファイルは、月日と年。 となっている。 これは日付が現在よりも半年以上前かどうかで振り分けが行われている。

どちらが良い形式かは意見が分かれそうだが、これの真似事をしてみよう。 まずは、半年前のUNIX時間を以下のように求めておく。

#define HALF_YEAR_SECOND (365 * 24 * 60 * 60 / 2)
static time_t half_year_ago;
...
half_year_ago = time(NULL) - HALF_YEAR_SECOND;
...

これだとうるう年を考慮していないから正確に半年前じゃない。 というツッコミはスルーさせてもらうことにする。 そもそもここでの「半年前」は厳密性が要求されるものではないので問題ない。

ファイルのタイムスタンプと、この半年前の時刻を比較し、前か後ろかでフォーマットを切り換える関数を用意する。

static void get_time_string(char *str, time_t time) {
  if (time - half_year_ago > 0) {
    strftime(str, 12, "%m/%d %H:%M", localtime(&time));
  } else {
    strftime(str, 12, "%m/%d  %Y", localtime(&time));
  }
}

前回の処理で日付のフォーマットを行っていた場所をこの処理に置き換えれば対応完了である。

日時に絡む計算では、UNIX時間、つまり秒単位で計算している範囲では問題は起きにくいのだが、 日付そのものを計算する場合、様々な注意が必要となることを覚えておこう。

例えば、翌年の日付を求めるために年の値をインクリメントするような処理を安直に書いてしまうと、 2016年2月29日(うるう年)の翌年は2017年2月29日という存在しない日付が得られてしまうことになる。 しかも、この処理はうるう年が来るまでは正常に動作するので、うるう年が来るまで判明しないバグになってしまう。


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

$ ./ls7 -l ../lstest
srwxrwxr-x   1  ryosuke  ryosuke         0 12/15 20:11 socket
prw-rw-r--   1  ryosuke  ryosuke         0 12/15 20:01 fifo
-rw-rw-r--   1  ryosuke  ryosuke         0 12/15 19:57 file
drwxrwxr-x   2  ryosuke  ryosuke      4096 01/09 19:14 dir
-rwxrwxr-x   1  ryosuke  ryosuke      8672 12/15 20:32 a.out
lrwxrwxrwx   1  ryosuke  ryosuke         4 12/27 21:37 link

本当の ls と遜色のない表示になってきた。