シェルスクリプトにおける適切な一時ファイルの使い方

シェルスクリプトにおける適切な一時ファイルの使い方

シェバンブック私のキャリアの中で、シェル スクリプトで次のようなコードに定期的に遭遇しました。

 TEMP_FILE=/tmp/一時ファイル

あるいは、もう少しエレガントに言うと:

 TEMPFILE=/tmp/tempfile.$$

最初の例の問題点、特にそれが多くの異なるスクリプトで使用されている場合は明らかです。2番目の例の方が優れています。「$$」は「自分のプロセスID」を意味し、もしスクリプトのプロセスIDが5309であれば、TEMPFILE変数は/tmp/tempfile.5309に設定されます。これによりスクリプト間の衝突は極めて起こりにくくなりますが、それでも最適ではありません。/tmp/tempfile.5309というファイルがあり、それが別のユーザーの所有物だったり、/tmpへの書き込み権限がなかったりしたらどうなるでしょうか?何かを書こうとして数行も経ってから気づくよりも、すぐに気づく方が賢明です。

これがここでの核心的な考慮事項です。上記の例では、単に変数に値を代入しているだけです。一時ファイルが使用できるかどうかは保証されていません。本来であれば、何らかの方法で (1) 一時ファイル名を取得し、(2) (少なくとも作成された時点では)使用できることを保証する必要があります。幸いなことに、mktemp を使えばまさにそれが実現できます。

ルート@クラッシュ:~# mktemp
/tmp/tmp.OSN8Yv7RUj
root@crash:~# ls -l /tmp/tmp.OSN8Yv7RUj
-rw------- 1 ルート ルート 0 4月  9 10:33 /tmp/tmp.OSN8Yv7RUj
ルート@クラッシュ:~# 

mktemp(1)コマンドは、一意の一時ファイル名を選択し、それを開いて、呼び出し元に所有権などを設定し、その名前を返します。つまり、一時ファイルを選択する唯一の正しい方法は次のとおりです。

 TEMPFILE=$(mktemp)

mktemp には、別のディレクトリの使用、ファイルの代わりに一時ディレクトリの作成、ファイル使用時の命名パターンの指定など、多くの便利なオプションが用意されています。しかし、Unix の流儀に倣い、「naked」(引数なし)で呼び出された場合は、便利なデフォルト設定で主な機能を実行します。

しかし、まだ一つ問題があります。スクリプト作成者は一時ファイルを作成してクリーンアップを忘れるという悪癖があります。多くのユーザーが利用する大規模なUnixシステムにログインしてみると、おそらく/tmpに何百、何千もの一時ファイルが見つかるでしょう。それらは誰かがスクリプトで作成し、クリーンアップし忘れたものです。これは単なる無知による場合もあれば、プログラマの怠慢による場合もあります。プログラムが予期せぬロジックの分岐で終了し、最後に「クリーンアップ」が呼び出されなかったために発生する場合もあります。

作成されたすべての一時ファイルを自動的にクリーンアップする方法があったら便利だと思いませんか?つまり、プログラムがどのように終了しても、一時ファイルはクリーンアップされるということですか?

がある!

私は Bash 5.1 を使用していますが、思い出してみると、ここで示しているのは ksh88 かそれ以前のものだと思います。

これから見ていくのはトラップ機能です。まず、シグナルについて少し知っておく必要があります。プロセスにシグナルが送られると、例えば「kill <PID>」と入力すると、通常はそのシグナルのデフォルトの動作が実行されます。例えば、次のように入力すると、

 5309人を殺害

次に、プロセス5309にQUITシグナルが送信されます。シグナルに対して何も処理を行わない場合、デフォルトではプロセスは終了します。ただし、プロセスはこのシグナルに対するハンドラをインストールし、「待ってください。終了する前に、XとYを実行したいのですが」または「申し訳ありませんが、QUITは無視します」と伝えます。KILL (9)シグナルは処理も無視もできないことに注意してください。

プログラムに何かを指示するためにシグナルが使用されることがあります。例えば、namedにHUP(ハングアップ)シグナルを送信すると、namedは設定を再読み込みします。これは一般的な慣例です。未定義のシグナルが2つ(USR1とUSR2)あります。これらはプログラム内で自由に使用できます。

Bash トラップ (シェル組み込み) の構文は次のとおりです。

トラップ [何をするか] [どの信号で]

たとえば、Bash スクリプトが USR1 シグナルを処理して構成を再読み取りする方法の簡単な例を次に示します。

 #!/bin/bash

関数 reread_config() {
printf "設定を再読み込み中\n"
}

トラップ reread_config USR1

# 無限待機ループ
[ 1 ] ; 行う
睡眠1
終わり

そして実際に行動に移すと:

 root@crash:~# ./trap_example.sh &
[1] 3285
ルート@クラッシュ:~# キル -USR1 3285
root@crash:~# 設定を再読み込み中

ルート@クラッシュ:~#

キルシグナルを送信した後、1秒ほどの一時停止があったことに注意してください。Bashは「コマンド間」でのみシグナルハンドラを起動します。

さて、一時ファイルをクリーンアップするために、Bashの「偽のシグナル」であるEXITを使用することができます。Unixには「EXIT」シグナルはありませんが、Bashはスクリプト終了時にEXITというシグナルを発行します。これにより、必要に応じてフックを追加できます。つまり、ロジックが「exit」コマンドを呼び出した場合であれ、誰かがCtrl-Cを押した場合であれ、SIGQUITを送信した場合であれ、EXITハンドラは常に実行されます。唯一の例外は、誰かがkill -9(SIGKILL)を実行した場合です。これはトラップできず、プロセスを即座に停止します。

