Bash 输出重定向问题
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/123235/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Problem with Bash output redirection
提问by Charles Duffy
I was trying to remove all the lines of a file except the last line but the following command did not work, although file.txt is not empty.
我试图删除除最后一行之外的所有文件行,但以下命令不起作用,尽管 file.txt 不为空。
$cat file.txt |tail -1 > file.txt
$cat file.txt
Why is it so?
为什么会这样?
回答by Charles Duffy
Redirecting from a file through a pipeline back to the same file is unsafe; if file.txtis overwritten by the shell when setting up the last stage of the pipeline before tailstarts reading off the first stage, you end up with empty output.
通过管道从文件重定向回同一个文件是不安全的;如果file.txt在tail开始读取第一阶段之前设置管道的最后阶段时被外壳程序覆盖,则最终输出为空。
Do the following instead:
请改为执行以下操作:
tail -1 file.txt >file.txt.new && mv file.txt.new file.txt
...well, actually, don't do that in production code; particularly if you're in a security-sensitive environment and running as root, the following is more appropriate:
...嗯,实际上,不要在生产代码中这样做;特别是如果您在安全敏感的环境中并以 root 身份运行,以下更合适:
tempfile="$(mktemp file.txt.XXXXXX)"
chown --reference=file.txt -- "$tempfile"
chmod --reference=file.txt -- "$tempfile"
tail -1 file.txt >"$tempfile" && mv -- "$tempfile" file.txt
Another approach (avoiding temporary files, unless <<<implicitly creates them on your platform) is the following:
另一种方法(避免临时文件,除非<<<在您的平台上隐式创建它们)如下:
lastline="$(tail -1 file.txt)"; cat >file.txt <<<"$lastline"
(The above implementation is bash-specific, but works in cases where echo does not -- such as when the last line contains "--version", for instance).
(上面的实现是特定于 bash 的,但在 echo 没有的情况下有效——例如当最后一行包含“--version”时)。
Finally, one can use sponge from moreutils:
最后,可以使用来自moreutils 的海绵:
tail -1 file.txt | sponge file.txt
回答by Chris
You can use sed to delete all lines but the last from a file:
您可以使用 sed 从文件中删除除最后一行之外的所有行:
sed -i '$!d' file
- -itells sed to replace the file in place; otherwise, the result would write to STDOUT.
- $is the address that matches the last line of the file.
- dis the delete command. In this case, it is negated by !, so all lines notmatching the address will be deleted.
- -i告诉 sed 就地替换文件;否则,结果将写入 STDOUT。
- $是与文件最后一行匹配的地址。
- d是删除命令。在这种情况下,它被否定!,因此所有与地址不匹配的行都将被删除。
回答by ephemient
Before 'cat' gets executed, Bash has already opened 'file.txt' for writing, clearing out its contents.
在执行 'cat' 之前,Bash 已经打开了 'file.txt' 进行写入,清除其内容。
In general, don't write to files you're reading from in the same statement. This can be worked around by writing to a different file, as above:
通常,不要在同一语句中写入您正在读取的文件。这可以通过写入不同的文件来解决,如上所述:
$cat file.txt | tail -1 >anotherfile.txt $mv anotherfile.txt file.txt或者通过使用像海绵这样的实用程序来自 moreutils更多实用程序:
$cat file.txt | tail -1 | sponge file.txt这是有效的,因为海绵在打开其输出文件之前等待其输入流结束。
回答by Craig Trader
When you submit your command string to bash, it does the following:
当您将命令字符串提交给 bash 时,它会执行以下操作:
- Creates an I/O pipe.
- Starts "/usr/bin/tail -1", reading from the pipe, and writing to file.txt.
- Starts "/usr/bin/cat file.txt", writing to the pipe.
- 创建一个 I/O 管道。
- 启动“/usr/bin/tail -1”,从管道中读取,并写入 file.txt。
- 启动“/usr/bin/cat file.txt”,写入管道。
By the time 'cat' starts reading, 'file.txt' has already been truncated by 'tail'.
当 'cat' 开始读取时,'file.txt' 已经被 'tail' 截断了。
That's all part of the design of Unix and the shell environment, and goes back all the way to the original Bourne shell. 'Tis a feature, not a bug.
这就是 Unix 和 shell 环境设计的一部分,并且可以追溯到原始的 Bourne shell。'这是一个功能,而不是一个错误。
回答by Ken
tmp=$(tail -1 file.txt); echo $tmp > file.txt;
tmp=$(tail -1 文件.txt); echo $tmp > file.txt;
回答by m104
This works nicely in a Linux shell:
这在 Linux shell 中运行良好:
replace_with_filter() {
local filename=""; shift
local dd_output byte_count filter_status dd_status
dd_output=$("$@" <"$filename" | dd conv=notrunc of="$filename" 2>&1; echo "${PIPESTATUS[@]}")
{ read; read; read -r byte_count _; read filter_status dd_status; } <<<"$dd_output"
(( filter_status > 0 )) && return "$filter_status"
(( dd_status > 0 )) && return "$dd_status"
dd bs=1 seek="$byte_count" if=/dev/null of="$filename"
}
replace_with_filter file.txt tail -1
dd's "notrunc" option is used to write the filtered contents back, in place, while ddis needed again (with a byte count) to actually truncate the file. If the new file size is greater or equal to the old file size, the second ddinvocation is not necessary.
dd的“notrunc”选项用于将过滤后的内容写回原位,而dd再次需要(使用字节数)来实际截断文件。如果新文件大小大于或等于旧文件大小,dd则不需要第二次调用。
The advantages of this over a file copy method are: 1) no additional disk space necessary, 2) faster performance on large files, and 3) pure shell (other than dd).
与文件复制方法相比,这种方法的优点是:1) 不需要额外的磁盘空间,2) 在大文件上的性能更快,以及 3) 纯 shell(除了 dd)。
回答by ony
Just for this case it's possible to use
对于这种情况,可以使用
cat < file.txt | (rm file.txt; tail -1 > file.txt)这将在连接“cat”与“(...)”中的子shell之前打开“file.txt”。“rm file.txt”将在子shell打开它以写入“tail”之前从磁盘中删除引用,但内容仍然可以通过传递给“cat”的打开的描述符访问,直到它关闭stdin。所以你最好确保这个命令会完成,否则“file.txt”的内容会丢失
回答by ghostdog74
echo "$(tail -1 file.txt)" > file.txt
回答by wnoise
As Lewis Baumstark says, it doesn't like it that you're writing to the same filename.
正如 Lewis Baumstark 所说,您不喜欢写入相同的文件名。
This is because the shell opens up "file.txt" and truncates it to do the redirection before "cat file.txt" is run. So, you have to
这是因为在运行“cat file.txt”之前,shell 打开“file.txt”并截断它以进行重定向。所以,你必须
tail -1 file.txt > file2.txt; mv file2.txt file.txt
回答by dsm
tail -1 > file.txtwill overwrite your file, causing cat to read an empty file because the re-write will happen beforeany of the commands in your pipeline are executed.
tail -1 > file.txt将覆盖您的文件,导致 cat 读取一个空文件,因为重写将在执行管道中的任何命令之前发生。

