读写文件:tee 命令

读写文件:tee 命令

众所周知,这样的命令:

cat filename | some_sed_command >filename

删除文件 filename,因为在命令之前执行的输出重定向导致 filename 被截断。

可以按照以下方式解决该问题:

cat file | some_sed_command | tee file >/dev/null

但我不确定这在任何情况下是否有效:如果文件(以及 sed 命令的结果)非常大会发生什么?操作系统如何避免覆盖一些尚未读取的内容?我看到还有一个 sponge 命令在任何情况下都应该有效:它比 tee “更安全”吗?

答案1

可以按照以下方式解决该问题:

cat file | some_sed_command | tee file >/dev/null

被截断的几率file会有下降,但是不能保证cat file | some_sed_command | tee file >/dev/null不会被截断file

这一切都取决于首先处理哪个命令,而不是人们所期望的,管道中的命令不是按从左到右的顺序处理的。无法保证哪个命令会首先被选中,因此不妨将其视为随机选择并绝不依靠外壳不挑选有问题的那个。

由于在三个命令之间首先选择有问题的命令的几率低于在两个命令之间首先选择有问题的命令的几率,因此被file截断的可能性较小,但是它仍会发生

script.sh

#!/bin/bash
for ((i=0; i<100; i++)); do
    cat >file <<-EOF
    foo
    bar
    EOF
    cat file |
        sed 's/bar/baz/' |
        tee file >/dev/null
    [ -s file ] &&
        echo 'Not truncated' ||
        echo 'Truncated'
done |
    sort |
    uniq -c
rm file
% bash script.sh
 93 Not truncated
  7 Truncated
% bash script.sh
 98 Not truncated
  2 Truncated
% bash script.sh
100 Not truncated

所以绝不使用类似的东西cat file | some_sed_command | tee file >/dev/nullsponge按照 Oli 的建议使用。

或者,对于更严格的环境和/或相对较小的文件,可以在运行任何命令之前使用此处的字符串和命令替换来读取文件:

$ cat file
foo
bar
$ for ((i=0; i<100; i++)); do <<<"$(<file)" sed 's/bar/baz/' >file; done
$ cat file
foo
baz

答案2

具体来说sed,您可以使用其-i就地参数。它只是保存回它打开的文件,例如:

sed -i 's/ /-/g' filename

如果您想做更强大的事情,假设您要做的事情不止于此,是的,您可以使用(来自包)sed缓冲整个过程,它将在写入文件之前“吸收”所有标准输入。它就像但功能较少。但对于基本用法,它几乎是一个嵌入式替代品:spongemoreutilstee

cat file | some_sed_command | sponge file >/dev/null

这样更安全吗?当然。它可能有限制,所以如果你正在做一些庞大的事情(并且不能用 sed 就地编辑),你可能希望对第二个文件进行编辑,然后mv将该文件恢复为原始文件名。这应该是原子的(因此任何依赖于这些文件的东西都不会中断,即使它们需要持续访问)。

答案3

哦,但这sponge不是唯一的选择;你不必moreutils为了让它正常工作而获得。任何机制只要满足以下两个要求就可以工作:

  1. 它接受输出文件的名称作为参数。
  2. 只有在处理完所有输入后才会创建输出文件。

你看,OP提到的众所周知的问题是,shell会在开始执行管道中的命令之前创建管道工作所需的所有文件,所以shell实际上在任何命令有机会开始执行之前就截断了输出文件(不幸的是,这也是输入文件)。

tee命令无法工作,即使它满足第一个要求,因为它不满足第二个要求:它总是在启动时立即创建输出文件,因此它本质上与直接在输出文件中创建管道一样糟糕。(它实际上更糟糕,因为它的使用在输出文件被截断之前引入了非确定性的随机延迟,因此您可能会认为它有效,但实际上并非如此。)

因此,为了解决这个问题,我们所需要的只是一些命令,这些命令将在产生任何输出之前缓冲其所有输入,并且能够接受输出文件名作为参数,这样我们就不必将其输出通过管道传输到输出文件中。 其中一个这样的命令是shuf。 因此,下面的命令将完成相同的任务sponge

    shuf --output=file --random-source=/dev/zero 

--random-source=/dev/zero部分采用技巧性shuf的做法,根本不进行任何改组,因此它会缓冲您的输入而不进行任何改变。

答案4

你可以在 Ex 模式下使用 Vim:

ex -sc '%!some_sed_command' -cx filename
  1. %选择所有行

  2. !运行命令

  3. x保存并退出

相关内容