sed 是否可以使用 w(写入)命令而不是 POSIX 中未指定的 -i 标志就地编辑文件?

sed 是否可以使用 w(写入)命令而不是 POSIX 中未指定的 -i 标志就地编辑文件?

w我的命令sed(macOS 13.1 的 sed)似乎能够使用cat(bash 3.2)编辑输入文件:

printf "hello\nworld\n" > foo.txt

cat foo.txt | sed 's/l/L/g' | sed -n 'w foo.txt'

cat foo.txt
> heLLo
> worLd

我看了看https://pubs.opengroup.org/onlinepubs/9699919799/utilities/sed.html但我不确定为什么上面的管道可以成功编辑foo.txt,这与使用重定向等时不同cat foo.txt | sed 's/l/L/g' > foo.txt

我知道我可以使用 POSIX-nonspecified标志或临时文件,但我想知道使用(写入)命令编辑输入文件-i是否安全。w

编辑:

我试过

printf "%d hello world\n" {1..100000} > foo.txt

cat foo.txt | sed 's/l/L/g' | sed -n 'w foo.txt'

并发现它不再正常工作。结果foo.txt只有 4000-8000 行。

答案1

使用sponge(来自更多实用程序,或者重定向到临时文件并将其重命名为原始文件,或者使用编辑(或ex来自 vi/vim/nvi)而不是sed- 请记住,这sed是面向流的版本eded= editor, sed=溪流编辑。

仅供参考:ed、sed 和 ex(还有 vi - vi 最初是作为ed 的sual 版本)都共享一个公共的命令子集,因为它们都有共同的根......但它们中的每一个都是朝不同的方向开发的,并且具有不同的增强功能。每个都有多个不同的版本,同样具有不同的功能。许多其他程序至少借用了一些常用命令(例如,rogue 和 nethack 都借用了 hjkl 移动键)。另外值得注意的是,以防不明显:ex命令是:vi 内的命令,并且是命令的超集ed(取决于vi您使用的实现)。

所有三种方法的示例。

sed -e 's/l/L/g' foo.txt | sponge foo.txt

sed -e 's/l/L/g' foo.txt > foo.new && mv foo.new foo.txt

printf '%s\n' %s/l/L/g w q | ed -s foo.txt
printf '%s\n' %s/l/L/g w q | ex foo.txt

顺便说一句,来自man sponge

sponge读取标准输入并将其写入指定文件。与 shell 重定向不同,sponge 在写入输出文件之前吸收所有输入。这允许构建读取和写入同一文件的管道。

如果输出文件已经存在,海绵会保留该文件的权限。

笔记:

  1. Sponge 本质上是重定向和重命名方法的便捷工具。

  2. 重定向和重命名不保留原始输出文件的权限。它创建一个新文件,其权限由用户确定umask(与创建的任何其他新文件一样) - 根据 umask,这些权限可能与原始权限相同,也可能不同。

    不同之处在于sponge 确保新文件具有与原始文件相同的权限,而简单的重定向则不然。

  3. 使用edand 时ex,每个命令(s///替换w为 write,最后替换q为 quit)每行都会打印一个,printf '%s\n'并通过管道输送到edor ex,它打开 foo.txt 并执行命令。


另请注意:ed并且ex两者都会覆盖原始文件(保留原始文件的索引节点号,因此不会破坏该文件的任何硬链接)。 sponge和 write-to-a-tempfile-and-rename 都会创建具有不同 inode 编号的新文件,这将破坏任何硬链接。大多数时候(即除非您有一个或多个文件的硬链接),这根本不重要,但您需要注意这一点。

例如:注意 inode 号如何随以下变化sponge

$ printf "hello\nworld\n" > foo.txt
$ ls -li foo.txt
2251637 -rw-rw-r-- 1 cas cas 12 Feb  6 18:07 foo.txt
$ sed -e 's/l/L/g' foo.txt | sponge foo.txt
$ ls -li foo.txt
2251985 -rw-rw-r-- 1 cas cas 12 Feb  6 18:07 foo.txt

再次使用重定向覆盖文件不会更改 inode 编号,也不会使用 ex (或 ed)编辑它:

$ printf "hello\nworld\n" > foo.txt
$ ls -li foo.txt
2251985 -rw-rw-r-- 1 cas cas 12 Feb  6 18:08 foo.txt
$ printf '%s\n' %s/l/L/g w q | ex foo.txt 
$ ls -li foo.txt
2251985 -rw-rw-r-- 1 cas cas 12 Feb  6 18:09 foo.txt

如果需要,您可以使用重定向和重命名方法保留原始索引节点,如下所示:

sed -e 's/l/L/g' foo.txt > foo.new
cat foo.new > foo.txt
rm foo.new

是的,我知道cat不需要。<重定向也有效。我发现(在命令行开头重定向,或没有实际命令的重定向)是令人厌恶的丑陋,并且没有恐惧或羞耻联合大学

而且,正如斯蒂芬·基特在评论中指出的那样,cp foo.new foo.txt它也可以工作并且还保留原始权限。

答案2

w sed命令在第一次调用时(此处是在sed从管道读取数据块后处理其第一行时)使用 打开输出文件O_WRONLY | O_TRUNC,因此此时该文件被清空(截断ated),所以如果正在读取文件的命令(在您的情况下cat尚未完成读取),它将无法读取其余部分。

相反,你可以这样做:

sed 's/l/L/g' < file 1<> file

shell 在 sed 的 stdin 上使用O_RDONLY和独立地在 sed 的 stdout 上使用 来打开文件O_RDWR,但更重要的是,如果不使用O_TRUNCsosed将覆盖其自己的输入。

仅当sed像这里一样总是写入与其读取的行大小(以字节数为单位)完全相同的输出行时,这才有效,否则它最终可能会覆盖尚未读取的行。

如果写入的内容比读取的内容短,它也会在文件末尾留下旧数据。可以通过调用在末尾截断标准输出的内容来解决这个问题,例如:

{ sed 's/hello/hi/g'; perl -e 'truncate STDOUT, tell STDOUT'; } < file 1<> file

但如果您要使用perl,您不妨使用-i一些sed实现已复制的它:

perl -pi -e 's/hello/hi/g' file

相关内容