lsっぽいコマンドを作る

作成:

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

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

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

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

今回からロングフォーマット出力に対応させていく。 ロングフォーマットと言われてもピンと来ないかもしれないが、以下の様な表示だ。

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

-lオプションをつけることで表示されるもので、 何もオプションを付けない表示よりも利用されているかもしれない。 よく利用されるものなので、 lsを使ったことがある人で知らない人はいないと言っていいだろう。 多くの環境でllというエイリアスでls -lが登録されている。

ロングフォーマットオプションの追加

常にロングフォーマットで出力するのではなく、オプションによって切り換えられるように、 引数のパース処理にロングフォーマット出力のオプションを追加する。 MANの記述によると、

-l, --format=long, --format=verbose
ファイル名に加えて、ファイルタイプ・アクセス権・ハードリンクの数・ 所有者名・グループ名・バイト単位のサイズ・タイムスタンプ (他の時刻が選択されなければ、修正時刻) を表示する。 ファイルの時刻が 6 カ月以上前または 1 時間以上先の場合、 タイムスタンプには時刻のかわりに年が入る。 ...

とあり、ロングフォーマット出力の「短い」オプションは-lでシンプルだが、 「長い」オプションは--formatオプションの引数で指定することになっている。 しかし、ロングフォーマット以外のフォーマットを指定できるようにする予定はないので、 「長い」オプションについては--long-formatで指定できるようにした。

以下のように引数パースに処理を追加し、 グローバル変数の long_format によって処理を切り換える。

static char *parse_cmd_args(int argc, char**argv) {
  char *path = "./";
  int opt;
  const struct option longopts[] = {
...
      { "long-format", no_argument, NULL, 'l' },
  };
  while ((opt = getopt_long(argc, argv, "aAlF", longopts, NULL)) != -1) {
    switch (opt) {
...
      case 'l':
        long_format = true;
        break;
...
}

モード文字列の表示

ロングフォーマットは様々な情報を表示するため、何回かに分けて解説する。 まずは「モード文字列」の表示ができるようにする。 「モード文字列」と表現しているのが以下の赤で示した部分だ。 ファイル種別とパーミッション情報を表現している。

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

モード文字列の意味

モード文字列が何を表現しているかについてまとめておく。

一文字目はファイルの種別を示しており、以下の様な意味になる。 前回のタイプ識別子と違って純粋にファイルの種別だけを表現しているためこちらのほうがシンプルだ。

bブロックデバイス
cキャラクターデバイス
dディレクトリ
-通常のファイル
p名前付きパイプ
lシンボリックリンク
sUNIXドメインソケット
?その他

2文字目から10文字目までの9文字は、 所有者(owner) / グループ(group) / その他(other) の順で それぞれの読み込み(read) / 書き込み(write) / 実行(execute) の権限を表現している。 権限がない場合は - が表示され、ある場合は、権限に応じて r w x が表示される。

通常見かけるのはここまでだと思うが、実行の箇所には追加の情報が表示されることがある。 所有者(owner) / グループ(group) / その他(other) の実行権の場所に表示され、 順に、setuid / setgid / sticky bit だ。 setuid / setgid については s そして、 sticky bit については t で表現される。 本来の実行権限の表現も行う必要があるため、その違いは、それぞれのアルファベットの大文字小文字で表現される。

この特殊なパーミッションはあまり知らない人も多いと思うので大雑把に説明すると。 setuid / setgid が付いていると、実行時にそのファイルの所有者・グループとして実行することになる。 また、setgid がディレクトリに付いていると、その配下に作成されるファイルのグループが上位ディレクトリを継承するようになる。 sticky bit がディレクトリについていると、配下のファイルのファイル名変更や削除が、 そのファイルの所有者、ディレクトリ所有者、スーパーユーザ以外にできなくなる。 特に sticky bit については OS によって意味が異なっていたりするので、必要であれば各自調べて欲しい。

各文字が現れる位置によって意味が違ってくるためわかりにくいと思うが、表にまとめると以下のようになる。

-権限なし
r(2, 5, 8 文字目) 読み込み権限あり
w(3, 6, 9 文字目) 書き込み権限あり
x(4, 7, 10 文字目) 実行権限あり
s(4, 7 文字目) setuid / setgid かつ所有者/グループの実行権限あり
S(4, 7 文字目) setuid / setgid かつ所有者/グループの実行権限なし
t(10 文字目) sticky bit かつその他の実行権限あり
T(10 文字目) sticky bit かつその他の実行権限なし

モード情報の取得

これらの情報は stat lstat で取得する struct stat 構造体の st_mode から取得することができる。

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

前回に引き続き、ファイル種別の判定には以下のビットと、判定マクロを利用する。

#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 /* 名前付きパイプ */

#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ドメインソケット */

パーミッション情報についても同様に以下のビット定義値を利用する。

#define S_ISUID  0004000 /* setuid */
#define S_ISGID  0002000 /* setgid */
#define S_ISVTX  0001000 /* sticky bit */

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

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

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

以上から、st_mode の値からモード文字列を作成する以下の様な関数を作成する。

static void get_mode_string(mode_t mode, char *str) {
  str[0] = (S_ISBLK(mode))  ? 'b' :
           (S_ISCHR(mode))  ? 'c' :
           (S_ISDIR(mode))  ? 'd' :
           (S_ISREG(mode))  ? '-' :
           (S_ISFIFO(mode)) ? 'p' :
           (S_ISLNK(mode))  ? 'l' :
           (S_ISSOCK(mode)) ? 's' : '?';
  str[1] = mode & S_IRUSR ? 'r' : '-';
  str[2] = mode & S_IWUSR ? 'w' : '-';
  str[3] = mode & S_ISUID ? (mode & S_IXUSR ? 's' : 'S')
                          : (mode & S_IXUSR ? 'x' : '-');
  str[4] = mode & S_IRGRP ? 'r' : '-';
  str[5] = mode & S_IWGRP ? 'w' : '-';
  str[6] = mode & S_ISGID ? (mode & S_IXGRP ? 's' : 'S')
                          : (mode & S_IXGRP ? 'x' : '-');
  str[7] = mode & S_IROTH ? 'r' : '-';
  str[8] = mode & S_IWOTH ? 'w' : '-';
  str[9] = mode & S_ISVTX ? (mode & S_IXOTH ? 't' : 'T')
                          : (mode & S_IXOTH ? 'x' : '-');
  str[10] = '\0';
}

文字列の格納先を引数で指定し、そこにモード文字列を1文字ずつ作成し、終端している。 三項演算子、しかもそのネストは、下手に使うと可読性を損なってしまうが、 このようなシンプルな分岐であれば if を使うより可読性が高いと思う。 モード文字列の仕様も、前述の言葉と表の説明よりもこちらのほうが一目瞭然だろう。

上記処理をファイル名の出力処理に条件分岐付きで追加すれば完成だ。

    strncpy(&path[path_len], dent->d_name, PATH_MAX - path_len);
    if (lstat(path, &dent_stat) != 0) {
      perror(path);
      continue;
    }
    if (long_format) {
      char mode_str[11];
      get_mode_string(dent_stat.st_mode, mode_str);
      printf("%s ", mode_str);
    }
    printf("%s", name);
    if (classify) {
      print_type_indicator(dent_stat.st_mode);
    }
    putchar('\n');

出力結果

これを実行してみると以下のようにモード文字列が表示できるようになっている。

$ ./ls5 -l ~/lstest
srwxrwxr-x socket
prw-rw-r-- fifo
-rw-rw-r-- file
drwxrwxr-x dir
-rwxrwxr-x a.out
lrwxrwxrwx link