[Linux][C言語]ディレクトリ操作をやってみる

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（バージョン７）


# 作成したプログラム

最初にディレクトリを操作するプログラムの作成を載せ全体像を示します。このプログラムは引数で指定されたディレクトリを開き、中のエントリを一覧表示し、ディレクトリを閉じます。また、 `./tmp` ディレクトリを作成します。既にファイルやディレクトリが存在すれば、削除を試みます。削除の際に `stat` 関数を使用していますが、ディレクトリの操作ではないため、ここでは説明を省きます。

``` c
#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)` を使います。定義は次のとおりです。

``` c
DIR *opendir(const char *name);

int closedir(DIR *dirp);
```

どちらも `DIR` 構造体を戻り値または引数として使用しているのがわかります。これはディレクトリを表す構造体です。ファイルを開いたり閉じたりする際には `FILE` 構造体を使用しますが、それのディレクトリ版になります。

それぞれの使い方は次のとおりです。

* `opendir(3)` は引数にディレクトリのパスを指定します。ディレクトリを開けなかった場合 `NULL` を返します。
* `closedir(3)` は引数に閉じたい `DIR` を指定します。ディレクトリを閉じれなかった場合 `-1` を返します。

上記のプログラムではこれらの関数を以下のように使いました。それぞれエラーが起きている場合は `perror` でメッセージを表示し、異常終了します。

``` c
  // 1. ディレクトリを開く
  DIR *dir = opendir(argv[1]);
  if (!dir) {
    perror(argv[0]);
    exit(EXIT_FAILURE);
  }

  // 〜中略〜

  // 3. ディレクトリを閉じる
  if (closedir(dir)) {
    perror(argv[0]);
    exit(EXIT_FAILURE);
  }
```

これらの関数の詳細は次のページで確認できます。

https://linuxjm.osdn.jp/html/LDP_man-pages/man3/opendir.3.html

https://linuxjm.osdn.jp/html/LDP_man-pages/man3/closedir.3.html


# ディレクトリ内を読み込む

ディレクトリ内を読み込むには `readdir(3)` を使用します。定義は次のとおりです。

``` c
struct dirent *readdir(DIR *dirp);
```

引数に `DIR` 構造体を受け取り、ディレクトリ内のエントリを表す `dirent` 構造体を返します。 `dirent` 構造体にアクセスすることで、エントリについての情報を取得することができます。この構造体はすべてのシステムで同じ定義にはなっていませんが、ファイル名を表す `d_name` フィールドについては POSIX.1 で定められているようなので、必ずあると言っていいと思います。注意として `d_name` の最大文字数は定められていないので、長過ぎるファイル名の場合はどのような動作になるかわかりません。 `dirent` の大まかな定義についてはマニュアルか、下記に記したリンク先に記載があります。

また、`readdir(3)` は呼び出すたびに次の `dirent` 構造体を返します。これはファイル入出力の `fgetc(3)` と同じような動作になっており、ディレクトリ内のエントリを一つづつ取得する動作になります。エントリを最後まで読み込むと `NULL` が返されます。ただし、エラーが発生した場合も `NULL` が返されるので、 `errno` の値を確認することが必要になります。

上記のプログラムではこの関数を次のように使っています。

``` c
  // 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` かどうかを見ています。つまり、末端かエラーが発生しない限り、ディレクトリの中の情報を取得する動きになります。

この関数の詳細は次のページで確認できます。

https://linuxjm.osdn.jp/html/LDP_man-pages/man3/readdir.3.html

# ディレクトリの作成

ディレクトリの作成には `mkdir(2)` を使います。この関数は標準ライブラリではなく、システムコールになります。定義は次のとおりです。

``` c
int mkdir(const char *pathname, mode_t mode);
```

この関数は `pathname` に指定されたパスにディレクトリを作成します。ディレクトリのパーミッションは `mode` により指定します。 `mode` の値は `0755` のように指定します。C言語では先頭に0がついている場合、８進数であることを表します。

