rcmdnk's blog
Last update

シェルスクリプト基本リファレンス

Bashでシェルスクリプトを書い居ると特に 適当に一時ファイルにコマンド結果を書き出して後で参照したりすることがよくありますが、 今まで適当にカレントディレクトリに.tmpとかのファイルを作ったりしてましたが、 今更ながらmktempというコマンドを知ったのでちょっと整理してみました。

これまでの一時ファイル

ざっとスクリプトを見たところ、今までにやったことがあるのは

  • 単純にf=/tmp/tmpfileなど、/tmpに適当なファイルを作る。
    • これだと違うプロセスから同じコマンドを使うと競合してしまう。
    • また、/tmp直下に置くことが良いのか、と言う問題。
    • f=/tmp/$USER/tmpfileとすればLinuxだとOKだが、Macだと 一時ディレクトリは別にランダムな文字列とともに用意されるので駄目。
    • f=$TMPDIR/tmpfileと環境変数を使うこともできるが、Cygwinでは この変数は用意されてない。 (この辺下を参照)
  • 単純にf=~/tmp/tmpfileなど、~/tmpに適当なファイルを作る。
    • 競合してしまうのは上と一緒。
    • 自分のHomeには必ず最初にtmpを作ってるので大丈夫、という環境依存。
      • たまに、最初にmkdir -p ~/tmpとかしてみる。
  • 単純にf=.tmpなど、カレントディレクトリに.で始まるファイルを作る
    • 違うディレクトリから呼んだ場合は競合を回避出来る。
    • 途中で終了シグナルを送られた場合の処理をきちんとしておかないと その場に一時ファイルが残ってしまう。
  • f=/tmp/tmpfile.$$など、プロセスIDを付与する。
    • 競合を回避出来る。

ということで、上の方だと結構問題があるものもありますが、 プロセスIDなんかを振ってやれば大分ましにはなると思います。

TMP/TEMDIR

Linuxとかだと、TMPDIRと言う環境変数が/tmp/$USERを指して居て、 ここを自分の一時ファイル置き場として使えます。 (リブート等のタイミングでクリアされる)

また、Macだと同じTMPDIRと言う環境変数に

/var/folders/XX/XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/X/

といった感じのランダムな文字列(Xの部分)で決められたディレクトリ (この場合は最後に/が付く)が 割り当てられていて、再起動のタイミング等で変わります。 (その代わり古いのも暫く残る。)

これらの値を使ってその配下に一時ファイルを作れば良いわけですが、 1つ問題があって、Cygwinではこの値が設定されていません。

その代わりCygwinにはTMP及びTEMPと言う値が/tmpとして設定されています。 これが結構面倒なので、.bashrcなんかでTMPDIRが無いときに 値を設定する様、次の様な設定を書いておくと便利かと。

.bashrc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# TMPDIR fix for Cygwin
if [ ! "$TMPDIR" ];then
  if [ "$TMP" ];then
    export TMPDIR=$TMP
  elif [ "$TEMP" ];then
    export TMPDIR=$TEMP
  elif [ -w /tmp/$USER ];then
    export TMPDIR=/tmp
  elif [ -w /tmp ];then
    mkdir -p /tmp/$USER
    export TMPDIR=/tmp/$USER
  else
    mkdir -p ~/tmp
    export TMPDIR=~/tmp
  fi
fi

elif [ -w /tmp/$USER ];then以下はちょっと冗長かもしれませんが、 念のため。

mktemp

こちらを見て知りました。

安全な一時ファイルの作成と削除の方法 - 拡張 POSIX シェルスクリプト Advent Calendar 2013 - ダメ出し Blog

結構当たり前なコマンドな感じなのに今まで人が作ったスクリプトで一回も見たことが無かった気がする。。。

適当なTMPディレクトリにランダムな文字列を使った一時ファイル(ディレクトリ)を 作ってその名前を返してくれるコマンドです。

上の様にTMPDIRを指定して、さらにプロセスIDなんかをファイル名に 与えてあげればほとんどの場合は回避出来ますが、 mktempはさらに自動的に700へパーミッションを指定することと、 シンボリックリンク攻撃の脆弱性を回避出来る、ということもあるみたいなので、 せっかく便利なコマンドですし、使っていこうと思います。

ただ、少し注意が必要なのが、GNUとBSDのコマンドで引数の定義が違います。

GNUだと、何も引数なしで実行すると、$TMPDIR/tmp.XXXXXXXX1 と言ったファイルが出来るのですが、BSDの場合は引数なしだと エラーになります。

