mktempの作法とPOSIX互換な自前実装
そもそも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")
参考文献
- POSIX準拠シェルスクリプト用の必要十分で短いmktempシェル関数
- mktemp(1) もどきを作ってみた
- Generate Random String in Bash (Linux)
- grepの-oオプションと-Pオプションの組み合わせが便利
- POSIX sedとGNU sedの両方で動かすテクニック4種盛り
- bashの似てて紛らわしいもの() / {} について
おまけ
GNU版のmktemp
の完全なクローンをPOSIX準拠なシェルスクリプトだけで書く、というのも結構なネタになりそうなので気が向いたら記事を書きます。
私がシェルスクリプトでコマンドラインツールを書く場合に気を使っていることや、引数の自前解析などもまとめたいですね。
Discussion