Linux環境でC言語を使ってディレクトリ操作を試してみました。
本記事では、ディレクトリの開き方、ディレクトリ内の読み込み方、ディレクトリの作成のやり方、ディレクトリの閉じ方を一通り説明します。
変更履歴
- 2021 / 06 / 25 タイトルを変更しました。
概要
- ディレクトリを開くには
opendir(3)
関数を使います。 - ディレクトリ内を読み込むには
readdir(3)
関数を使います。 - ディレクトリを閉じるには
closedir(3)
関数を使います。 - ディレクトリを作成するには
mkdir(2)
関数を使います。 - ディレクトリを削除するには
rm(2)
関数を使います。 - 標準ライブラリのディレクトリ操作では、ディレクトリを
DIR
構造体で表現し、ディレクトリ内のエントリ(ファイル)をdirent
構造体で表現します。
動機
前回Linuxの理解を深めようとファイルに関する入出力を調査したのですが、同時にディレクトリもLinuxにおいて重要な要素だと思い、調査しました。
前提知識
下記に挙げた内容に関して詳細な説明は省略します。
- C言語の基本的な知識(条件分岐やポインタなど)
- C言語で書かれたプログラムをコンパイルし実行できる
動作環境
本記事の内容は以下の環境で作成しました。
- コンパイラ:gcc(バージョン4.8.5 20150623 (Red Hat 4.8.5-44))
- OS:CentOS(バージョン7)
作成したプログラム
最初にディレクトリを操作するプログラムの作成を載せ全体像を示します。このプログラムは引数で指定されたディレクトリを開き、中のエントリを一覧表示し、ディレクトリを閉じます。また、 ./tmp
ディレクトリを作成します。既にファイルやディレクトリが存在すれば、削除を試みます。削除の際に stat
関数を使用していますが、ディレクトリの操作ではないため、ここでは説明を省きます。
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <dirent.h>
#include <errno.h>
#include <unistd.h>
#define DIR_PATH "./tmp"
static void rm(const char *path);
int
main(int argc,
char *argv[])
{
if (argc != 2) {
fprintf(stderr, "require one argument");
exit(EXIT_FAILURE);
}
// 1. ディレクトリを開く
DIR *dir = opendir(argv[1]);
if (!dir) {
perror(argv[0]);
exit(EXIT_FAILURE);
}
// 2. ディレクトリ内に存在するファイルを一覧表示する
printf("ファイルを一覧表示\n");
struct dirent *ent;
while ((ent = readdir(dir))) {
printf("%s\n", ent->d_name);
}
if (errno) {
perror(argv[0]);
exit(EXIT_FAILURE);
}
// 3. ディレクトリを閉じる
if (closedir(dir)) {
perror(argv[0]);
exit(EXIT_FAILURE);
}
rm(DIR_PATH);
// 4. ディレクトリを作成する
printf("%s を作成します\n", DIR_PATH);
if (mkdir(DIR_PATH, 0755)) {
perror(argv[0]);
exit(EXIT_FAILURE);
}
exit(EXIT_SUCCESS);
}
// 対象のパスにファイルが存在する場合、
// 普通のファイルまたはディレクトリであれば削除
void
rm(const char *path)
{
struct stat buf;
if (!stat(path, &buf)) {
// stat関数は失敗するとerrnoを変更する
// 後続処理のためにリセット
errno = 0;
printf("%s を削除します\n", path);
if (S_ISDIR(buf.st_mode)) {
if (rmdir(path)) {
perror(path);
exit(EXIT_FAILURE);
}
} else if (S_ISREG(buf.st_mode)) {
if (unlink(path)) {
perror(path);
exit(EXIT_FAILURE);
}
} else {
fprintf(stderr, "削除できませんでした\n");
exit(EXIT_FAILURE);
}
}
}
ディレクトリを開く・閉じる
ディレクトリを開いたり、閉じたりするには opendir(3)
と closedir(3)
を使います。定義は次のとおりです。
DIR *opendir(const char *name);
int closedir(DIR *dirp);
どちらも DIR
構造体を戻り値または引数として使用しているのがわかります。これはディレクトリを表す構造体です。ファイルを開いたり閉じたりする際には FILE
構造体を使用しますが、それのディレクトリ版になります。
それぞれの使い方は次のとおりです。
-
opendir(3)
は引数にディレクトリのパスを指定します。ディレクトリを開けなかった場合NULL
を返します。 -
closedir(3)
は引数に閉じたいDIR
を指定します。ディレクトリを閉じれなかった場合-1
を返します。
上記のプログラムではこれらの関数を以下のように使いました。それぞれエラーが起きている場合は perror
でメッセージを表示し、異常終了します。
// 1. ディレクトリを開く
DIR *dir = opendir(argv[1]);
if (!dir) {
perror(argv[0]);
exit(EXIT_FAILURE);
}
// 〜中略〜
// 3. ディレクトリを閉じる
if (closedir(dir)) {
perror(argv[0]);
exit(EXIT_FAILURE);
}
これらの関数の詳細は次のページで確認できます。
ディレクトリ内を読み込む
ディレクトリ内を読み込むには readdir(3)
を使用します。定義は次のとおりです。
struct dirent *readdir(DIR *dirp);
引数に DIR
構造体を受け取り、ディレクトリ内のエントリを表す dirent
構造体を返します。 dirent
構造体にアクセスすることで、エントリについての情報を取得することができます。この構造体はすべてのシステムで同じ定義にはなっていませんが、ファイル名を表す d_name
フィールドについては POSIX.1 で定められているようなので、必ずあると言っていいと思います。注意として d_name
の最大文字数は定められていないので、長過ぎるファイル名の場合はどのような動作になるかわかりません。 dirent
の大まかな定義についてはマニュアルか、下記に記したリンク先に記載があります。
また、readdir(3)
は呼び出すたびに次の dirent
構造体を返します。これはファイル入出力の fgetc(3)
と同じような動作になっており、ディレクトリ内のエントリを一つづつ取得する動作になります。エントリを最後まで読み込むと NULL
が返されます。ただし、エラーが発生した場合も NULL
が返されるので、 errno
の値を確認することが必要になります。
上記のプログラムではこの関数を次のように使っています。
// 2. ディレクトリ内に存在するファイルを一覧表示する
printf("ファイルを一覧表示\n");
struct dirent *ent;
while ((ent = readdir(dir))) {
printf("%s\n", ent->d_name);
}
if (errno) {
perror(argv[0]);
exit(EXIT_FAILURE);
}
while
文では readdir(dir)
の戻り値が NULL
かどうかを見ています。つまり、末端かエラーが発生しない限り、ディレクトリの中の情報を取得する動きになります。
この関数の詳細は次のページで確認できます。
ディレクトリの作成
ディレクトリの作成には mkdir(2)
を使います。この関数は標準ライブラリではなく、システムコールになります。定義は次のとおりです。
int mkdir(const char *pathname, mode_t mode);
この関数は pathname
に指定されたパスにディレクトリを作成します。ディレクトリのパーミッションは mode
により指定します。 mode
の値は 0755
のように指定します。C言語では先頭に0がついている場合、8進数であることを表します。
パーミッションの指定形式についてもう少し詳しく解説すると、パーミッションはユーザー、ユーザグループ、その他に分かれて管理されます。 mode
の値は8進数表記において、それぞれの桁数がそれぞれのパーミッションを表現しています。755
の場合、7は読み取り・書き込み・実行のすべての権限を示し、5は読み取りと実行の権限を表しています。つまり、ディレクトリの所有者は読み取り・書き込み・実行ができ、ユーザグループとその他のユーザーは読み取りと実行ができます。ただし、ここで設定した値がそのままディレクトリに設定されるわけではありません。詳しくは下記のumaskのセクションで解説します。
mkdir
関数で作成されるディレクトリの所有者はプロセスの実行ユーザになります。所有グループも基本的にはプロセスの実行グループになりますが、親のディレクトリを継承する場合もあります。詳しくはマニュアルを参照してください。
上記のプログラムでは以下のように関数を使用しています。
// 4. ディレクトリを作成する
printf("%s を作成します\n", DIR_PATH);
if (mkdir(DIR_PATH, 0755)) {
perror(argv[0]);
exit(EXIT_FAILURE);
}
エラーが発生すると -1
が返却されるので、その場合はプログラムを異常終了させます。
関数の詳しい情報は以下で確認できます。
umask
ディレクトリを作成する際に指定する mode
値のパーミッションはumaskと言われる値の影響を受け、最終的なパーミッションが決まります。パーミッションは次の処理によって決定されます。
mode & ~umask & 0777
この処理の意味は、modeで設定された値をumask値でマスク(除去)するということになります。例えば、umaskに 0002
と設定されていた場合、modeで 0777
を指定すると、設定されるパーミッションは 0775
となります。この時、umaskの 0002
は他者による書き込みを表しており、最終的なパーミッションからその値が削られていることになります。
umaskの値は umask(2)
関数で変更することができます。また umask
コマンドでも変更が可能です。
詳しくは次のページを参考にしてください。
また、数字がどんな権限を表しているかは以下のページで確認できます。
ディレクトリの削除
ディレクトリを削除するには rmdir(2)
を使います。こちらもディレクトリの作成と同様、システムコールです。定義は次のとおりです。
int rmdir(const char *pathname);
関数の引数に削除したいディレクトリのパスを指定します。ディレクトリの中は空でなければいけません。もし、空でないディレクトリを削除するには中のファイルをすべて削除するか、移動させるかする必要があります。削除に失敗すると -1
が返されます。
上記のプログラムでは以下のように使っています。
if (rmdir(path)) {
perror(path);
exit(EXIT_FAILURE);
}
この関数の詳しい情報はこちらを参考にしてください。
ファイル入出力との比較
ディレクトリ操作とファイル操作について比較してみたいと思います。
操作 | ファイル | ディレクトリ |
---|---|---|
開く | fopen | opendir |
閉じる | fclose | closedir |
読み込む | fgetc | readdir |
読み込む基本単位 | バイト | dirent構造体 |
書き込む | fputc | - |
作成する | fopen | mkdir |
表現する構造体 | FILE | DIR |
参考文献
- 書籍: ふつうのLinuxプログラミング第2版 青木 峰郎著
- Webサイト: 苦しんで覚えるC言語 (https://9cguide.appspot.com/ )
- Webサイト: JM Project - Linux関連のマニュアルページの日本語版 (http://linuxjm.osdn.jp/ )
- Webサイト: kazmax Linuxで自宅サーバー (https://kazmax.zpp.jp/ )