Help us understand the problem. What is going on with this article?

一時ファイル無しで、ファイルを上書き更新するシェルスクリプト

More than 5 years have passed since last update.

どんなコマンドでも上書き更新OK

シェルスクリプトでは通常、(追記ではなく)ファイルの中身を書き換えたいと思ったら一時ファイルに新しい中身を書き出してから、元のファイルに上書きしてやるしかない。

単独のsedコマンドやnkfコマンドで済む場合は、それらのコマンドが個別に対応している場合もあるが、そういうのは運がよい場合だけ。どんなコマンドでもできるようにはならないものか?

→なります

こたえ

更新のために通したいコマンドが、CMD1、さらにCMD2、……、であって、更新したいファイルパスが$fileの中に格納されていたとすると、次のコードを書けば実現できる。

(rm "$file" && CMD1 | CMD2 | ... > "file") < "$file"

ただし、使用上の注意があるから、最後まで読むこと!

なぜこれでうまくいくのか?

UNIXにおいて、rmコマンドなどによる削除(unlink)は、ファイルの実体を消すのではなくinodeと呼ばれる見出しを消すだけであることは御存知のとおり。つまり、ファイルが直ちに消滅するわけでなない。

もし、誰かがファイルをオープンしている途中に削除したらどうなるかというと、以後は誰もファイルを二度とオープンできなくなるものの、そのファイルを既にオープンしているプロセスはクローズするまで使い続けることができる。

つまり、ファイルをオープンして中身を読み出している間にそのファイルを削除をしてしまっても、読み出しは最後まで行えるのだ。そこですかさず読み出されたデータを受け取って好きな加工を施した上で、同名(2代目)のファイルを新規作成する。

同名のファイルなど作れないように思うが、初代のファイル実体は既にinodeを失っているために無名である。よって2代目ファイルを全く同じ名前で作ることが可能ということなのだ。

使用上の注意

今の解説を読めば想像付くことだが、初代ファイルと2代目ファイルは別人である(inode番号が異なる)。するとどうしてもこの技には制約が生じる。

1. シンボリックリンクなら実体を探してから使うこと

もし、先程のコードをシンボリックリンクに対してやったらどうなるか……。そう、リンク元の実体はそのまま残り、リンクが切れ、リンクが実体化してしまう。

こうならないためには、readlinkコマンドでシンボリックリンクであった場合に実体パスを調べておかねばならない。

file=$(readlink -f "$file")
(rm "$file" && CMD1 | CMD2 | ... > "file") < "$file"

もっとも私はPOSIX原理主義者なので、readlinkなどに頼らず自力で探すのだがな。

while :; do
  # 1) lsコマンドで、属性とファイルサイズを取ってくる
  s=$(ls -adl "$file" 2>/dev/null) || {
    printf '%s\n' "${0##*/}: cannot open \`$file' (Permission denied)" 1>&2
    exit 1
  }
  # 2) lsが返した文字列を要素毎に分割
  set -- $s # 属性は$1に格納、ファイルサイズは$5に格納される
  # 3) 実ファイルを見つけるためのループ
  #    ・そもそも見つからなければエラー終了
  #    ・通常ファイルならそのファイル名でループ終了
  #    ・リンクならリンク元のパスを調べて再度ループ
  #    ・それ以外のファイルならエラー終了
  case "$1" in
    '') printf '%s: %s: No such file or directory\n' "${0##*/}" "$file" 1>&2
        exit 1
        ;;
    -*) break
        ;;
    l*) s=$(printf '%s' "$file"         |
            sed 's/\([].\*/[]\)/\\\1/g' |
            sed 's/^\^/\\^/'            |
            sed 's/\$$/\\$/'            )
        srcfile=$(file "$file"                              |
                  sed 's/^.\{'"$s"'\}: symbolic link to //' |
                  sed 's/^`\(.*\)'"'"'$/\1/'                )
        case "$srcfile" in                 # fileコマンドが↑の書式で
          /*) file=$srcfile             ;; # 返してくることは
           *) file="${file%/*}/$srcfile";; # POSIXで保証されている
        esac
        continue
        ;;
     *) printf '%s\n' "${0##*/}: \`$file' is not a regular file." 1>&2
        exit 1
        ;;
  esac
done
(rm "$file" && CMD1 | CMD2 | ... > "file") < "$file"

2.パーミッションが違う、またACL付なら自力で復元せよ

ファイルのパーミッションやACLは2代目には引き継がれない。よって引き継ぎたいなら初代のものを覚えておき、後で復元するしかない。

file=$(readlink -f "$file")

# パーミッションと、ACL情報(あれば)の保存
perm=$(ls -adl "$file" | awk '{print substr($0,1,11)}')
case "$perm" in
  [^-]*) echo "Not a regular file" 1>&2;exit 1;;
   *'+') acl=$(getfacl "$file")               ;;
      *) acl=''                               ;;
esac

# パーミッションをchmodで使える形式(4桁ゼロ埋め8進数)に変換
mode=$(echo "$perm"                                                     |
       sed 's/.\(..\)\(.\)\(..\)\(.\)\(..\)\(.\)./\2\4\6 \1\2\3\4\5\6/' |
       awk '{gsub(/[x-]/,"0",$1);gsub(/[^0]/,"1",$1);print $1 $2;}'     |
       tr 'STrwxst-' '00111110'                                         |
       xargs printf 'ibase=2;%s\n'                                      |
       bc                                                               |
       xargs printf '%04o'                                              )

# ここで、一時ファイルを作らず上書き更新
(rm "$file" && CMD1 | CMD2 | ... > "file") < "$file"

# パーミッション・ACL(あれば)の復元
chmod $mode "$file"
case "$acl" in '') :;; *) printf '%s' "$acl" | setfacl -M - "$file";; esac

もっとも私はPOSIX原理主義者なので、readlinkなど(ry

3.ハードリンクが他にあったり、所有者が他人だったら

ハードリンクが他にあるというのは、ls -lなどとやった時に2列目の数字が1より大きい状態。所有者が他人というのは、例えば対象ファイルもしくは対象ファイルの置かれているディレクトリーが自分のものではないのに、パーミッションが666になっていて読み書きできる状態。
これらの場合は諦めよ! そして一時ファイルを経由せよ。

他のハードリンクを調べる簡単な方法はないし、一般権限ユーザーが所有者を復元する方法はない。

参考

シェルスクリプトを極める by bsdhackさん

richmikan@github
困った時はすぐQiitaを始めとしたTipsを表面的に鵜呑みにし、使えそうなプロダクトを拾ってきてマニュアル通りに対応するプロダクト至上主義者達よ。そんなことでは、想定外の事態に見舞われた時すぐ死ぬぞ。想定外とは、マニュアルには載ってないから想定外なのだ。マニュアル通りにしか動けぬ者は、典型的なコンビニ店員の如く薄給に喘ぐだけ。頭を使え!UNIX哲学に目覚めよ!そしてPOSIX原理主義を崇拝せよ!
https://github.com/ShellShoccar-jpn
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした