lsっぽいコマンドを作る

作成:

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

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

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

タイプ識別子の表示

素の「ls」はディレクトリ内のエントリ名をリストアップするだけで そのエントリがファイルなのかディレクトリなのかは区別できない。 オプションを指定することで追加情報を得ることができるのだが、 そのオプションの一つが「-F」だ。

MANで「ls」を調べると、 「-F」オプションについては以下のように説明されている。

-F, --classify
タイプ識別子 (*/=>@| のうちの一つ) を付けて出力する

「ls」に「-F」オプションを付けて実行すると以下のように表示されるだろう。

$ ls ~/lstest/
a.out  dir  fifo  file  link  socket
$ ls -F ~/lstest/
a.out*  dir/  fifo|  file  link@  socket=

見慣れないものもあると思うが、ディレクトリの末尾についている「/」や、 実行可能ファイルの末尾についている「*」を見たことはないだろうか。 このようにファイル名の末尾に、そのファイルの種別を示すタイプ識別子が表示される。 今回対応するのはこれである。

それぞれの記号の意味は以下のようになっている

*
実行可能ファイル
/
ディレクトリ
=
UNIXドメインソケット
@
シンボリックリンク
|
名前付きパイプ

*/=>@| のうち「>」は、Solaris の door ファイルを意味しており、 Linuxには存在しないファイルタイプだ。 ここでのサンプルソースでは処理自体記述していないし、説明もしない。

ファイル情報の取得

ファイルの種別だけなら、ディレクトリエントリの取得 で紹介した、struct direntd_type を使用することもできる。 Linux に限定すれば d_type が使用できない環境というのは考えにくいが、 今後ファイルタイプ以外の情報も表示できるようにしていくことを考え、この時点から以下の関数を使用する。

#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);

指定したパス名のファイル情報を取得する関数だ。 パスは絶対パスでもプログラムのカレントディレクトリからの相対パスでも良い。 取得した情報は struct stat 構造体に格納される。 この情報がいわゆる inode の情報である。

statlstat の違いは、 stat は対象ファイルがシンボリックリンクの場合に、参照先の情報を取得するのに対して、 lstat は、そのシンボリックリンク自体の情報を取得するという点である。 今回は、シンボリックリンクの場合は、シンボリックリンクとしての情報が必要なので、 lstat を使用する。

今回必要となるファイル種別の情報は st_mode に格納されている。 このフィールドはビットオアで複数の情報を保持しており、 以下のように定義されている。S_IFMT がファイル種別を表す部分のビットマスクになっている。 ここでの定義は 8 進数である点に注意。

#define S_IFMT  00170000
#define S_IFSOCK 0140000 /* UNIXドメインソケット */
#define S_IFLNK  0120000 /* シンボリックリンク */
#define S_IFREG  0100000 /* 通常ファイル */
#define S_IFBLK  0060000 /* ブロックデバイス */
#define S_IFDIR  0040000 /* ディレクトリ */
#define S_IFCHR  0020000 /* キャラクターデバイス */
#define S_IFIFO  0010000 /* 名前付きパイプ */

例えばシンボリックリンクであることを判定するには (mode & S_IFMT) == S_IFLNKといった判定を行えば良い。 しかし、このような判定は頻繁に利用されるため、予め以下の様なマクロが用意されている。 通常これを利用する。

#define S_ISLNK(m)      (((m) & S_IFMT) == S_IFLNK)  /* シンボリックリンク */
#define S_ISREG(m)      (((m) & S_IFMT) == S_IFREG)  /* 通常ファイル */
#define S_ISDIR(m)      (((m) & S_IFMT) == S_IFDIR)  /* ディレクトリ */
#define S_ISCHR(m)      (((m) & S_IFMT) == S_IFCHR)  /* キャラクターデバイス */
#define S_ISBLK(m)      (((m) & S_IFMT) == S_IFBLK)  /* ブロックデバイス */
#define S_ISFIFO(m)     (((m) & S_IFMT) == S_IFIFO)  /* 名前付きパイプ */
#define S_ISSOCK(m)     (((m) & S_IFMT) == S_IFSOCK) /* UNIXドメインソケット */

また、 st_mode にはパーミッション情報も格納されており、 実行可能ファイルの判定にはこちらを利用する。 パーミッションは以下のように定義されている。

#define S_IRWXU 00700 /* 所有者に全権限あり */
#define S_IRUSR 00400 /* 所有者に読み込み権限あり */
#define S_IWUSR 00200 /* 所有者に書き込み権限あり */
#define S_IXUSR 00100 /* 所有者に実行権限あり */

#define S_IRWXG 00070 /* グループに全権限あり */
#define S_IRGRP 00040 /* グループに読み込み権限あり */
#define S_IWGRP 00020 /* グループに書き込み権限あり */
#define S_IXGRP 00010 /* グループに実行権限あり */

#define S_IRWXO 00007 /* その他に全権限あり */
#define S_IROTH 00004 /* その他に読み込み権限あり */
#define S_IWOTH 00002 /* その他に書き込み権限あり */
#define S_IXOTH 00001 /* その他に実行権限あり */

