lsっぽいコマンドを作る

作成:

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

Linux で使用される、「本物の」lsは Coreutils という様々な基本的コマンドラインプログラムのパッケージに含まれている。 公式サイトは Coreutils - GNU core utilities で、ここからソースコード一式を含め入手することができる。また、 GitWeb などを使ってlsのソースコードを直接閲覧することもできる。

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

今回は ls1.c と、 ls2.c を利用した説明になる。

「ls」とは?

前置きに書いたとおり、UNIX環境を少しでも触れたことのある人であれば ls を使ったことがない、という人はいないだろうが、 実行すると以下のような表示が行われる。

$ ls
examples.desktop  ダウンロード  デスクトップ  ビデオ    ミュージック
ls                テンプレート  ドキュメント  ピクチャ  公開

lsMAN で調べると

ls - ディレクトリの内容をリスト表示する

と、ある通り、lsとは ディレクトリの内容を表示するコマンドラインプログラムだ。

ディレクトリエントリの取得

さて、lsとはディレクトリの内容を表示するプログラムである。 同様のプログラムを作るにあたり、 まずはディレクトリの中にあるファイルの名前一覧を表示してみよう。 やるべきことは非常にシンプルなことだが、 ディレクトリの内容を取得する方法はC言語の入門書には通常書かれていない。 C言語標準の方法はないからだ。 そのためにはOSなどに依存したAPIを利用する必要がある。

ディレクトリストリーム

C言語標準ではファイルの入出力にファイルストリームを利用する。 ファイルストリームという呼び名にピンとこない人もいるかもしれないが、 fopenの戻り値であるFILE構造体で表現されるあれだ。 POSIX標準ではこのファイルストリームと似た扱いができるディレクトリストリームというものがあり、 これを利用してディレクトリの内容を読みだす仕組みが用意されている。

ディレクトリストリームのオープン・クローズは以下のAPIを使用する。 使い方はほぼファイル入出力で使用するfopenfcloseと同じだ。

#include <sys/types.h>
#include <dirent.h>

DIR *opendir(const char *name);
int closedir(DIR *dirp);

オープンしたディレクトリストリームは以下のAPIを利用して読み出しを行う。 readdir()はスレッドセーフではない、 スレッドセーフ版としてreaddir_r()も利用できる。

#include <dirent.h>

struct dirent {
    ino_t          d_ino;
    off_t          d_off;
    unsigned short d_reclen;
    unsigned char  d_type;
    char           d_name[256];
};
struct dirent *readdir(DIR *dirp);
int readdir_r(DIR *dirp, struct dirent *entry, struct dirent **result);

ディレクトリエントリを示すstruct direntは環境依存が強い構造体だ。

struct direntの中でPOSIXで要求されているメンバーは inode番号を示すd_inoと、ファイル名を示すd_nameのみである。 また。d_nameは通常最大長がNAME_MAXの文字列を格納できる長さになっている。 NAME_MAXは通常255なので、 d_nameの長さは終端を含めて256というシステムが一般的だが、 規定されているわけではない。

Linuxのヘッダを見るとNAME_MAXは linux/limits.h などで定義されている。 しかし、dirent.h ではこれを include しなくても良いように、 上記のようにd_nameの長さがヘッダ内でハードコードされている。

d_offd_reclenは例えばCygwinでは利用できないし、 利用できるシステムであってもアプリケーションで意味のある値として利用することは通常ない。

d_typeは多くのシステムで利用できるが、 POSIXで規定されていないため利用できないシステムもあるだろう。 また、このパラメータによってファイルタイプを得ることができるか否かは、ファイルシステムにも依存する。

DT_BLKブロックデバイス
DT_CHRキャラクターデバイス
DT_DIRディレクトリ
DT_FIFO名前付きパイプ (FIFO)
DT_LNKシンボリックリンク
DT_REG通常のファイル
DT_SOCKUNIX ドメインソケット
DT_UNKNOWNファイルタイプ不明

まとめると、通常使用できそうなフィールドとしてはd_nameぐらいで、 次が環境依存はあるもののd_typeとなるだろう。

