Linux:如何同时使用文件作为输入和输出?


55

我只是在bash中运行以下命令:

uniq .bash_history > .bash_history

我的历史记录文件最终完全为空。

我想我需要一种在写入文件之前先读取整个文件的方法。怎么做?

PS:我显然想过要使用一个临时文件,但是我正在寻找一个更优雅的解决方案。


这是因为文件是从右到左打开的。另请参见stackoverflow.com/questions/146435/…–
WheresAlice

您必须将输出写入同一目录中的新文件,然后将其重命名为旧文件。如果其他方法中途中断,则可能会丢失数据。有些工具可能会使您看不到这一步。
kasperd '16

或者,bash如果将HISTCONTROL设置为包含忽略的内容,则不会在其历史记录中放置连续的重复项;请参阅联机帮助页。
dave_thompson_085 '16

Answers:


49

我建议spongemoreutils使用。从联机帮助页:

DESCRIPTION
  sponge  reads  standard  input  and writes it out to the specified file. Unlike
  a shell redirect, sponge soaks up all its input before opening the output file.
  This allows for constructing pipelines that read from and write to the same 
  file.

要将其应用于您的问题,请尝试:

uniq .bash_history | sponge .bash_history

6
它就像猫,但具有吸吮能力:D
MilliaLover

77

我只想提供一个简单但不使用海绵的答案(因为它通常不包含在轻量级环境中)。

echo "$(uniq .bash_history)" > .bash_history

应该具有预期的结果。在打开.bash_history进行写入之前,将执行该子Shell。正如Phil P的答案所解释的,当在原始命令中读取.bash_history时,它已经被'>'运算符截断了。


15
我通常不喜欢回答那些已经有一个有效的,可以接受的答案的古老问题的答案-但这很优雅,写得很好,并为它的必要性(轻量级环境)提出了有力的论据。对我来说,这确实为现有的答案增加了一些东西。欢迎来到SF,哈特(您已经在这里呆了一个月,但我认为这是您的第一个实质性发文)。我希望能从您这里读到更多答案!
MadHatter

4
这是最好的解决方案。$()由于某些转义问题,我不得不使用subshel​​l 而不是反引号。
CMCDragonkai 2015年

3
我想知道这个解决方案是否可以扩展到大文件,例如20或50 GB。
阿米特·奈都

1
这确实应该是可以接受的答案。
maxywb

1
echo "$(fmt -p '# ' -w 50 readme.txt)" > readme.txt今天用这个答案做。一直在寻找一种优雅的解决方案。非常感谢,@ Hart Simha!
shredalert

12

问题在于您的外壳在运行命令之前正在建立命令管道。这与“输入和输出”无关,因为文件的内容在uniq甚至运行之前就已经消失了。它类似于:

  1. Shell打开>输出文件进行写入,将其截断
  2. Shell设置为将文件描述符1(用于stdout)用于该输出
  3. Shell执行uniq,也许类似于execlp(“ uniq”,“ uniq”,“ .bash_history”,NULL)
  4. uniq运行,打开.bash_history,在此未找到任何内容

有多种解决方案,包括其他人提到的就地编辑和临时文件使用,但是关键是要了解问题,实际出了什么问题以及原因。


9

无需使用即可完成此操作的另一个技巧sponge是以下命令:

{ rm .bash_history && uniq > .bash_history; } < .bash_history

这是在backreference.org上出色的文件“就地”编辑文章中描述的作弊技巧之一。

基本上,它会打开文件进行读取,然后“删除”它。但是,它并没有真正被删除:有一个指向它的打开的文件描述符,只要保持打开状态,文件就仍然存在。然后,它将创建一个具有相同名称的新文件,并向其中写入唯一的行。

该解决方案的缺点:如果uniq由于某种原因失败,您的历史记录将消失。



3

sed脚本删除相邻的重复项。使用该-i选项,它就地进行修改。来自sed info文件:

sed -i 'h;:b;$b;N;/^\(.*\)\n\1$/ {g;bb};$b;P;D' .bash_history

sed仍使用temp文件,并添加了带有strace插图的答案(并不是真的很重要):-)
Kyle Brandt 2010年

3
@Kyle:的确如此,但是“视而不见,心不在“”。就个人而言,我会使用显式的临时文件,因为process input > tmp && mv tmp input比起使用sed欺骗手段来避免临时文件,这样的操作要简单得多,而且可读性强,如果失败了,它将不会覆盖我的原始文件(我不知道是否sed -i会正常失败-我会认为会的)。此外,使用output-to-temp-file方法可以做很多事情,如果没有比此sed脚本还要涉及更多的事情,就无法就地完成。我知道您知道所有这一切,但可能会使围观者受益。
丹尼斯·威廉姆森

3

作为一个有趣的花絮,sed也使用一个临时文件(这正是为您做的):

$ strace sed -i 's/foo/bar/g' foo    
open("foo", O_RDONLY|O_LARGEFILE)       = 3
...
open("./sedPmPv9z", O_RDWR|O_CREAT|O_EXCL|O_LARGEFILE, 0600) = 4
...
read(3, "foo\n"..., 4096)               = 4
write(4, "bar\n"..., 4)                 = 4
read(3, ""..., 4096)                    = 0
close(3)                                = 0
close(4)                                = 0
rename("./sedPmPv9z", "foo")            = 0
close(1)                                = 0
close(2)                                = 0

说明:
临时文件./sedPmPv9z变成fd 4,foo文件变成fd3。读操作在fd 3上,写在fd 4(临时文件)上。然后在重命名调用中用临时文件覆盖foo文件。




0

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

ex -sc '%!uniq' -cx .bash_history
  1. % 选择所有行

  2. ! 运行命令

  3. x 保存并关闭


By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.