【问题标题】:sed -i touching files that it doesn't changesed -i 触摸它不会改变的文件
【发布时间】:2015-01-20 04:03:25
【问题描述】:

我们服务器上的某个人运行了sed -i 's/$var >> $var2/$var > $var2/ *,以将公共目录中某些 bash 脚本中的插入更改为覆盖。没什么大不了的,它首先用grep 进行了测试,它返回了只有他的文件会被触及的预期结果。

他运行了脚本,现在文件夹中的 1400 个文件中的 1200 个文件有了新的修改日期,但据我们所知,实际上只有他的一小部分文件被更改了。

  1. 为什么 sed 会“触摸”一个它没有改变的文件。
  2. 为什么它只会“触摸”部分文件而不是全部文件。
  3. 它是否真的改变了某些东西(可能是一些尾随空格或由于 sed 正则表达式中的 $ 而完全出乎意料的东西)?

【问题讨论】:

  • 是 gnu sed 吗?其他可能会有所不同。
  • 作为对 pt 3 的评论(这不是答案):如果您在脚本上使用版本控制 (git/subversion/etc),那么像这样的问题是什么改变或没有改变总能得到清晰的回答。
  • 我喜欢版本控制,但我无法控制这台服务器。上面的所有内容都已过时,需要国会通过才能安装 git。但是,是的,我完全 100% 同意你的观点。在那之前……每晚的备份和祈祷。
  • @Flexo GNU sed 4.1.4 版
  • 有什么可以阻止您从备份中提取文件树(或者如果政策允许,则定期提取文件树以 rsync)并在镜像树中运行 Git?

标签: sed ksh


【解决方案1】:

当 GNU sed 成功“就地”编辑文件时,其时间戳会更新。要了解原因,让我们回顾一下“就地”编辑是如何完成的:

  1. 创建一个临时文件来保存输出。

  2. sed 处理输入文件,将输出发送到临时文件。

  3. 如果指定了备份文件扩展名,则输入文件将重命名为备份文件。

  4. 无论是否创建备份,临时输出都会移动 (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 sourcesed目录下的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 也是如此。

【讨论】:

  • 我认为这是就地编辑的情况。感谢您在答案中删除源代码! 200 上的失败仍然有点神秘,因为权限似乎与其他文件一致,但也许是其他原因导致它们失败。不管怎样,我认为你是对的。谢谢!
  • 很好的分析;在不检查源代码的情况下,我发现 GNU sed(至少从 4.2.2 开始)正确地保留了原始输入文件的 permissions 。然而,遗憾的是,如果原始文件是 symlink,它会被替换为 regular 文件。同样保留的是原始文件的创建时间戳。 BSD sed(至少从 OSX 10.10 附带的版本开始)似乎在所有方面都表现相同。
  • @mklement0 哇!这是一个重要的观察。虽然我可以理解作者这样做的方式,但这肯定不是我对 "in-place" 编辑的期望。 (我已经用这些信息更新了答案。)
  • @John1024:谢谢,完全同意。当涉及到 true 就地编辑时,可悲的被忽视的ed 是你的朋友,它保留了现有的inode - 它带有一个警告,它总是将原始文件读入内存作为一个整体
  • “无论是否创建备份,临时输出都会移动(重命名)到输入文件。”如果文件没有更改,是否不能在两者之间应用差异来防止这种情况发生?
【解决方案2】:

我使用以下解决方法,即分别查看每个文件,使用 grep 检查文件是否包含字符串,然后使用 sed。不是很好,但工作...

for i in *;do grep mytext $i && sed -i -e 's/mytext/replacement/g' $i;done

【讨论】:

    猜你喜欢
    • 2016-07-15
    • 2020-02-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-09-30
    • 1970-01-01
    • 2015-09-03
    相关资源
    最近更新 更多