inode 番号とはなんぞやと思う人も多いだろう。 UNIX 系のファイルシステムではファイルの情報を inode というデータ構造で管理されている。 この inode にはファイルシステム内でユニークな番号が割り当てられており、これが inode 番号である。 一般に番号そのものを利用する機会は少ないだろうが、この概念は覚えておいたほうが良いだろう。

ファイル名一覧を表示するプログラム

さて、必要となるAPIがわかったところで、当初の目的に戻ろう。 これらAPIを使用して、ディレクトリの中にあるファイルの名前一覧を表示するプログラムを作ってみよう。

#include <stdio.h>
#include <dirent.h>

int main(int argc, char**argv) {
  char *path = "./";
  DIR *dir;
  struct dirent *dent;
  if (argc > 1) {
    path = argv[1];
  }
  dir = opendir(path);
  if (dir == NULL) {
    perror(path);
    return 1;
  }
  while ((dent = readdir(dir)) != NULL) {
    printf("%s\n", dent->d_name);
  }
  closedir(dir);
  return 0;
}

引数無しで呼び出せばカレントディレクトリ、 引数があればそれをディレクトリへのパスと解釈して、 そのディレクトリエントリのリストを表示する。 というものだ。

これを実行すると、以下のように出力されるだろう。

$ ./ls1
..
ls3.c
ls7
ls3
ls5
ls12.c
Makefile
ls6
ls11
ls4.c
ls11.c
.git
ls1
ls2
ls5.c
ls7.c
ls4
ls6.c
ls12
ls8
ls2.c
ls8.c
ls10
ls9
Doxyfile
LICENSE.txt
ls9.c
.gitignore
.
ls1.c
ls10.c
README.md

すこし期待と違っていただろうか? lsの出力は通常名前順に並んでいる。 しかし、ディレクトリストリームから取れた順番はてんでバラバラだ。 lsのような出力を得るためには名前順でのソートが必要となる。

Cygwin環境では少し違っていて名前順で取れることだろう この辺はファイルシステム自体の違いやOSの管理方式の違いによる。

ソート機能は最終的に実装するが、 ソートするためにはディレクトリエントリを全て一度メモリ上に読み出す必要があり、メモリ使用量が大きくなってしまう。 まずは、メモリ使用量が大きくなる機能は避け、他の機能を酒に追加していき、最後のほうで追加する予定だ。

エラー処理についても少し触れておく。 正しくないパスを指定すると、opendirでNULLが返るので、 perrorによって以下の様にエラー表示が行われる。

$ ./ls1 aa
aa: No such file or directory
$ ./ls1 ls1.c
ls1.c: Not a directory
$ ./ls1 /root
/root: Permission denied

さて、これでlsっぽいプログラムの取っ掛かりができた。 ここから、様々な機能を付加していって、それなりに使えるプログラムにしていこう。

今後の拡張を見据えたコード整理

最後に、このプログラムを拡張していくにあたって、以下の様な関数化を行っておく。 機能が大きくなれば関数化はさけられないので、 予めこのようにしておけばどこにどのような修正が入ったかという確認がしやすくなるだろう。

#include <stdio.h>
#include <stdlib.h>
#include <dirent.h>

static char *parse_cmd_args(int argc, char**argv);
static void list_dir(const char *base_path);

/**
 * @brief コマンドライン引数をパースする
 * @param[IN] argc 引数の数
 * @param[IN/OUT] argv 引数配列
 * @return パス
 */
static char *parse_cmd_args(int argc, char**argv) {
  char *path = "./";
  if (argc > 1) {
    path = argv[1];
  }
  return path;
}

/**
 * @brief 指定パスのディレクトリエントリをリストする
 * @param[IN] base_path パス
 */
static void list_dir(const char *base_path) {
  DIR *dir;
  struct dirent *dent;
  dir = opendir(base_path);
  if (dir == NULL) {
    perror(base_path);
    return;
  }
  while ((dent = readdir(dir)) != NULL) {
    printf("%s\n", dent->d_name);
  }
  closedir(dir);
}

int main(int argc, char**argv) {
  char *path = parse_cmd_args(argc, argv);
  if (path == NULL) {
    return EXIT_FAILURE;
  }
  list_dir(path);
  return EXIT_SUCCESS;
}