如何在命令中使用文件并将输出重定向到同一个文件而不截断它?

基本上,我想从一个文件中获取输入文本,从该文件中删除一行,然后将输出发送回同一个文件。如果这些能让你更清楚的话。

grep -v 'seg[0-9]\{1,\}\.[0-9]\{1\}' file_name > file_name

然而,当我这样做时,我得到的是一个空白文件。 有什么想法吗?

64924 次浏览

使用 sed 代替:

sed -i '/seg[0-9]\{1,\}\.[0-9]\{1\}/d' file_name

您不能这样做,因为 bash 首先处理重定向,然后执行命令。因此,当 grep 查看 file _ name 时,它已经是空的。但是您可以使用一个临时文件。

#!/bin/sh
tmpfile=$(mktemp)
grep -v 'seg[0-9]\{1,\}\.[0-9]\{1\}' file_name > ${tmpfile}
cat ${tmpfile} > file_name
rm -f ${tmpfile}

像这样,考虑使用 mktemp来创建 Tmpfile,但要注意它不是 POSIX。

Use 海绵 for this kind of tasks. Its part of moreutils.

试试这个命令:

 grep -v 'seg[0-9]\{1,\}\.[0-9]\{1\}' file_name | sponge file_name

还有 ed(作为 sed -i的替代品) :

# cf. http://wiki.bash-hackers.org/howto/edit-ed
printf '%s\n' H 'g/seg[0-9]\{1,\}\.[0-9]\{1\}/d' wq |  ed -s file_name

一种替代方法——将文件内容设置为变量:

VAR=`cat file_name`; echo "$VAR"|grep -v 'seg[0-9]\{1,\}\.[0-9]\{1\}' > file_name

你可以在 POSIX Awk 中使用 slurp:

!/seg[0-9]\{1,\}\.[0-9]\{1\}/ {
q = q ? q RS $0 : $0
}
END {
print q > ARGV[1]
}

例子

不能对同一个文件使用重定向操作符(>>>) ,因为它的优先级更高,甚至在调用命令之前就会创建/截断文件。为了避免这种情况,应该使用适当的工具,例如 teespongesed -i或任何其他可以将结果写入文件的工具(例如 sort file -o file)。

基本上,将输入重定向到同一个原始文件是没有意义的,你应该为此使用适当的就地编辑器,例如 Ex 编辑器(Vim 的一部分) :

ex '+g/seg[0-9]\{1,\}\.[0-9]\{1\}/d' -scwq file_name

地点:

  • '+cmd'/-c - run any Ex/Vim command
  • 使用 global(help :g)删除匹配模式的行
  • 静音模式(man ex)
  • 执行 :write:quit命令

你可以使用 sed来达到同样的效果(如其他答案所示) ,但是 in-place(-i)是非标准的 FreeBSD 扩展(可能在 Unix/Linux 之间工作方式不同) ,基本上它是一个 <是的trong>是的tream ed编辑器,而不是一个文件编辑器。见: Ex 模式有什么实际用途吗?

试试这个简单的

grep -v 'seg[0-9]\{1,\}\.[0-9]\{1\}' file_name | tee file_name

这次您的文件不会是空白的:)并且您的输出也将打印到您的终端。

我通常使用 T 恤程序这样做:

grep -v 'seg[0-9]\{1,\}\.[0-9]\{1\}' file_name | tee file_name

它自己创建和删除一个临时文件。

Try this

echo -e "AAA\nBBB\nCCC" > testfile


cat testfile
AAA
BBB
CCC


echo "$(grep -v 'AAA' testfile)" > testfile
cat testfile
BBB
CCC

由于这个问题在搜索引擎中排名第一,这里有一个基于 https://serverfault.com/a/547331的一行程序,它使用子 shell 而不是 sponge(它通常不是像 OS X 那样的普通安装的一部分) :

echo "$(grep -v 'seg[0-9]\{1,\}\.[0-9]\{1\}' file_name)" > file_name

The general case is:

echo "$(cat file_name)" > file_name

编辑,上面的解决方案有一些警告:

  • printf '%s' <string> should be used instead of echo <string> so that files containing -n don't cause undesired behavior.
  • Command substitution strips trailing newlines (这是 bash 等 shell 的一个 bug/特性) so we should append a postfix character like x to the output and remove it on the outside via 临时变量的参数展开 like ${v%x}.
  • 使用临时变量 $v会压缩当前 shell 环境中任何现有变量 $v的值,因此我们应该将整个表达式嵌套在括号中,以保留以前的值。
  • Bash 等 shell 的另一个 bug/特性是,指令替代从输出中删除了像 null这样不可打印的字符。我通过调用 dd if=/dev/zero bs=1 count=1 >> file_name并使用 cat file_name | xxd -p在十六进制中查看它来验证这一点。但是 echo $(cat file_name) | xxd -p被剥离了。因此,这个答案应该 没有用于二进制文件或任何使用不可打印字符,如 Lynch pointed out

