当 GNU sed 成功“就地”编辑文件时,其时间戳会更新。要了解原因,让我们回顾一下“就地”编辑是如何完成的:
创建一个临时文件来保存输出。
sed 处理输入文件,将输出发送到临时文件。
如果指定了备份文件扩展名,则输入文件将重命名为备份文件。
无论是否创建备份,临时输出都会移动 (rename) 到输入文件。
GNU sed 不跟踪是否对文件进行了任何更改。临时输出文件中的任何内容都通过rename 移动到输入文件。
此过程有一个很好的好处:POSIX requires that rename be atomic。因此,输入文件永远不会处于损坏状态:它要么是原始文件,要么是修改后的文件,并且永远不会介于两者之间。
作为此过程的结果,sed 成功处理的任何文件都将更改其时间戳。
示例
让我们考虑一下inputfile:
$ cat inputfile
this is
a test.
现在,在strace 的监督下,让我们在它上面运行sed -i,保证不会造成任何变化:
$ strace sed -i 's/XXX/YYY/' inputfile
编辑后的结果如下:
execve("/bin/sed", ["sed", "-i", "s/XXX/YYY/", "inputfile"], [/* 55 vars */]) = 0
[...snip...]
open("inputfile", O_RDONLY) = 4
[...snip...]
open("./sediWWqLI", O_RDWR|O_CREAT|O_EXCL, 0600) = 6
[...snip...]
read(4, "this is\na test.\n", 4096) = 16
write(6, "this is\n", 8) = 8
write(6, "a test.\n", 8) = 8
read(4, "", 4096) = 0
[...snip...]
close(4) = 0
[...snip...]
close(6) = 0
[...snip...]
rename("./sediWWqLI", "inputfile") = 0
如您所见,sed 在文件句柄 4 上打开输入文件 inputfile。然后它在文件句柄 6 上创建一个临时文件 ./sediWWqLI 来保存输出。它从输入文件中读取数据并将其原封不动地写入输出文件。完成后,调用rename 覆盖inputfile,更改其时间戳。
GNU sed 源代码
相关源码在the source的sed目录下的execute.c文件中。从 4.2.1 版本开始:
ck_fclose (input->fp);
ck_fclose (output_file.fp);
if (strcmp(in_place_extension, "*") != 0)
{
char *backup_file_name = get_backup_file_name(target_name);
ck_rename (target_name, backup_file_name, input->out_file_name);
free (backup_file_name);
}
ck_rename (input->out_file_name, target_name, input->out_file_name);
free (input->out_file_name);
ck_rename 是 stdio 函数 rename 的覆盖函数。 ck_rename 的来源在sed/utils.c。
如您所见,没有保留任何标志来确定文件是否实际更改。无论如何都会调用rename。
时间戳未更新的文件
对于 1400 个文件中的 200 个时间戳没有更改的文件,这意味着 sed 在这些文件上以某种方式失败。一种可能性是权限问题。
sed -i 和符号链接
正如mklement0 所指出的,将sed -i 应用于符号链接会导致令人惊讶的结果。 sed -i 不更新符号链接指向的文件。相反,sed -i 用新的常规文件覆盖符号链接。
这是sed 调用STDIO rename 的结果。正如man 2 rename所记录的那样:
如果 newpath 引用符号链接,则该链接将被覆盖。
mklement0 报告说,Mac OSX 10.10 上的 (BSD) sed 也是如此。