2026/09/11

Win32/NTFS の妙な挙動の話

 小さな機能のライブラリを作っていると、だんだん凝り性になっていくのは悪いクセなのですが、os.rename / POSIX の rename(2) / Win32 の MoveFileEx 辺りをイジっていてですね。

 Python の os.rename が、POSIXでは改名先を上書きする仕様、Win32では上書きせずにエラーになる仕様、ということを踏まえて、次のコードを見て下さい。

#!/usr/bin/python3
import os, sys

with open("1", "w") as f: f.write("content 1")
with open("2", "w") as f: f.write("content 2")

try:
    os.rename("2", "1")
except:
    print(f"move 2 => 1 failed : {sys.exn_info()}")
print(f"after 2 => 1: 2 isexist: {os.path.exists('2')}")

os.link("1", "1h")
try:
    os.rename("1h", "1")
except:
    print(f"move 1h => 1 failed : {sys.exn_info()}")
print(f"after 1h => 1: 1h isexist: {os.path.exists('1h')}")
ファイル 2 で 1 を、そのハードリンク 1h で 1 に上書きリネームするプログラムです。それぞれ実行後に、左側のファイルの存在を確認しています

実行結果を想像してください。まあ普通は Linux では False、Win32 では True になりそうですよね。

では結果。


Linux:

after 2 => 1: 2 isexist: False
after 1h => 1: 1h isexist: True

Win32:

move 2 => 1 failed : (<class 'FileExistsError'>, ...)
after 2 => 1: 2 isexist: True
after 1h => 1: 1h isexist: False

……謎すぎますよね。

まず、Linux のほう。2 => 1 は 2 がなくなって 1 が上書きされる常識的な動作。
1h => 1 のほう、ハードリンクがあると、元のファイルが消えません。リンクされたまま残ります。 これは正式なPOSIXの仕様で、仕様書にもLinuxのマニュアルにも書いてあります。納得はできませんが。

If oldpath and newpath are existing hard links referring to the same file, then rename() does nothing, and returns a success status.

で、謎なのがWin32のほう。2 => 1 が上書きされないのは良いんですが、1h => 1 のほう、エラーにならずに元ファイルのリンクが消えます。正直意味不明。

念のためリファレンスを見ても、 

The new name must not already exist.

としか書いていないので、さすがにこれでエラーにならずに元が消えるのはバグなんじゃないかなと。