パーミッションの指定形式についてもう少し詳しく解説すると、パーミッションはユーザー、ユーザグループ、その他に分かれて管理されます。 `mode` の値は８進数表記において、それぞれの桁数がそれぞれのパーミッションを表現しています。`755` の場合、7は読み取り・書き込み・実行のすべての権限を示し、5は読み取りと実行の権限を表しています。つまり、ディレクトリの所有者は読み取り・書き込み・実行ができ、ユーザグループとその他のユーザーは読み取りと実行ができます。ただし、ここで設定した値がそのままディレクトリに設定されるわけではありません。詳しくは下記のumaskのセクションで解説します。

`mkdir` 関数で作成されるディレクトリの所有者はプロセスの実行ユーザになります。所有グループも基本的にはプロセスの実行グループになりますが、親のディレクトリを継承する場合もあります。詳しくはマニュアルを参照してください。

上記のプログラムでは以下のように関数を使用しています。

``` c
  // 4. ディレクトリを作成する
  printf("%s を作成します\n", DIR_PATH);
  if (mkdir(DIR_PATH, 0755)) {
    perror(argv[0]);
    exit(EXIT_FAILURE);
  }
```

エラーが発生すると `-1` が返却されるので、その場合はプログラムを異常終了させます。

関数の詳しい情報は以下で確認できます。

https://linuxjm.osdn.jp/html/LDP_man-pages/man2/mkdir.2.html


## umask

ディレクトリを作成する際に指定する `mode` 値のパーミッションはumaskと言われる値の影響を受け、最終的なパーミッションが決まります。パーミッションは次の処理によって決定されます。

``` c
mode & ~umask & 0777
```

この処理の意味は、modeで設定された値をumask値でマスク（除去）するということになります。例えば、umaskに `0002` と設定されていた場合、modeで `0777` を指定すると、設定されるパーミッションは `0775` となります。この時、umaskの `0002` は他者による書き込みを表しており、最終的なパーミッションからその値が削られていることになります。

umaskの値は `umask(2)` 関数で変更することができます。また `umask` コマンドでも変更が可能です。

詳しくは次のページを参考にしてください。

https://linuxjm.osdn.jp/html/LDP_man-pages/man2/umask.2.html

また、数字がどんな権限を表しているかは以下のページで確認できます。

https://kazmax.zpp.jp/linux_beginner/permission_numerical.html


# ディレクトリの削除

ディレクトリを削除するには `rmdir(2)` を使います。こちらもディレクトリの作成と同様、システムコールです。定義は次のとおりです。

``` c
int rmdir(const char *pathname);
```

関数の引数に削除したいディレクトリのパスを指定します。ディレクトリの中は空でなければいけません。もし、空でないディレクトリを削除するには中のファイルをすべて削除するか、移動させるかする必要があります。削除に失敗すると `-1` が返されます。

上記のプログラムでは以下のように使っています。

``` c
      if (rmdir(path)) {
        perror(path);
        exit(EXIT_FAILURE);
      }
```

この関数の詳しい情報はこちらを参考にしてください。

https://linuxjm.osdn.jp/html/LDP_man-pages/man2/rmdir.2.html


# ファイル入出力との比較

ディレクトリ操作とファイル操作について比較してみたいと思います。

|操作|ファイル|ディレクトリ|
|:---|:---|:---|
|開く|fopen|opendir|
|閉じる|fclose|closedir|
|読み込む|fgetc|readdir|
|読み込む基本単位|バイト|dirent構造体|
|書き込む|fputc|-|
|作成する|fopen|mkdir|
|表現する構造体|FILE|DIR|


# 参考文献

1. 書籍：　ふつうのLinuxプログラミング第２版　青木 峰郎著
1. Webサイト：　苦しんで覚えるC言語　（https://9cguide.appspot.com/ ）
1. Webサイト：　JM Project - Linux関連のマニュアルページの日本語版　（http://linuxjm.osdn.jp/ ）
1. Webサイト：　kazmax Linuxで自宅サーバー　（https://kazmax.zpp.jp/ ）