したがって、TEMP_FILE をクリーンアップするには、次の操作を実行するだけです。

トラップ "rm -f $TEMP_FILE" EXIT

例:

 #!/bin/bash

TEMPFILE=$(mktemp)
トラップ "rm $TEMPFILE" EXIT
printf "一時ファイルは%s:\nです" $TEMPFILE
ls -l $TEMPFILE

動作例:

ルート@クラッシュ:~# ./trap_example2.sh 
一時ファイルは /tmp/tmp.no5wPXRwfj です:
-rw------- 1 ルート ルート 0 4月  9 11:25 /tmp/tmp.no5wPXRwfj
root@crash:~# ls -l /tmp/tmp.no5wPXRwfj
ls: '/tmp/tmp.no5wPXRwfj' にアクセスできません: そのようなファイルまたはディレクトリはありません
ルート@クラッシュ:~#

トラップ文は呼び出された時点でTEMPFILEを評価することに注意してください。以下の例は動作しません。

 #!/bin/bash

トラップ "rm $TEMPFILE" EXIT
TEMPFILE=$(mktemp)
printf "一時ファイルは%s:\nです" $TEMPFILE
ls -l $TEMPFILE

問題は、trap ステートメントが評価されるときに TEMPFILE が空白なので、EXIT シグナル ハンドラーが「rm /tmp/tmp.xxxxx」ではなく「rm 」になることです。

しかし、もし多くの一時ファイルがある場合はどうすればよいでしょうか?解決策としては、一時ファイルに固有のパターンを使用し、ワイルドカードを使ってrmコマンドを発行することです。ただし、ワイルドカードとして二重引用符を使用する際は注意してください。二重引用符はtrap文の実行時ではなく、作成時に評価されます。そのため、常に一重引用符を使用することをお勧めします。

ここに 2 つの解決策があり、他にも解決策はあります。

まず、UUIDパターンを使って一時ファイルを作成します。そして、それらを削除する際には、そのUUIDに一致するワイルドカードを使ってrmコマンドを実行するだけです。誰かが同じUUIDを取得する可能性が5.3×10 36分の1しかない場合(まず起こりませんが)、パターンを使って削除できます。

例:

 #!/bin/bash

UUID=$(uuid -v 4)

TEMPFILE1=$(mktemp /tmp/${UUID}-XXX)
printf "TEMPFILE1は$TEMPFILE1です\n"
TEMPFILE2=$(mktemp /tmp/${UUID}-XXX)
printf "TEMPFILE2は$TEMPFILE2です\n"
TEMPFILE3=$(mktemp /tmp/${UUID}-XXX)
printf "TEMPFILE3は$TEMPFILE3です\n"

トラップ "rm -f /tmp/${UUID}*" 終了

動作例:

ルート@クラッシュ:~# ./trap_example3.sh 
TEMPFILE1は/tmp/62d7075d-d837-4327-8ae6-2bed81e1ad64-SsRです
TEMPFILE2 は /tmp/62d7075d-d837-4327-8ae6-2bed81e1ad64-fRv です
TEMPFILE3は/tmp/62d7075d-d837-4327-8ae6-2bed81e1ad64-bJcです
ルート@クラッシュ:~# ls -l /tmp/62d7075d-d837-4327-8ae6-2bed81e1ad64*
ls: '/tmp/62d7075d-d837-4327-8ae6-2bed81e1ad64*' にアクセスできません: そのようなファイルまたはディレクトリはありません
ルート@クラッシュ:~#

別の方法としては、一時ファイルに配列を使用し、それをクリーンアップする方法があります。例:

 #!/bin/bash 

-a TEMPFILES を宣言する

TEMPFILES[1]=$(mktemp)
TEMPFILES[2]=$(mktemp)
TEMPFILES[3]=$(mktemp)
TEMPFILES[4]=$(mktemp)
TEMPFILES[5]=$(mktemp)

i が 1 2 3 4 5 の場合; 実行する
printf "一時ファイル %d は %s:\n" $i ${TEMPFILES[i]}
ls -l ${TEMPFILES[i]}
終わり

trap 'for tempfile in "${TEMPFILES[@]}" ; do rm -f $tempfile ; done' EXIT

動作例:

ルート@クラッシュ:~# ./trap_example4.sh 
一時ファイル 1 は /tmp/tmp.DwuPExLh3B です:
-rw------- 1 ルート ルート 0 4月  9 11:49 /tmp/tmp.DwuPExLh3B
一時ファイル2は/tmp/tmp.9qiJxzLxCCです:
-rw------- 1 ルート ルート 0 4月  9 11:49 /tmp/tmp.9qiJxzLxCC
一時ファイル3は/tmp/tmp.RzWwthPXx9です:
-rw------- 1 ルート ルート 0 4月  9 11:49 /tmp/tmp.RzWwthPXx9
一時ファイル4は/tmp/tmp.u4RFSQfORYです:
-rw------- 1 ルート ルート 0 4月  9 11:49 /tmp/tmp.u4RFSQfORY
一時ファイル5は/tmp/tmp.D4HbvM9egSです:
-rw------- 1 ルート ルート 0 4月  9 11:49 /tmp/tmp.D4HbvM9egS
root@crash:~# ls -l /tmp/tmp*
ls: '/tmp/tmp*' にアクセスできません: そのようなファイルまたはディレクトリはありません
ルート@クラッシュ:~#

他にも解決策はあります。例えば、関数を使って一時ファイルのリストを管理し、そのリストを参照するクリーンアップ関数を呼び出すといった方法です。実装については、Linux Journalの記事をご覧ください。

おすすめの記事