问题
v2 大佬比较多,想在这里请教各位大佬一个困惑了我多年的问题:如何在一行命令里,排序文件内容并用 tee 写到原来的文件中?
使用临时文件
想要将 foo.txt 文件中的文本排序后依然保存到 foo.txt 文件中,需要先写到一个临时文件,然后将临时文件重命名为 foo.txt 。这也是一个比较常见的方案。
sort foo.txt | tee tmp-foo.txt
mv tmp-foo.txt foo.txt

我一直以来都以为 tee 无法直接回写(不知道这个词用的对不对)到文件,如果直接 sort foo.txt | tee foo.txt,那么 foo.txt 的内容会是空的。
tee 有时可以直接回写文件
但是最近我发现并不是这样,有时是可以回写成功的。文件足够小时有很大概率可以直接回写,比如下图可以看到回写成功了两次。而稍微大点的文件就比较难

和朋友们的讨论
在 v2 上发帖提问之前,我和同事、朋友们讨论过这个问题,我们有了一点点进展。
我们认为,没有回写成功,可能是因为文件还没读完就去写入。因此可以让写入晚一点,比如加一个 sleep ,这样确实可以解决,也是目前为止唯一的解决方案。
sort foo.txt | { sleep 1; tee foo.txt; }

这样听起来很合理,但是我们还是不理解为什么有时没有读完

我用 strace 分析过回写成功和失败的日志,没有发现任何区别。
sort 命令是在内存中排序,读取速度和硬件性能有关,但是内存频率高、性能好、读的快,就可以成功写入,这也太不稳定了。
这可能和 Bash 管道的实现有关吗?如果还没有执行完管道符前的命令,就去执行管道符后的命令了,听起来不太合理。

tee 应该是没判断管道是否关闭。你可以用 ai 帮你写一个命令替代 tee ,确认输入管道完全关闭后,再写入文件。

tee 跟排序进程是同时启动的吧,tee 不加-a 打开文件的时候就清空了,但是 sort 读文件失败?

在 tee 前面加 strace 你就可以看到,tee 进程和 sort 并行启动,tee 启动就会用 w 模式打开文件,这一步已经清空文件了,sort 再读就会为空进程退出

曾经我也有相同的疑问,tee 行为相当之迷惑。期待大佬的权威解答

bash 的 pipeline 只声明了会将命令程序的输出和输入连接起来,可没声称这些命令的执行开始和结束顺序。

为什么会有不确定的行为:当你使用 tee 写回到相同的文件时,tee 和 sort 的处理对文件的打开、读取、写入的时序会影响最终结果。这个命令有一个竞态条件的问题:文件读写的时间差:sort 命令开始读取文件 foo 的内容,并进行排序。如果在 sort 读取完成之前 tee 就开始写入数据到 foo ,tee 的写入操作可能会覆盖 sort 还未读取的数据,导致数据丢失。缓存和写入的延迟:UNIX 系统通常会使用缓存来优化读写操作。sort 可能还在处理数据,而 tee 可能已经开始写入,这种不同的处理速度可能导致 foo 文件的内容在未完全排序前就被覆盖。延迟写入如果你希望避免使用临时文件但仍需要确保数据的完整性,你可以考虑使用命令缓冲的方法,例如使用 Bash 的进程替换功能。这种方法可以让你在不创建物理临时文件的情况下处理数据。下面是一个使用 Bash 进程替换来安全更新文件内容的例子:bashsort -u foo | sponge foo这里使用了 sponge 命令,它属于 moreutils 包的一部分。sponge 会读取所有的标准输入直到 EOF ,然后将数据写入到文件。这样可以避免在读取数据时同时写入同一个文件所引起的问题。如果你的系统上还没有 sponge ,你可以通过包管理器安装 moreutils:bashsudo apt-get install moreutils延迟写入:由于 sponge 延迟写入,它避免了 tee 可能遇到的读写冲突问题,但代价是必须有足够的内存来存储所有输入,直到处理完成。

管道是流式的,如果你写「 sort foo.txt | tee foo.txt 」,「 sort foo.txt 」和「 tee foo.txt 」会一起启动,而后者启动时会清空 foo.txt ,导致前者读不到东西。对于这种需求,你应该使用 sponge 命令,它会等读取完所有数据再一次写入:sort foo.txt | sponge foo.txt

原来是我对管道的理解有误,感谢楼上各位大佬答疑,也感谢推荐 sponge 的大佬。

sort 有个 -o 参数sort -o foo.txt foo.txt

惭愧,居然一直不知道有这个参数...

pipeline 水管嘛,想想现实世界中的水管,谁会用水管储水,不都拿蓄水池嘛所以你想把上游的输出全部放到水管里以后再放到下游的水龙头,就知道这样做是不合适的了吧但是如果你真想干这种奇怪的事,那就是想办法造一个非常大非常粗的水管了

大佬解释的非常形象

这是个 XY 问题,sort 本来就可以-o 回写。

sort foo.txt | cat | tee foo.txt 就 ok 了

