lsっぽいコマンドを作る

作成:

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

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

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

隠しファイルのフィルタリング

前回第一ステップとして、指定ディレクトリ内のファイル名を表示するプログラムを作成した。 実行すると、以下の様な表示が得られる。

$ ./ls2 ~/
..
.bash_logout
.profile
examples.desktop
.
.bashrc

まずはソートされていないことが気になるかもしれないが、 前回も書いたとおり、最後のほうでソート機能を追加する予定なので後回しにする。

通常lsではオプションを指定しなければ表示されない、 「.」から始まる隠しファイルが表示されてしまっている。 今回はこれらのファイルの有無をフィルタリングする機能を追加する。

念のため説明すると、「.」は現在のディレクトリを、「..」は親ディレクトリを指す。 また、 UNIX のファイルシステムには通常隠しファイルという属性はないが、 習慣的に「.」から始まるファイルは隠しファイルとして扱われる。

lsはデフォルトでは隠しファイルを表示しない。 引数で指定した場合に隠しファイルを含めて表示するようになる。 その時に指定するオプションは2種類ある。

-a, --all
「.」で始まる要素を表示する
-A, --almost-all
「.」「..」を除く、「.」で始まる要素を表示する

これに習い、デフォルトでは「.」で始まるファイルは表示せず、上記2つのオプションで表示を指定できるようにしてみよう。

コマンドライン引数の処理

コマンドライン引数の処理を追加しよう。 コマンドライン引数によって、デフォルトと、2つのモードを切り替えるため、 このモードを表現する値が必要となるので enum で定義し、 その値を スタティックグローバル変数の int 値として保持するようにする。 もっと規模の大きなプログラムになるとあまりよい方法ではないが、 コマンドラインから一発実行するだけの小さなプログラムなので、モードをこのように保持するようにする。

enum {
  FILTER_DEFAULT, /**< '.'から始まるもの以外を表示する */
  FILTER_ALMOST,  /**< '.'と'..'以外を表示する */
  FILTER_ALL,     /**< すべて表示する */
};

static int filter = FILTER_DEFAULT;

引数のパースは getopt_long を利用する。 利用する上で間違いやすいところのある関数なので、 利用方法については「getopt_long関数の利用」を参照して欲しい。 2つの引数について「長い」引数と「短い」引数両方を利用可能な形で表現すると以下のようになる。

static char *parse_cmd_args(int argc, char**argv) {
  char *path = "./";
  int opt;
  const struct option longopts[] = {
      { "all", no_argument, NULL, 'a' },
      { "almost-all", no_argument, NULL, 'A' },
  };
  while ((opt = getopt_long(argc, argv, "aA", longopts, NULL)) != -1) {
    switch (opt) {
      case 'a':
        filter = FILTER_ALL;
        break;
      case 'A':
        filter = FILTER_ALMOST;
        break;
      default:
        return NULL;
    }
  }
  if (argc > optind) {
    path = argv[optind];
  }
  return path;
}

フィルタリング

コマンドライン引数からモードを設定することができるようになったので、 この変数と、名前からフィルタリング条件を作ると以下のようになる。 表示「しない」条件を判定し、continueするというものだ。

  while ((dent = readdir(dir)) != NULL) {
    const char *name = dent->d_name;
    if (filter != FILTER_ALL
        && name[0] == '.'
        && (filter == FILTER_DEFAULT
            || name[1 + (name[1] == '.')] == '\0')) {
      continue;
    }
    printf("%s\n", name);
  }

技巧的な書き方をしてしまっているので、説明する。

フィルタ条件が FILTER_ALL の場合はすべてを表示するので条件非成立。 それ以外の場合、条件が成立するには1文字目が「.」である必要がある。

フィルタ条件が FILTER_DEFAULT の場合は「.」から始まる名前はすべて表示しないので、条件成立。 それ以外、すなわち、 FILTER_ALMOST の場合は、「.」と「..」のみ表示しない、である。

ここまでは良いと思うが、 その条件が、 name[1 + (name[1] == '.')] == '\0' という C言語において比較演算子の「真」の値は1と決められているのを利用した技巧的な表現になっている。 先に添字部分が評価され、 name[1] が「.」でなければ、 name[1] が「\0」、 name[1] が「.」ならば、 name[2] が「\0」の時、真という条件文になっている。

素直に書き下すと name[1] != '.' && name[1] == '\0' || name[1] == '.' && name[2] == '\0' などと長ったらしくなってしまう。 どう表現するべきか迷ったのだが、Coreutilsのソースコードでも利用されているため採用することにした。

比較演算子の戻り値が「偽」の場合は 0 、「真」の場合は 1 と決められているが、 条件判定においては 0 は「偽」、それ以外の値は「真」と判定される。 そのため、条件判定では、「真」の値が 1 であると期待する処理を書いてはいけない。 ここを混同しないように注意して欲しい。

実行結果

以上でフィルタリング処理が実装できたので試してみる。

$ ./ls3 ~/
examples.desktop

$ ./ls3 ~/ -a
..
.bash_logout
.profile
examples.desktop
.
.bashrc

$ ./ls3 ~/ -A
.bash_logout
.profile
examples.desktop
.bashrc

期待通りの出力結果になっている。 上記例では短いオプションのみ試しているが、長いオプションでも同様に動作する。