The general solution (albiet slightly slower, more memory intensive and still stripping unprintable characters) is:

(v=$(cat file_name; printf x); printf '%s' ${v%x} > file_name)

来自 https://askubuntu.com/a/752451的测试:

printf "hello\nworld\n" > file_uniquely_named.txt && for ((i=0; i<1000; i++)); do (v=$(cat file_uniquely_named.txt; printf x); printf '%s' ${v%x} > file_uniquely_named.txt); done; cat file_uniquely_named.txt; rm file_uniquely_named.txt

应该打印:

hello
world

而在当前 shell 中调用 cat file_uniquely_named.txt > file_uniquely_named.txt:

printf "hello\nworld\n" > file_uniquely_named.txt && for ((i=0; i<1000; i++)); do cat file_uniquely_named.txt > file_uniquely_named.txt; done; cat file_uniquely_named.txt; rm file_uniquely_named.txt

打印一个空字符串。

我还没有在大文件(可能超过2或4 GB)上测试过。

我从 Hart SimhaKos借用了这个答案。

下面的步骤将完成 sponge所做的同样的事情,而不需要 moreutils:

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

--random-source=/dev/zero部分欺骗 shuf做它的事情而不做任何洗牌,所以它将缓冲您的输入而不改变它。

但是,由于性能原因,使用临时文件确实是最好的。这是我写的一个函数,它可以用一种通用的方式来实现:

# Pipes a file into a command, and pipes the output of that command
# back into the same file, ensuring that the file is not truncated.
# Parameters:
#    $1: the file.
#    $2: the command. (With $3... being its arguments.)
# See https://stackoverflow.com/a/55655338/773113


siphon()
{
local tmp file rc=0
[ "$#" -ge 2 ] || { echo "Usage: siphon filename [command...]" >&2; return 1; }
file="$1"; shift
tmp=$(mktemp -- "$file.XXXXXX") || return
"$@" <"$file" >"$tmp" || rc=$?
mv -- "$tmp" "$file" || rc=$(( rc | $? ))
return "$rc"
}

这是非常有可能的,您只需要确保在写入输出时,您正在将其写入另一个文件。这可以通过在打开文件描述符之后,但在写入文件之前删除该文件来实现:

exec 3<file ; rm file; COMMAND <&3 >file ;  exec 3>&-

或者一行一行的来更好的理解它:

exec 3<file       # open a file descriptor reading 'file'
rm file           # remove file (but fd3 will still point to the removed file)
COMMAND <&3 >file # run command, with the removed file as input
exec 3>&-         # close the file descriptor

这仍然是一件危险的事情,因为如果 COMMAND 不能正常运行,就会丢失文件内容。如果 COMMAND 返回非零退出代码,可以通过还原文件来减轻这种情况:

exec 3<file ; rm file; COMMAND <&3 >file || cat <&3 >file ; exec 3>&-

We can also define a shell function to make it easier to use :

# Usage: replace FILE COMMAND
replace() { exec 3<$1 ; rm $1; ${@:2} <&3 >$1 || cat <&3 >$1 ; exec 3>&- }

例如:

$ echo aaa > test
$ replace test tr a b
$ cat test
bbb

Also, note that this will keep a full copy of the original file (until the third file descriptor is closed). If you're using Linux, and the file you're processing on is too big to fit twice on the disk, you can check out 这个剧本 that will pipe the file to the specified command block-by-block while unallocating the already processed blocks. As always, read the warnings in the usage page.

在我遇到的大多数情况下,这种方法都能很好地解决问题:

cat <<< "$(do_stuff_with f)" > f

请注意,虽然 $(…)带尾换行,<<< 确保最后换行,所以一般的结果是神奇的满意。 (如果你想了解更多,可以在 man bash中查找“ Here Strings”。)

完整的例子:

#! /usr/bin/env bash


get_new_content() {
sed 's/Initial/Final/g' "${1:?}"
}


echo 'Initial content.' > f
cat f


cat <<< "$(get_new_content f)" > f


cat f

这不会截断文件并生成:

Initial content.
Final content.

注意,我在这里使用一个函数是为了清晰和可扩展性,但这不是必需的。

一个常见的用例是 JSON 版本:

echo '{ "a": 12 }' > f
cat f
cat <<< "$(jq '.a = 24' f)" > f
cat f

结果是:

{ "a": 12 }
{
"a": 24
}