不是你对管道理解有误,而是你对 tee 理解有误,tee 是三通,所有你的使用方法不对,虽然 sort foo.txt | cat | tee foo.txt 也能解决,但是很明显 sort -o foo.txt foo.txt 资源使用上是最优解,但不是最安全的

bash 的管道,就是先创建一个 pipe ,然后 fork ,再分别设置输入输出,然后 exec ,并不是前一个命令执行完毕,后一个命令拿到它的输出,开始执行。应该理解为,read write 系统调用会在管道没有数据的时候阻塞,如果后一个命令需要读输入,而管道没有数据,就会阻塞等待前一个命令输出。而 read write 系统调用时,进程进入阻塞状态,而进程转为就绪状态时,何时执行又依赖于调度器,所以 bash 管道连接的两个命令,执行时序不容易预测举一个例子,有个需求是给一个目录 xxx 加上 x 权限,然后 cd 进去,我有个朋友在初学 shell 时使用的命令是 chmod +x xxx | cd xxx这个命令,有时能行,有时又 permission denied ,本质就是进程执行时序的问题。如果需要保证时序,可以用分号分成两个命令,也可以使用&&

#14 这是错误的,中间的 cat 和没写的执行效果是完全一样的,纯粹是浪费资源。

最后的问题 3 ,系统就是你说的那样,先创建两个进程,把他们用管道连起来,然后在分别 exec 执行管道两边的命令。所以一行里写若干个管道的话,实际上管道里的多个进程都是同时在执行的。需要注意的就是,因为 fork 多个进程,再去 exec 不同命令( sort ,tee 这些)的调度依赖于系统的进程调度,所以谁先执行文件操作这点,并不一定。所以有时候小文件能执行成功,可能就是前面的已经把文件内容读到内存里了,那这时候 tee 情况文件已经不影响结果了。另外,这种问题其实可以写个 c 程序验证一下。其他语言在操作文件之前的准备工作可能久一些,会影响观察结果。

#16 op 的操作是依赖的,如果还想要流式处理就不能用这样简单用 pipe 组合。sponge 就是拿来保证生产者可以一直往里塞

这种情况下,用多个文件是最合理的。尤其是文件比较大的时候。因为删掉一个文件是直接操作文件系统的分配表,不会真的去写个大文件,把新文件改名成原来的文件名也是一样的文件系统目录结构修改。另外,如果一个文件的处理过程比较长,那么在这时候系统重启或者断电的时候,都操作一个文件的方式就会导致文件的状态不可知,用临时文件的方式可以重复执行很多遍,都是同样的结果,即使中间有失败的情况也无所谓,因为在完整流程完成之前,新的文件没有“提交”。

我其实觉得 sponge 不够 “管道”,因为它断流了。

sort rpc.sh > >(tee rpc.sh)可以使用进程替换来实现呢

也是类似于临时文件 但是 bash 在处理过程中有使用缓存或者临时存储

www.yuque.com/wangsendi/hmeaaw/yhti79b6guut4yt5可以参考 awk 的 结尾 1<>a 这样的模式 这样就不会截留了

我的提问是不太准确,sort foo.txt | tee foo.txt 只是一种简化的场景,它代表了「如何在修改文件内容的同时,写入原文件」和「 | tee 的用法」 , 和 sort 没有太大关系。想了想,我为什么都没想过 sort 有 -o ,因为更常见的场景是 cat foo.txt | xxx | xxx | tee foo.txt ,显然没人会去奢望前边有一个 -o 可以解决所有的问题...

进程替换这种用法还没见过,学习了,确实挺好用。但是我这里测试看有一个小问题,短时间内执行多次 sort foo.txt > >(tee foo.txt ) ,会有很低的概率把 foo 清空,如果用 for 批量执行,清空的概率就非常高了 for i in {01..20}; do echo $i; sort foo > >(tee foo ) ; done

还没写完就回车里,不过这种场景我现在还遇不到,就不太关心了。 1<> 是没有这个问题的,很好用

回车里 -> 回车了

sort 有个比较特殊的点是,它必须一次性把所有内容都读入才能开始输出,因为有可能最后一行的内容被排序到最前面。在输出之前,内容都是要读到内存里的,处理大文件要足够的内存。所以可以用一些方法来延迟 tee 创建输出流的时间,确保 sort 已经读取所有内容。如果是 cat xxx | tee xxx 这样的,cat 是支持流式处理的,也就是读多少输出多少,读取的内容可能比内存都要大,这种情况 sort 命令都肯定要失败的。这种就不建议延迟 tee 了,还是换个文件名来写,确保读取写入全部完成之后再做文件替换是比较稳妥的。

从技术上讲,用管道把俩进程连接起来,其打开顺序只能有一种吧

理论上 bash 对管道没有这样的声明,实际上自然也不能做这样的假定。而且先开输入再开输出再连接它们在技术上是完全可行的。

因为 process substitution 也可能用 pipe 实现,道理一样 www.gnu.org/software/bash/manual/html_node/Process-Substitution.html

先开输入再开输入似乎不能保证完整性吧?