POSIX準拠シェルスクリプト用の必要十分で短いmktempシェル関数

## はじめに

一般的に一時ファイルやディレクトリを作成するには `mktemp` コマンドを使用します。しかしこのコマンドは POSIX 準拠ではないためどの環境でも使えるとは限りません。POSIX 準拠で実装されたものとしては [こちら](https://github.com/ShellShoccar-jpn/misc-tools/blob/master/mktemp) などがありますが `mktemp` コマンドの互換コマンドとして作られているようでシェルスクリプトの中で使うには過剰です。私が欲しいのは最小のコードで目的を達成できる正しく動作する POSIX 準拠のシェル関数です。ということで作りました。（なお記事のタイトルは `mktemp` としていますが検索性をあげるためにそうしただけで、作成するシェル関数は名前も違いますし互換にもしていません。）

## 仕様

まず必要な機能、いらない機能を明確にします。

* 必要な機能
  * 作成ディレクトリの指定（または環境変数 `TMPDIR` or `/tmp`）
  * プリフィックス・サフィックス指定
  * 適切なパーミッション
  * すでにファイルがある場合のエラー処理
* 不要な機能
  * 一時ディレクトリ・一時ファイル作成処理の共通化はしない
  * コマンドと同じような `-` で始まるオプション引数は使わない
  * ランダムなファイル名にはしない
  * リトライ処理はしない
  * 過剰なエラー処理は行わない

## 実装

長い解説で読む気をなくしてしまいそうなので先に実装を提示します。引数の意味は `make_tempfile [プリフィックス] [サフィックス] [作成ディレクトリ]` です。 

```sh
# 一時ファイル
make_tempfile() {
  (
    now=$(date +'%Y%m%d%H%M%S') || return $?
    file="${3:-${TMPDIR:-/tmp}}/${1:-}$now-$$${2:-}"
    umask 0077
    set -C
    : > "$file" || return $?
    echo "$file"
  )
}

# 一時ディレクトリ
make_tempdir() {
  (
    now=$(date +'%Y%m%d%H%M%S') || return $?
    file="${3:-${TMPDIR:-/tmp}}/${1:-}$now-$$${2:-}"
    umask 0077
    mkdir "$file" || return $?
    echo "$file"
  )
}
```

これだけです。簡単ですね？

## 解説

### 必要な機能

#### 作成ディレクトリの指定（または環境変数 `TMPDIR` or `/tmp`）

一時ファイル・ディレクトリを作成するディレクトリを指定できるようにします。またディレクトリが指定されていない場合は `mktemp` の仕様にあわせて、環境変数 `TMPDIR` が存在する場合はそのディレクトリ、存在しない場合は `/tmp` に作成するようにします。

#### プリフィックス・サフィックス指定

機能的には必須と言うわけではないですが、これがあるとファイル名を区別しやすくなってデバッグなどが楽になるので実装します。サフィックスにも対応すると引数が増えてしまうので少し悩んだのですが拡張子によって異なる処理をするプログラムが存在し得るので対応することにしました。コード的には数文字増えるだけです。

#### 適切なパーミッション

`/tmp` ディレクトリという共通の場所に作成するので他のユーザーから中身を見られないようにするためにパーミッションは 600（ファイル） または 700 (ディレクトリ） でなければいけません。セキュリティ関連なので実装しないという選択肢はありえません。基本機能の一つです。これを実現するために `umask` を使用しています。

ちなみに、ファイル作成 → `chmod` コマンドによる変更 だとファイル作成から `chmod` コマンド実行までに僅かな時間があるためセキュリティ的には良くない実装です。

#### すでにファイルがある場合のエラー処理

これも当然の機能です。すでに誰かが作成したファイルを上書きしたり、他の人が作成したファイルを使ってはなりません。特に後者だと予め作成された不適切なファイルによりセキュリティ上の問題が発生する可能性があります。

### 不要な機能

#### 一時ディレクトリ・一時ファイル作成処理の共通化はしない

関数名は `make_tempfile` もしくは `make_tempdir` とします。なぜなら一つのスクリプトで両方の関数を使用することはないとあまり思うからです。一時ファイルを一つだけ使用するのであれば `make_tempfile` を使いますし、複数の一時ファイルが必要なのであれば `make_tempdir` でディレクトリを作成しその中では固定のファイル名や連番などで作成すれば事足りると思います。

#### コマンドと同じような `-` で始まるオプション引数は使わない

シェル関数でもコマンドと同じような `-` で始まるオプション引数を採用している場合を時々見かけますが私は推奨しません。オプション引数の解析でコードが長くなってしまいます。（`getopts`を使った所で簡単にはなりません。）

シェル関数は "関数" です。コマンドは汎用性を持たせるために高機能になっていますが、シェル関数は内部で使うだけなのでそのような汎用性は不要です。他の言語の "関数" と同様に引数の位置で判断したり、もっとパースしやすい形の引数にするのをおすすめします。

#### ランダムなファイル名にはしない

ファイル名は一意であればランダムである必要はありません。今回は「日時＋プロセスID」とします。同じ日時を繰り返さない限り1秒をすぎれば必ず別になりますし、プロセスIDが含まれているので、別のプロセスとかぶることもありません。1 秒の間に同じプロセスが何度も一時ファイルを作成するのであれば、一時ファイルの代わりに一時ディレクトリを一つだけ作りましょう。その中に連番でファイルを作成すればかぶることはありません。プロセスIDは最大値（`/proc/sys/kernel/pid_max` の値、おそらく 32768）を超えるとまた 1 からに戻りますが、1 秒の間に 1巡してしまうこともまずないでしょう。理屈の上では名前がかぶることはありえますが、実際にかぶることはまずありません。

どうしてもランダムな名前にしたい場合は、こちらを参照して下さい。「[POSIX準拠のシェルスクリプトで乱数を計算する（Xorshift32実装）](https://qiita.com/ko1nksm/items/3f4a90ad32224a2f7d56)」呼び出すたびに別の値を返す関数なので短時間で大量にファイルを生成しても問題ありません。

#### リトライ処理はしない

名前がかぶる可能性は低いのでリトライ処理も実装しません。もしどうしても必要なら呼び出し元でやれば十分です。（もちろんシェル関数を修正しても構いません。）ただし「すでにファイルがある場合のエラー処理」で書いた通り、作成しようとしている一時ファイル・ディレクトリがすでに存在していた場合はエラーにします。

#### 過剰なエラー処理は行わない

エラー処理をしないという意味ではありません。必要なことだけを正しく実装します。エラーメッセージなどは各コマンドが丁寧に出力するのでそれをそのまま使います。本来のエラーメッセージを握りつぶしたり独自のエラーメッセージに置き換えたりしないと言うだけのことです。必要ならば関数呼び出し側で実装すればよいです。

#### 実装の補足

シェル関数内のコード全体をサブシェル `( )` で括っているのでは、`umask` または `set -C` の変更を元に戻すためです。また使用している変数がサブシェルに閉じ込められるのでローカル変数相当となるという効果もあります。`make_tempfile() { (・・・) } ` という形ではなく関数定義そのものを `make_tempfile() (・・・)` とサブシェルとして定義することもできます。またこの関数は `file=$(make_tempfile)` などという形でどちらにしろコマンド置換（サブシェル相当）で呼び出すのが主な使い方となるので、わかっているならばシェル関数内のサブシェルを省いても構いません。

### 少しだけ注意

busybox バージョン 1.20.0 未満の 64bit 版には busybox 内蔵の `mkdir` で作成されるパーミッションがフルアクセス（全フラグON）になる[バグ](https://bugs.busybox.net/show_bug.cgi?id=4814)があるので注意して下さい。もっとも2012年頃、Debian 6.0（2016年サポート終了）ぐらいの話なので今は気にする必要はないと思いますが。（ちなみに私は 一時ディレクトリを作成したのち、意図せず SUID がついていたら `chmod` で変更してディレクトリの中が空であるか確認する対応を入れました・・・）