インフィニットループ 技術ブログ

2016年02月26日 (金)

著者 : mizuno_as

sedがシンボリックリンクを破壊する話

こんにちは、mizuno_asです。
sedは便利ですよね。自動化する際や手順書を作る際、人間が対話的にやらなければならない操作をコマンドで自動化できるのはすばらしいです。
sedには入力元ファイルを上書きする-iオプションが存在します。みなさんも設定ファイルをコマンドラインから書き換えるような際に、よく使うのではないでしょうか。ところがsedの-iオプションはノーケアで使うと、シンボリックリンクを破壊することはご存知でしょうか?
hammer-sledgehammer-mallet-tool-striking-hitting
Photo via Visualhunt

例として以下のように、テキストファイルにシンボリックリンクを張っておきます。

$ echo hoge > test.txt
$ ln -s test.txt link
$ ls -l
合計 4
lrwxrwxrwx 1 h-mizuno h-mizuno 8  1月 15 11:57 link -> test.txt
-rw-rw-r-- 1 h-mizuno h-mizuno 5  1月 15 11:57 test.txt

この状態で置換をかけてみます。

$ sed -i -e 's/hoge/fuga/' link

はい、この通りです。

$ ls -l
合計 8
-rw-rw-r-- 1 h-mizuno h-mizuno 5  1月 15 12:00 link
-rw-rw-r-- 1 h-mizuno h-mizuno 5  1月 15 11:57 test.txt
$ cat link
fuga
$ cat test.txt
hoge

なぜこのようなことが起こるのでしょうか? straceで、sedが呼び出しているシステムコールを追ってみましょう。

$ strace sed -i -e 's/hoge/fuga/' link
execve("/bin/sed", ["sed", "-i", "-e", "s/hoge/fuga/", "link"], [/* 66 vars */]) = 0
(...略...)
open("link", O_RDONLY)                  = 3
ioctl(3, SNDCTL_TMR_TIMEBASE or SNDRV_TIMER_IOCTL_NEXT_DEVICE or TCGETS, 0x7ffe518240b0) = -1 ENOTTY (Inappropriate ioctl for device)
fstat(3, {st_mode=S_IFREG|0664, st_size=5, ...}) = 0
umask(0700)                             = 02
open("./sedairHHD", O_RDWR|O_CREAT|O_EXCL, 0600) = 4
umask(02)                               = 0700
fcntl(4, F_GETFL)                       = 0x8002 (flags O_RDWR|O_LARGEFILE)
fstat(3, {st_mode=S_IFREG|0664, st_size=5, ...}) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f98e28ae000
read(3, "hoge\n", 4096)                 = 5
fstat(4, {st_mode=S_IFREG, st_size=0, ...}) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f98e28ad000
write(4, "fuga\n", 5)                   = 5
read(3, "", 4096)                       = 0
fchown(4, 1000, 1000)                   = 0
fchmod(4, 0100664)                      = 0
close(3)                                = 0
munmap(0x7f98e28ae000, 4096)            = 0
close(4)                                = 0
munmap(0x7f98e28ad000, 4096)            = 0
rename("./sedairHHD", "link")           = 0
close(1)                                = 0
close(2)                                = 0

上記の例では、sedは以下のような挙動を取っています。

  1. linkというファイルをリードオンリーでopen(2)する(fd=3)
  2. ./sedairHHDという一時ファイルをopen(2)する(fd=4)
  3. fd3から”hoge\n”をread(2)する
  4. fd4へ置換結果(“fuga\n”)をwrite(2)する
  5. fd3をclose(2)する
  6. fd4をclose(2)する
  7. 一時ファイルの名前をlinkにrename(2)する

このように、新規作成したファイルでシンボリックリンクを上書きしてしまっているため、既存のリンクが破壊されてしまうのです。
これを防ぐためには、–follow-symlinksというオプションを指定します。するとファイルをopen(2)した後に、lstat(2)とreadlink(2)でシンボリックリンクを辿ってくれます。そして最後にテンポラリファイルをrename(2)する際、リンク元を置き換えるように挙動が変化するのです。
たとえばCentOSでは、/etc/sysconfig/selinuxは/etc/selinux/configへのシンボリックリンクになっています。なのでそれを知らずに

# sed -i -e 's/^SELINUX=.*/SELINUX=disabled/' /etc/sysconfig/selinux

とすると ((いしかわさんごめんなさい)) 、設定ファイルがsplit brainしてしまいます。くれぐれも気をつけましょう。
余談ですが、似たような話として、Emacsの自動バックアップ機能がハードリンクを切る現象も有名です。
Emacsでは変数make-backup-filesにnil以外が設定されていると、編集したファイルのバックアップを自動的に作成します。この際オリジナルファイルをrenameした上で新しいファイルを作成するという挙動を取るため、オリジナルファイルにハードリンクが張られていると、そのハードリンクはリネームされたバックアップファイルの方を向いてしまうのです。これはEmacsのデフォルト動作のため、ハードリンクが張られたファイルをEmacsで編集すると、意図しない問題を引き起こす可能性があります。
これを防ぐには変数backup-by-copyingにnil以外を設定してください。するとEmacsはリネームではなく、コピーでバックアップを作成するようになります。参考までに。

ブログ記事検索

このブログについて

このブログは、札幌市・仙台市の「株式会社インフィニットループ」が運営する技術ブログです。 お仕事で使えるITネタを社員たちが発信します!