lsっぽいコマンドを作る
UNIX 環境のコマンドラインを触ったことがある人ならどんな人でも使ったことがあるコマンドの一つ、 lsっぽいコマンドラインプログラムを作りながら、そのために必要な要素について解説していく。 あくまで「っぽい」であり、本物と全く同じものを作るわけではないので注意。
Linux で使用される、「本物の」lsは Coreutils という様々な基本的コマンドラインプログラムのパッケージに含まれている。 公式サイトは Coreutils - GNU core utilities で、ここからソースコード一式を含め入手することができる。また、 GitWeb などを使ってlsのソースコードを直接閲覧することもできる。
説明に使用するプログラムコードについては GitHub で公開している。 ソースコードの全文をよく見たい、ダウンロードしたいなどの場合はこちらを参照してほしい。
今回は ls1.c と、 ls2.c を利用した説明になる。
「ls」とは?
前置きに書いたとおり、UNIX環境を少しでも触れたことのある人であれば ls を使ったことがない、という人はいないだろうが、 実行すると以下のような表示が行われる。
$ ls examples.desktop ダウンロード デスクトップ ビデオ ミュージック ls テンプレート ドキュメント ピクチャ 公開
lsを MAN で調べると
ls - ディレクトリの内容をリスト表示する
と、ある通り、lsとは ディレクトリの内容を表示するコマンドラインプログラムだ。
ディレクトリエントリの取得
さて、lsとはディレクトリの内容を表示するプログラムである。 同様のプログラムを作るにあたり、 まずはディレクトリの中にあるファイルの名前一覧を表示してみよう。 やるべきことは非常にシンプルなことだが、 ディレクトリの内容を取得する方法はC言語の入門書には通常書かれていない。 C言語標準の方法はないからだ。 そのためにはOSなどに依存したAPIを利用する必要がある。
ディレクトリストリーム
C言語標準ではファイルの入出力にファイルストリームを利用する。
ファイルストリームという呼び名にピンとこない人もいるかもしれないが、
fopen
の戻り値であるFILE
構造体で表現されるあれだ。
POSIX標準ではこのファイルストリームと似た扱いができるディレクトリストリームというものがあり、
これを利用してディレクトリの内容を読みだす仕組みが用意されている。
ディレクトリストリームのオープン・クローズは以下のAPIを使用する。
使い方はほぼファイル入出力で使用するfopen
fclose
と同じだ。
#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_off
とd_reclen
は例えばCygwinでは利用できないし、
利用できるシステムであってもアプリケーションで意味のある値として利用することは通常ない。
d_type
は多くのシステムで利用できるが、
POSIXで規定されていないため利用できないシステムもあるだろう。
また、このパラメータによってファイルタイプを得ることができるか否かは、ファイルシステムにも依存する。
DT_BLK | ブロックデバイス |
---|---|
DT_CHR | キャラクターデバイス |
DT_DIR | ディレクトリ |
DT_FIFO | 名前付きパイプ (FIFO) |
DT_LNK | シンボリックリンク |
DT_REG | 通常のファイル |
DT_SOCK | UNIX ドメインソケット |
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;
}