【Linux】touchコマンドのソースコードを解説してみる

エンジニアの方は開発している時にLinuxコマンドをよく使っていると思います。
そんななかで、touchコマンドのソースコードを読む機会があったので解説してみたいと思います。ただし、システムコールの内部までは深堀しませんのでカーネルガチ勢の方がいたらすみません:bow:
ここの意味間違っているとか、私はこういう解釈をしたとかあったら、気軽にコメントください:pray:

では、いってみよう:bicyclist:

# touchコマンドとは？
touchコマンドの解説に入る前に、そもそもtouchコマンドとは何かを説明したいと思います。
touchコマンドのマニュアルを参照すると以下のような説明があります。
> The touch utility sets the modification and access times of files.  If any file does not exist, it is created with default permissions.

日本語意訳すると、touchコマンドはファイルの変更(更新)やアクセスした時間を設定するためのコマンドです。また、指定したファイルが存在していなかったら、デフォルトのパーミッションを付与してファイルを新規作成します。
つまり何が言いたいかというと、touchコマンドの主たる機能はファイルの変更・アクセス時間の設定です。
多くの方はtouchコマンドはファイルを新規作成するだけのコマンドであると思われているかもしれません。この後の解説を読む際は、このようなバックグランドがあることを頭に入れながら読み進めてください。