追記: 2015/10/19

どうもOS X内のmktempコマンドにアップデートがあったらしく、 10.11 El Capitanでは 引数無しでもエラー無しに-t tmpを与えたのと同様の結果が得られます。

追記ここまで

同様のファイルを作るにはprefixを-tで与えて

$ mktemp -t tmp # BSD mktemp
/var/folders/XX/XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/X/tmp.XXXXXXXX

とします。ただ、ここでさらにやっかいなのが、 GNUの場合は-tを付けると、TMPDIRの中にその名前その物が作られます。

$ mktemp -t tmp # GNU mktemp
/tmp/user/tmp

従ってこれだと競合が回避出来ません。 (mktmepの場合だとファイルが存在するとエラーを返すのでここで止まります)

そこで、両方で使えるような移植性を実現するためにはこんな感じでmktempを呼びます。

1
tmpfile=$(mktemp 2>/dev/null||mktemp -t tmp)

最初にGNUだと仮定して、エラーになった場合にBSDだと思って-tでprefixを指定して 一時ファイルを作成します。

もしくはTMPDIRが正しく設定してあると仮定すれば

1
tmpfile=$(mktemp $TMPDIR/tmp.XXXXXXXX)

の様に、直接ファイル名を与えてあげると両方共同じ様に働くので 綺麗には出来ますし、tmpの部分も自由に替えられます。 この場合に、最後にXを付けると、その部分はXの数だけランダムな文字が 付け加えられます。(競合を避けるために複数個のXが必須。)

もしくはTMPDIRの問題を考えると、mktempを使う場合には単純に/tmpディレクトリのが良いかもしれません。

途中でスクリプトを止めた時の一時ファイルの処理

一時ファイルをスクリプトの最後で消すような処理をしている場合、 C-c等でスクリプトを止めた時にはそのままファイルが残ってしまいます。 これを回避するためには上で挙げたサイトにもありますが、

1
2
3
4
5
6
7
8
9
10
#!/bin/bash

unset tmpfile
trap '[[ "$tmpfile" ]] && rm -f $tmpfile' 1 2 3 15

tmpfile=$(mktemp 2>/dev/null||mktemp -t tmp)

# do something

rm -f $tmpfile

追記: 2015/11/19

trapの部分が以前はダブルクォートで囲ってましたが、 ダブルクォートしてしまうと、これを書いた時点で変数が評価されてしまいます

つまり最初にtmpfileの値が入ってないので削除が行われません。

この場合はシングルクォートが正しく、これだと実際に trapされた時に評価されます。

また$tmpfileの部分も以前はクォートされてませんでしたが、 [[ ]]の中身がなにもないとbash: syntax error near `]]’ なエラーが出ます。

ここでは中身が空白で無かったら、と言う事で使いたいので、 ダブルクォートで囲んで使います。

追記ここまで

この様にtrapを使って何らかの要因で止められた時に ファイルを削除する様にしておきます。

上でtrapに与えてる数字は送られるシグナルの番号でそれぞれ

  • 1 HUP: ハングアップ(sshで接続が切れる、等)
  • 2 INT: C-cやDelが押された時の割り込みシグナル
  • 3 QUIT: クイットシグナル
  • 15 TERM: killコマンドがデフォルトで送るシグナル

の時(これらの名前を与えてもOK)。 通常送られるのは上の4つくらいなのでこれで十分です。

killによる強制終了(kill killまたはkill -9)で送られる 9(KILL)はtrapでは捉えられません。

0(EXIT)は終了時に必ず送られるシグナルで、 上に挙げたシグナル(KILL以外)が送られても、その後にEXITが行われるので、

trap "echo exit" 0 1 2 3 15

みたいな設定をしておくと、C-c等で止めるとexitが2回出ます。

$tmpfileを必ず終了時に消して、特にチェックも必要なければ

trap "[[ $tmpfile ]] && rm -f $tmpfile" 0

と上のスクリプトを置き換えて、最後のrm文を消すことも出来ます。

Sponsored Links
  1. Xはランダムな文字列、TMPDIRが指定されてない場合は/tmpが使われます。 従ってCygwinの場合には丁度?TMP/TEMPと同じディレクトリを指す事になります。

Sponsored Links

« Octopressで外部リンクを別ウィンドウで開く様にする GNU screenでクリップボードの履歴を使えるようにする 3 »

}