lsっぽいコマンドを作る

作成:

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

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

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

色分け表示

今回は色分け表示に対応させる。 最近の多くのディストリビューションでは ls コマンドのエイリアスでこのオプションが付いているため、 ls が色付きで表示されるのが当たり前と思っている人もいるだろう。

$ ls -l --color
合計 16
-rwxrwxr-x  1 ryosuke ryosuke 8672 12月 15 20:32 a.out
drwxrwxr-x  2 ryosuke ryosuke 4096  1月  9 19:14 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

デフォルトのエイリアスで指定されているが、明示的に指定する場合は、 --color オプションを指定する。 すると、上記のようにファイルの種別などに応じて色分け表示が行われる。

色分け表示が行われることは知っていても、それぞれの色がどのような意味なのかはしっかりとは知らないという人がほとんどだろう。 使用者側としては識別できる情報があり、自分が識別する上で便利なものだけ理解していれば良いのでそれで十分だろう。 しかし、それを作る側になるとちゃんと知っておかないと作ることはできない。

まとめると以下のようになっている。おそらくこの内の一部しか知らないという人がほとんどだろう。 私もこれを作るまでは知らなかった。

表示意味
filename
赤文字リンク先が存在しないシンボリックリンク
filename
赤背景/白文字setuidがついた通常ファイル
filename
黄背景/黒文字setgidがついた通常ファイル
filename
緑文字(ボールド)実行可能な通常ファイル
filename
修飾無し通常ファイル
filename
緑背景/黒文字Sticky bit が立ち、 Other のライトアクセスが可能なディレクトリ
filename
緑背景/青文字Other のライトアクセスが可能なディレクトリ
filename
青背景/白文字Sticky bit が立っているディレクトリ
filename
青文字(ボールド)ディレクトリ
filename
シアン文字(ボールド)シンボリックリンク
filename
黄文字名前付きパイプ
filename
マゼンタ文字(ボールド)UNIX ソケット
filename
黄文字(ボールド)キャラクターデバイス / ブロックデバイス

単純に文字の色が変わるだけではなく、フォントがボールドになったり、背景に色が付いているものもある。

色分け表示オプションの追加

まずは、色分け表示をさせるためのコマンドラインオプションを追加させる。 本物の ls では、 --color という「長い」オプションのみだが、 ここでは、 -C という短いオプションでも指定できるようにする。 なお、本物の ls では、 -C はカラム表示を指示するために使用されているので別の意味になる。

コマンドラインオプションのパース処理に以下のような修正を加える。

  const struct option longopts[] = {
...
      { "color", no_argument, NULL, 'C' },
...
  };
  while ((opt = getopt_long(argc, argv, "aACFl", longopts, NULL)) != -1) {
    switch (opt) {
...
      case 'C':
        if (isatty(STDOUT_FILENO)) {
          color = true;
        }
        break;
...

オプションの追加自体は良いだろう。 このオプションが指定された時の処理で一つ分岐が入っている。 ここで使用している関数は指定したファイルディスクリプタがコンソールを参照しているかを確認するものだ。 引数に指定している STDOUT_FILENO は名前からわかるだろうが、 stdout すなわち、標準出力を指すファイルディスクリプタだ。

#include <unistd.h>
int isatty(int fd);

色をつけて表示するためにはコンソール制御が必要となるが、 リダイレクトなどによってファイルに出力する場合に色付き出力が有効になっていると都合が悪い。 そのような場合はわざわざオプションを指定しないだろうという意見もあるだろうが、 ここでは指定された場合でも、標準出力がコンソールである場合にのみ有効になるようにした。

色付き表示の方法

次に色を付けて表示させる方法だ。 ファイル種別等の識別についてはロングフォーマットの出力のところですでに扱っている。 同じ方法で情報を判別すれば良い。 しかし、コンソールへの出力方法は、普通に文字を出力する方法しか知らないというのが通常だろう。 これについては、別の記事 ANSIエスケープコード で紹介しているエスケープシーケンスを利用する。 詳細についてはリンク先を参照してほしい。

このエスケープシーケンスを利用し、以下のような色付きでファイル名を表示する関数を用意する。

static void print_name_with_color(const char *name, mode_t mode, bool link_ok) {
  if (!link_ok) {
    printf("\033[31m");
  } else if (S_ISREG(mode)) {
    if (mode & S_ISUID) {
      printf("\033[37;41m");
    } else if (mode & S_ISGID) {
      printf("\033[30;43m");
    } else if (mode & S_IXUGO) {
      printf("\033[01;32m");
    } else {
      printf("\033[0m");
    }
  } else if (S_ISDIR(mode)) {
    if ((mode & S_ISVTX) && (mode & S_IWOTH)) {
      printf("\033[30;42m");
    } else if (mode & S_IWOTH) {
      printf("\033[34;42m");
    } else if (mode & S_ISVTX) {
      printf("\033[37;44m");
    } else {
      printf("\033[01;34m");
    }
  } else if (S_ISLNK(mode)) {
    printf("\033[01;36m");
  } else if (S_ISFIFO(mode)) {
    printf("\033[33m");
  } else if (S_ISSOCK(mode)) {
    printf("\033[01;35m");
  } else if (S_ISBLK(mode)) {
    printf("\033[01;33m");
  } else if (S_ISCHR(mode)) {
    printf("\033[01;33m");
  }
  printf("%s", name);
  printf("\033[0m");
}

ファイル名と、モード情報、リンクが有効か否かのフラグを元に分岐させ、表示している。 前述の表のとおりになっているはずだ。

ファイル名出力のところで、色付きオプションが指定されていればこの関数を呼び出すように変更する。 リンクが有効か否かのフラグを指定する必要があるが、これについては次項で説明する。

if (color) {
  print_name_with_color(name, dent_stat.st_mode, link_ok);
} else {
  printf("%s", name);
}

シンボリックリンクのリンク先情報の色分け

前回シンボリックリンクのリンク先の情報を表示するようにしたが、 これについてもシンボリックリンク自体とは別に色分けが必要となる。 また、ファイル名の色分けにおいて、 シンボリックリンクのリンク先が存在するかどうか、つまりリンクが有効かどうかの確認も必要となる。

そこで、シンボリックリンクの情報の読み出しを先に行うように変更する。 また、シンボリックリンクのリンク先の stat 情報も読みだしておく。 リンク先の stat 情報を読み出すには lstat ではなく、 stat を利用する。 lstat が成功しているのに stat が失敗する場合は、 リンク先が存在しないものとして扱う。

if (S_ISLNK(dent_stat.st_mode)) {
  link_len = readlink(path, link, PATH_MAX);
  if (link_len > 0) {
    link[link_len] = 0;
  }
  if (stat(path, &link_stat) != 0) {
    link_ok = false;
  }
}

リンク先を表示する箇所では、ファイル名と同様に色付き表示の関数を利用する。 ここで渡すモード情報は stat で取得した、リンク先のモード情報だ。

if (long_format) {
  if (link_len > 0) {
    printf(" -> ");
    if (color) {
      print_name_with_color(link, link_stat.st_mode, link_ok);
    } else {
      printf("%s", link);
    }
  }
}

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

$ ./ls9 -l --color ../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 -> file

以上で色付き表示への対応は完了である。