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 的内容会是空的。
但是最近我发现并不是这样,有时是可以回写成功的。文件足够小时有很大概率可以直接回写,比如下图可以看到回写成功了两次。而稍微大点的文件就比较难
在 v2 上发帖提问之前,我和同事、朋友们讨论过这个问题,我们有了一点点进展。
我们认为,没有回写成功,可能是因为文件还没读完就去写入。因此可以让写入晚一点,比如加一个 sleep ,这样确实可以解决,也是目前为止唯一的解决方案。
sort foo.txt | { sleep 1; tee foo.txt; }
这样听起来很合理,但是我们还是不理解为什么有时没有读完
1
tool2dx 207 天前 1
tee 应该是没判断管道是否关闭。
你可以用 ai 帮你写一个命令替代 tee ,确认输入管道完全关闭后,再写入文件。 |
2
lieh222 207 天前 via Android 1
tee 跟排序进程是同时启动的吧,tee 不加-a 打开文件的时候就清空了,但是 sort 读文件失败?
|
3
lieh222 207 天前 via Android 1
在 tee 前面加 strace 你就可以看到,tee 进程和 sort 并行启动,tee 启动就会用 w 模式打开文件,这一步已经清空文件了,sort 再读就会为空进程退出
|
4
hxy100 207 天前 1
曾经我也有相同的疑问,tee 行为相当之迷惑。期待大佬的权威解答
|
5
zhuisui 207 天前 2
bash 的 pipeline 只声明了会将命令程序的输出和输入连接起来,可没声称这些命令的执行开始和结束顺序。
|
6
sandylaw 207 天前 2
为什么会有不确定的行为:
当你使用 tee 写回到相同的文件时,tee 和 sort 的处理对文件的打开、读取、写入的时序会影响最终结果。这个命令有一个竞态条件的问题: 文件读写的时间差:sort 命令开始读取文件 foo 的内容,并进行排序。如果在 sort 读取完成之前 tee 就开始写入数据到 foo ,tee 的写入操作可能会覆盖 sort 还未读取的数据,导致数据丢失。 缓存和写入的延迟:UNIX 系统通常会使用缓存来优化读写操作。sort 可能还在处理数据,而 tee 可能已经开始写入,这种不同的处理速度可能导致 foo 文件的内容在未完全排序前就被覆盖。 **延迟写入** 如果你希望避免使用临时文件但仍需要确保数据的完整性,你可以考虑使用命令缓冲的方法,例如使用 Bash 的进程替换功能。这种方法可以让你在不创建物理临时文件的情况下处理数据。 下面是一个使用 Bash 进程替换来安全更新文件内容的例子: ```bash sort -u foo | sponge foo ``` 这里使用了 sponge 命令,它属于 moreutils 包的一部分。sponge 会读取所有的标准输入直到 EOF ,然后将数据写入到文件。这样可以避免在读取数据时同时写入同一个文件所引起的问题。 如果你的系统上还没有 sponge ,你可以通过包管理器安装 moreutils: ```bash sudo apt-get install moreutils ``` 延迟写入:由于 sponge 延迟写入,它避免了 tee 可能遇到的读写冲突问题,但代价是必须有足够的内存来存储所有输入,直到处理完成。 |
7
aloxaf 207 天前 3
管道是流式的,如果你写「 sort foo.txt | tee foo.txt 」,「 sort foo.txt 」和「 tee foo.txt 」会一起启动,而后者启动时会清空 foo.txt ,导致前者读不到东西。
对于这种需求,你应该使用 sponge 命令,它会等读取完所有数据再一次写入:sort foo.txt | sponge foo.txt |
8
jinqzzz OP 原来是我对管道的理解有误,感谢楼上各位大佬答疑,也感谢推荐 sponge 的大佬。
|
9
blessingsi 207 天前 2
sort 有个 -o 参数
sort -o foo.txt foo.txt |
10
jinqzzz OP @blessingsi 惭愧,居然一直不知道有这个参数...
|
11
zhuisui 207 天前 3
pipeline 水管嘛,想想现实世界中的水管,谁会用水管储水,不都拿蓄水池嘛
所以你想把上游的输出全部放到水管里以后再放到下游的水龙头,就知道这样做是不合适的了吧 但是如果你真想干这种奇怪的事,那就是想办法造一个非常大非常粗的水管了 |
13
mohumohu 207 天前 1
这是个 XY 问题,sort 本来就可以-o 回写。
|
14
hellolinuxer 207 天前 1
sort foo.txt | cat | tee foo.txt 就 ok 了
|
15
hellolinuxer 207 天前 1
@jinqzzz 不是你对管道理解有误,而是你对 tee 理解有误,tee 是三通,所有你的使用方法不对,虽然 sort foo.txt | cat | tee foo.txt 也能解决,但是很明显 sort -o foo.txt foo.txt 资源使用上是最优解,但不是最安全的
|
16
vituralfuture 207 天前 via Android 1
bash 的管道,就是先创建一个 pipe ,然后 fork ,再分别设置输入输出,然后 exec ,并不是前一个命令执行完毕,后一个命令拿到它的输出,开始执行。应该理解为,read write 系统调用会在管道没有数据的时候阻塞,如果后一个命令需要读输入,而管道没有数据,就会阻塞等待前一个命令输出。而 read write 系统调用时,进程进入阻塞状态,而进程转为就绪状态时,何时执行又依赖于调度器,所以 bash 管道连接的两个命令,执行时序不容易预测
举一个例子,有个需求是给一个目录 xxx 加上 x 权限,然后 cd 进去,我有个朋友在初学 shell 时使用的命令是 chmod +x xxx | cd xxx 这个命令,有时能行,有时又 permission denied ,本质就是进程执行时序的问题。如果需要保证时序,可以用分号分成两个命令,也可以使用&& |
17
geelaw 207 天前 via iPhone 1
@hellolinuxer #14 这是错误的,中间的 cat 和没写的执行效果是完全一样的,纯粹是浪费资源。
|
18
nuffin 207 天前 1
最后的问题 3 ,系统就是你说的那样,先创建两个进程,把他们用管道连起来,然后在分别 exec 执行管道两边的命令。所以一行里写若干个管道的话,实际上管道里的多个进程都是同时在执行的。需要注意的就是,因为 fork 多个进程,再去 exec 不同命令( sort ,tee 这些)的调度依赖于系统的进程调度,所以谁先执行文件操作这点,并不一定。所以有时候小文件能执行成功,可能就是前面的已经把文件内容读到内存里了,那这时候 tee 情况文件已经不影响结果了。
另外,这种问题其实可以写个 c 程序验证一下。其他语言在操作文件之前的准备工作可能久一些,会影响观察结果。 |
19
GrayXu 207 天前 1
@vituralfuture #16 op 的操作是依赖的,如果还想要流式处理就不能用这样简单用 pipe 组合。sponge 就是拿来保证生产者可以一直往里塞
|
20
nuffin 207 天前 1
这种情况下,用多个文件是最合理的。尤其是文件比较大的时候。因为删掉一个文件是直接操作文件系统的分配表,不会真的去写个大文件,把新文件改名成原来的文件名也是一样的文件系统目录结构修改。另外,如果一个文件的处理过程比较长,那么在这时候系统重启或者断电的时候,都操作一个文件的方式就会导致文件的状态不可知,用临时文件的方式可以重复执行很多遍,都是同样的结果,即使中间有失败的情况也无所谓,因为在完整流程完成之前,新的文件没有“提交”。
|
21
nuffin 207 天前 1
我其实觉得 sponge 不够 “管道”,因为它断流了。
|
24
sendi 207 天前 1
https://www.yuque.com/wangsendi/hmeaaw/yhti79b6guut4yt5
可以参考 awk 的 结尾 1<>a 这样的模式 这样就不会截留了 |
25
jinqzzz OP @mohumohu 我的提问是不太准确,sort foo.txt | tee foo.txt 只是一种简化的场景,它代表了「如何在修改文件内容的同时,写入原文件」和「 | tee 的用法」 , 和 sort 没有太大关系。
想了想,我为什么都没想过 sort 有 -o ,因为更常见的场景是 cat foo.txt | xxx | xxx | tee foo.txt ,显然没人会去奢望前边有一个 -o 可以解决所有的问题... |
26
jinqzzz OP @sendi 进程替换这种用法还没见过,学习了,确实挺好用。
但是我这里测试看有一个小问题,短时间内执行多次 sort foo.txt > >(tee foo.txt ) ,会有很低的概率把 foo 清空,如果用 for 批量执行,清空的概率就非常高了 for i in {01..20}; do echo $i; sort foo > >(tee foo ) ; done |
28
jinqzzz OP 回车里 -> 回车了
|
29
jinliming2 207 天前 via iPhone 1
@jinqzzz sort 有个比较特殊的点是,它必须一次性把所有内容都读入才能开始输出,因为有可能最后一行的内容被排序到最前面。在输出之前,内容都是要读到内存里的,处理大文件要足够的内存。
所以可以用一些方法来延迟 tee 创建输出流的时间,确保 sort 已经读取所有内容。 如果是 cat xxx | tee xxx 这样的,cat 是支持流式处理的,也就是读多少输出多少,读取的内容可能比内存都要大,这种情况 sort 命令都肯定要失败的。这种就不建议延迟 tee 了,还是换个文件名来写,确保读取写入全部完成之后再做文件替换是比较稳妥的。 |
32
zhuisui 205 天前
@jinqzzz 因为 process substitution 也可能用 pipe 实现,道理一样
https://www.gnu.org/software/bash/manual/html_node/Process-Substitution.html |
34
zhuisui 205 天前
|