lsっぽいコマンドを作る
UNIX 環境のコマンドラインを触ったことがある人ならどんな人でも使ったことがあるコマンドの一つ、
ls
っぽいコマンドラインプログラムを作りながら、そのために必要な要素について解説していく。
あくまで「っぽい」であり、本物と全く同じものを作るわけではないので注意。
説明に使用するプログラムコードについては GitHub で公開している。 ソースコードの全文をよく見たい、ダウンロードしたいなどの場合はこちらを参照してほしい。
今回は ls10.c を利用した説明になる。
サブディレクトリの再帰的表示
前回までで指定ディレクトリの表示に関しては概ねできるようになった。 今回は ls の -R オプションを指定した時のような、 指定ディレクトリ以下のサブディレクトリについても再帰的に探索して表示する機能を追加する。
まずは以下のようにオプションの追加と、それに基づくグローバルフラグを用意する。
static bool recursive = false;
static char *parse_cmd_args(int argc, char**argv) {
char *path = "./";
int opt;
const struct option longopts[] = {
...
{ "recursive", no_argument, NULL, 'R' },
};
while ((opt = getopt_long(argc, argv, "aACFlR", longopts, NULL)) != -1) {
switch (opt) {
...
case 'R':
recursive = true;
break;
...
短いオプションは -R
で、長いオプションは --reursive
だ。
これは本物の ls と同じものだ。
あとは、以下のように list_dir
関数の最後で、
このフラグが立っている場合に再帰処理を追加すれば完成だ。
static void list_dir(const char *base_path) {
...
if (recursive) {
rewinddir(dir);
while ((dent = readdir(dir)) != NULL) {
const char *name = dent->d_name;
if (name[0] == '.'
&& (filter == FILTER_DEFAULT
|| name[1 + (name[1] == '.')] == '\0')) {
continue;
}
if (dent->d_type == DT_DIR) {
strncpy(&path[path_len], dent->d_name, PATH_MAX - path_len);
printf("\n%s:\n", path);
list_dir(path);
}
}
}
closedir(dir);
}
再帰処理のところで以下の関数を使用している。
#include <sys/types.h>
#include <dirent.h>
void rewinddir(DIR *dirp);
名前の通り、一度読み出し処理をしたディレクトリストリームを最初の位置に戻す効果がある。
単純にサブディレクトリ以下すべてを表示するという処理にしようと考えた場合、 ディレクトリ内の表示処理の途中でディレクトリを見つけたら、その中身を表示する、 という処理が最初に思い浮かぶかもしれない。 しかし、そうすると、親ディレクトリの中身にそのサブディレクトリの内容が混じって表示されてしまい、 ディレクトリ単位での一覧表示にならない。 そのため、ディレクトリの内容を一旦表示した後に、再度走査するようにしている。
次にドットファイルの除外であるが、 ここについては、オプション指定の有無にかかわらず、 親ディレクトリ及びカレントディレクトリは必ず除外するようにする。 そうしないと無限ループに陥るからだ。
表示すべきディレクトリがわかったら、現在のディレクトリへのパスと結合して、 そのディレクトリを表示するように自分自身を呼び出す。
再帰呼び出しを使うことで、非常に簡単にサブディレクトリの再帰的な表示が実現できた。 しかし、関数の再帰呼び出しは、記述量が小さくてすむというメリットもあるが、必ずしも効率が良いとはいえない。 ここでの処理で言えば、書き方の問題もあるが、ディレクトリストリームはオープンしたまま再帰を行っているし、 スタックに不要なものが積まれるため、リソースの消費量が多く、かつ消費量やその上限が読みにくい。 次回はこの辺を改善した方法について説明する。