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

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

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

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

**→なります**

# こたえ

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

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

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

# なぜこれでうまくいくのか？

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

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

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

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

# 使用上の注意

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

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

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

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

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

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

```sh:
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代目には引き継がれない。よって引き継ぎたいなら初代のものを覚えておき、後で復元するしかない。

```sh:
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になっていて読み書きできる状態。
**これらの場合は諦めよ！** そして一時ファイルを経由せよ。

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

# 参考

[シェルスクリプトを極める](http://sssslide.com/www.slideshare.net/bsdhack/ss-43064758) by [bsdhack](http://qiita.com/bsdhack)さん