chmod コマンドを利用したことがあればわかると思うが、 chmod 644 などと指定する値そのままである。

実行可能ファイルとして扱うのは、User / Group / Other いずれかで実行可能の場合である。 その場合、それぞれのORをとった値でマスクして、いずれかのビットが立っていれば良いので、 以下のようにORをとった値を予め用意しておく。 なお、実行可能ビットから実行可能ファイルとして判断するのは通常ファイルの場合のみである。

#ifndef S_IXUGO
#define S_IXUGO (S_IXUSR | S_IXGRP | S_IXOTH)
#endif

以上を踏まえて、タイプ識別子を出力する関数を作成する。

static void print_type_indicator(mode_t mode) {
  if (S_ISREG(mode)) {
    if (mode & S_IXUGO) {
      putchar('*');
    }
  } else {
    if (S_ISDIR(mode)) {
      putchar('/');
    } else if (S_ISLNK(mode)) {
      putchar('@');
    } else if (S_ISFIFO(mode)) {
      putchar('|');
    } else if (S_ISSOCK(mode)) {
      putchar('=');
    }
  }
}

今後の拡張などを考えると、直接出力するのではなく、 どの文字を出力するかの判定関数として作成し、出力は別に行うほうが筋が良いだろう。 そのような拡張が必要になった際はリファクタリングすることとしよう。

リスト表示関数の修正

前回までに作成した list_dir 関数に、 タイプ識別子を表示する処理を追加したのが以下になる。

#define PATH_MAX 4096

static void list_dir(const char *base_path) {
  DIR *dir;
  struct dirent *dent;
  char path[PATH_MAX + 1];
  size_t path_len;
  dir = opendir(base_path);
  if (dir == NULL) {
    perror(base_path);
    return;
  }
  path_len = strlen(base_path);
  if (path_len >= PATH_MAX - 1) {
    fprintf(stderr, "too long path\n");
    return;
  }
  strncpy(path, base_path, PATH_MAX);
  if (path[path_len - 1] != '/') {
    path[path_len] = '/';
    path_len++;
    path[path_len] = '\0';
  }
  while ((dent = readdir(dir)) != NULL) {
    struct stat dent_stat;
    const char *name = dent->d_name;
    if (filter != FILTER_ALL
        && name[0] == '.'
        && (filter == FILTER_DEFAULT
            || name[1 + (name[1] == '.')] == '\0')) {
      continue;
    }
    strncpy(&path[path_len], dent->d_name, PATH_MAX - path_len);
    if (lstat(path, &dent_stat) != 0) {
      perror(path);
      continue;
    }
    printf("%s", name);
    if (classify) {
      print_type_indicator(dent_stat.st_mode);
    }
    putchar('\n');
  }
  closedir(dir);
}

ポイントとしては、lstat に渡すためのパスの作成だ。 list_dir に渡されたディレクトリパスにファイル名を結合させれば良いのだが、 ディレクトリパス部分は共通なので、各ファイルごとにこの部分をコピーしたり、 毎回 strcat を使用するのは無駄である。 予めワーク領域にコピーしておき、各ファイル名部分をその後ろにコピーすることで作成している。

ファイル名の出力の際、改行の出力を分離し、 間にオプションが指定されていたら、タイプ識別子を表示するようにしている。 オプションが指定されなかった場合、lstat の呼び出しはムダになるのだが、 lstat の値は今後様々に利用してくため、ここでは毎回呼び出すようにしている。 最終的には最適化を行いたい部分である。

実行結果

各タイプのファイルが存在するディレクトリを指定し、表示させると以下のように表示されるだろう。 まだまだ物足りないと思うが、今後、少しずつ機能を追加していく。

$ ./ls4 -F ~/lstest/
socket=
fifo|
file
ln@
dir/
a.out*

各ファイルの作成

今回ファイルの種別を示すタイプ識別子を表示させたが、 そもそもそれらの種別のファイルを作成するにはどうすればよいかを簡単に紹介しておく。 UNIX ドメインソケット以外は一般的なコマンドラインツールで作成可能であるので以下のように実行すれば良い。

$ touch file      ←ファイルの作成
$ mkdir dir       ←ディレクトリの作成
$ mkfifo fifo     ←名前付きパイプの作成
$ ln -s file link ←fileへのシンボリックリンクlinkを作成
$ chmod +x a.out  ←a.outを実行可能に

UNIX ドメインソケットについては、単に作成するだけの一般的なコマンドはなさそうである。 (筆者が知らないだけの可能性もある) 作成だけをしても意味のあるものではないのだが、 ひとまず今回の確認のためだけにカレントディレクトリに socket という名前の UNIX ドメインソケットを作成するだけのコード例を以下に示す。

#include <string.h>
#include <sys/un.h>
#include <sys/socket.h>
#include <unistd.h>

int main(void) {
  int sock = socket(AF_UNIX, SOCK_STREAM, 0);
  struct sockaddr_un sa;
  sa.sun_family = AF_UNIX;
  strcpy(sa.sun_path, "socket");
  bind(sock, (struct sockaddr*)&sa, sizeof(struct sockaddr_un));
  close(sock);
  return 0;
}