# 解説
touchコマンドのソースコードは[こちら](https://github.com/coreutils/coreutils/blob/master/src/touch.c)でゲットできます。
解説する際にヘッダーのインクルードやコマンドの使い方(Help)のコードは省略します。あと簡単に理解できると思われる箇所はすっ飛ばしますのでご了承ください。無駄に長くなるので...
解説方法ですが、ソースコード中にコメントとして説明を加えていきます。個人的にそのほうがソースコードを読みながら理解できると思ったので...


## touch.c
touch.cに書かれているコアな***touch関数***について解説します。main関数はオプションを受け取ってその値がちゃんとした値かチェックしたり、整形したりしてからtouch関数を呼び出しているだけなので特に解説しません。

```touch.c
/* touch関数はbooleanを返します。
   引数にはchar型のポインタを受け取ります。
   この引数の文字列がファイル名となります。*/
static bool
touch (const char *file)
{
  bool ok;
  int fd = -1;
  int open_errno = 0;
  struct timespec const *t = newtime;

  if (STREQ (file, "-"))
    /* 引数のファイル名が"-"であれば、
       fd(ファイルディスクリプター)はSTDOUT_FILENO = 1となります*/
    fd = STDOUT_FILENO;
  else if (! (no_create || no_dereference))
  /* ファイルを作成しないオプション、 あるいはシンボリックリンクの
     アクセスタイムを変更するオプションが指定されていなければ、
     以下の処理を実行 */
    {
      /* fd_reopen関数の詳細は後述します。
         簡潔に説明すると、open(file, O_WRONLY | O_CREAT | O_NONBLOCK | O_NOCTTY, MODE_RW_UGO)の
         返り値がSTDIN_FILENO = 0、あるいは0より小さければopen()の返り値をそのまま返します。
         それ以外であれば、openされたファイルをコピーしてfd値がSTDIN_FILENOとなるように作り直して
         新たなfd値(STDIN_FILENO = 0)を返します。

         O_WRONLY:      書き込みのみ
         O_CREATE:      ファイルが存在しなければ作成する
         O_NONBLOCK:    ノンブロッキングモードでファイルを開く
         O_NOCTTY:      ファイル名がTTY(ターミナルデバイス)であっても、
                        オープンしたファイルは制御用のターミナルデバイスにはならない
         MODE_RW_UGO:   ファイルのパーミッションを666に設定
                        (S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH)
                        umaskが設定されている場合は新規作成されたファイルのパーミッションは
                        必ずしも666にはならない
                        初期設定のままならumask = 0022になっているのでパーミッションは644になるだろう */
      fd = fd_reopen (STDIN_FILENO, file,
                      O_WRONLY | O_CREAT | O_NONBLOCK | O_NOCTTY, MODE_RW_UGO);

      /* fd_reopenの返り値が-1、かつエラーの内容がディレクトリである(EISDIR)、無効な引数である(EINVAL)、
         権限がない(EPERM)からでない時に処理を実行する */
      if (fd == -1 && errno != EISDIR && errno != EINVAL && errno != EPERM)
        open_errno = errno;
    }

  if (change_times != (CH_ATIME | CH_MTIME))
  /* -aと-m オプションが両方指定されていなければ以下の処理を実行 */
    {
      if (change_times == CH_MTIME)
        /* -mオプションが指定された場合はアクセスタイムは変更しない  */
        newtime[0].tv_nsec = UTIME_OMIT;
      else
        {
          /* -aオプションが指定された場合は更新タイムは変更しない  */
          assert (change_times == CH_ATIME);
          newtime[1].tv_nsec = UTIME_OMIT;
        }
    }

  if (amtime_now)
  /* アクセスと更新時間の両方を変更する場合に以下の処理を実行
     -aと-mオプションが指定されていなければamtime_nowは
     trueに設定されているのでtは必ずNULLになる */
    {
      t = NULL;
    }

  /* fdutimensatはfdのアクセス・更新時間をセットするための関数である。
     返り値は成功時は0、失敗時は-1となる。
     第4引数であるtがNULLの場合fdのアクセス・更新時間は現在時刻が設定される。
     AT_FDCWD:            カレントワーキングディレクトリのfd値
     RT_SYMLINK_NOFOLLOW: シンボリックリンクの情報を返すためのフラグ */
  ok = (fdutimensat (fd, AT_FDCWD, (fd == STDOUT_FILENO ? NULL : file), t,
                     (no_dereference && fd == -1) ? AT_SYMLINK_NOFOLLOW : 0)
        == 0);

  if (fd == STDIN_FILENO)
    {
      if (close (STDIN_FILENO) != 0)
        {
          error (0, errno, _("failed to close %s"), quoteaf (file));
          return false;
        }
    }
  else if (fd == STDOUT_FILENO)
    {
      /* Do not diagnose "touch -c - >&-".  */
      if (!ok && errno == EBADF && no_create)
        return true;
    }

  if (!ok)
    {
      if (open_errno)
        {
          /* The wording of this diagnostic should cover at least two cases:
             - the file does not exist, but the parent directory is unwritable
             - the file exists, but it isn't writable
             I think it's not worth trying to distinguish them.  */
          error (0, open_errno, _("cannot touch %s"), quoteaf (file));
        }
      else
        {
          if (no_create && errno == ENOENT)
            return true;
          error (0, errno, _("setting times of %s"), quoteaf (file));
        }
      return false;
    }

  return true;
}
```

## fd_reopen.c
touch関数でファイルを作成するための***fd_reopen関数***の説明をします。

```fd_open.c
/* fd_reopen関数はファイルディスクリプターの値を返します。これは必ずint型になります。
   一般的なopenのシステムコールとは違い、必ず返り値が第一引数のdesired_fd値になるように
   処理を行います。
   touchコマンドから呼ばれるときはdesired_fd = STDIN_FILENOi = 0となります */
int
fd_reopen (int desired_fd, char const *file, int flags, mode_t mode)
{
  /* openのシステムコールを呼び出してファイルをオープンします。
     touchコマンドではファイルが存在しなければ、この段階でファイルが新規作成されます。
     もちろん成功すればの話ですが... */
  int fd = open (file, flags, mode);

  if (fd == desired_fd || fd < 0)
    /* openの返り値のファイルディスクリプターの値がdesired_fdと同じか、
       0より小さいときはfdを返します。*/
    return fd;
  else
    {
      /* ファイルのfd値がdesired_fdになるように作り直します。*/
      int fd2 = dup2 (fd, desired_fd);
      int saved_errno = errno;
      close (fd);
      errno = saved_errno;
      return fd2;
    }
}
```

# おわりに
いや〜お疲れさまでした。
これでtouchコマンドの内部に関して以前より理解が深まったと思います。

また機会があったらシステムコールの世界に踏み込んでみようと思います:pick:

# 参考文献
- [Man page of OPEN](https://linuxjm.osdn.jp/html/LDP_man-pages/man2/open.2.html)