📑

mktempの作法とPOSIX互換な自前実装

2023/08/14に公開

そもそもmktempとは

mktempコマンドは一時ファイルやディレクトリを作成するコマンドです。

環境変数TMPDIRに基づいて、重複しないようにランダムな名前のファイルやディレクトリを作成してくれます。

非常に便利ですが、使い所や使い方を間違えると色々面倒にもなります。

mktempの基本的な使い方はUbuntuのmanページを参照して下さい。

一時ファイルを使わなくていい場面

ディスクIOは遅くなる原因なので極力一時ファイルを使うべきではありません。(tmpfsの場合どうなんだろうか...)

もし配列を利用可能なBashを用いている場合、ファイルにデータを保存するのではなくreadarrayなどを用いることができます。

POSIX Shellであっても改行周りが面倒ですが、jsonなどは変数に代入することもできます。

備考

データが非常に大きい場合、配列や変数に入れてしまうとメモリを大きく消費してしまう可能性があるので、一時ファイルに書き込んだほうがいいかもしれません。

(でもtmpfsって結局メモリだからそれも変わらないのかもしれない...)

mktempの作法

個人的な思想が強いですが、mktempは使い方を間違えるとディスクが汚くなる気がします。

基本のコード

作成されたファイル/ディレクトリのパスが変数に代入されます。

# フォルダ作成
tmpdir="$(mktemp -d)"

# ファイル作成
tmpfile="$(mktemp)"

異常終了時の処理

シェルスクリプトが異常終了した際に一時ファイルを自動で消去します。

このコードはデバッグが難しくなるので、開発中は無効化しておくことをおすすめします。

trap 'rm -rf "${tmpdir}" "${tmpfile}"' 1 2 3 15

テンプレートの設定

mktempはデフォルトでランダムな文字列を生成しファイル名とします。

しかし、完全にランダムなファイル名はデバッグを困難にし、ディレクトリが異常に汚くなることにも繋がります。

そこで、ファイル名の一時にアプリ名をつけることで他のファイルを区別します。

テンプレートと呼ばれるこの機能は一時ファイルの名前をある程度指定できる機能です。

上記のようにXがランダムな文字列に置き換えられます。Xは少なくとも3文字は指定する必要があります。

個人的に好みではない実装

こういうスクリプトを何度か見たことがありますが、あまり好きではないです。

#!/bin/sh
cd "$(mktemp -d)"
touch myfile
echo hogehoge > myfile
git commit -m "add file"

このスクリプトの問題点をいくつか指摘します。

mktempの実行結果を直接コマンドに渡さない

cdコマンドに直接mktempのパスを渡すと、前の項で説明したtrapによる例外処理を実装できません。

ディレクトリが汚れるのが気にならない人は良いのかもしれませんが...

グローバルにcdコマンドを実行しない

そもそもシェルスクリプトの先頭でcdコマンドを実行するべきではありません。

パスは絶対コマンドで指定できますし、パス名が長いのなら変数を用いるべきです。

gitなどのカレントディレクトリに依存するコマンドでも、作業ディレクトリを指定するオプションはあります。

どうしても作業ディレクトリを変更する必要がある場合にはサブシェルを用いるべきです。

シェルスクリプトで、複数コマンドを()で囲むと別のプロセスで実行されます。

サブシェルでは名前空間が別個になるので、サブシェル内で変更されたカレントディレクトリや変数は元のスクリプトに影響を与えません。

後述するmktempの自前実装でも、変数がスクリプト全体に影響を与えることがないようにサブシェルを用いています。

いっそのこと自前で実装する

そもそもmktempはPOSIXではないので本気でシェルスクリプトを書く場合は自前で実装したほうがいいかもしれません。

そこでPOSIXに準拠したシェルスクリプトで簡単に実装してみました。

make_tmpfile(){
    (
        __template="${1-"tmp.XXXXXXXX"}"
        __tmpname="$(echo "$__template" | sed -E "s/X+/$(LC_CTYPE=C tr -dc A-Za-z0-9 < /dev/urandom | head -c "$(echo "${__template}" | grep -o "X" | wc -l)")/")"
        __tmpdir="${TMPDIR:-"/tmp"}"
        mkdir -p "${__tmpdir}/"
        printf "" > "${__tmpdir}/${__tmpname}"
        printf "%s\n" "${__tmpdir%/}/${__tmpname}"
    )
}

make_tmpdir(){
    (
        __template="${1-"tmp.XXXXXXXX"}"
        __tmpdir="${TMPDIR:-"/tmp"}/$(echo "$__template" | sed -E "s/X+/$(LC_CTYPE=C tr -dc A-Za-z0-9 < /dev/urandom | head -c "$(echo "${__template}" | grep -o "X" | wc -l)")/")"
        mkdir -p "${__tmpdir}/"
        printf "%s\n" "${__tmpdir%/}"
    )
}

ディレクトリ用とファイル用で関数を分けていますが、十分実用的なものです。

ネットで調べてみると先人にも何人かシェルスクリプトでmktempもどきを実装した例はありました。

しかしPOSIXに準拠していなかったり、テンプレート機能をサポートしていなかったりと問題もあったので自力で自走してみました。

使い方

使い方は非常に簡単です。

tempfile=$(make_tmpfile) #引数なし
tempfile_2=$(make_tmpfile "hogehoge-XXXXXX") # テンプレート

tempdir="$(make_tmpdir)"
tempdir_2=$(make_tmpdir "hogedir-"XXXXXX")

参考文献

おまけ

GNU版のmktempの完全なクローンをPOSIX準拠なシェルスクリプトだけで書く、というのも結構なネタになりそうなので気が向いたら記事を書きます。

私がシェルスクリプトでコマンドラインツールを書く場合に気を使っていることや、引数の自前解析などもまとめたいですね。

Discussion