如何在命令中使用文件并将输出重定向到同一个文件而不截断它?
Posted
技术标签:
【中文标题】如何在命令中使用文件并将输出重定向到同一个文件而不截断它?【英文标题】:How can I use a file in a command and redirect output to the same file without truncating it? 【发布时间】:2011-10-05 12:38:22 【问题描述】:基本上我想从文件中获取输入文本,从该文件中删除一行,然后将输出发送回同一个文件。类似这样的东西,如果这样更清楚的话。
grep -v 'seg[0-9]\1,\\.[0-9]\1\' file_name > file_name
但是,当我这样做时,我会得到一个空白文件。 有什么想法吗?
【问题讨论】:
在 Unix 和 Linux SO 上也可以看到:How to make reading and writing the same file in the same pipeline always “fail”?。 【参考方案1】:改用 sed:
sed -i '/seg[0-9]\1,\\.[0-9]\1\/d' file_name
【讨论】:
iirc-i
是 GNU 唯一的扩展,请注意。
在 *BSD(因此也包括 OSX)上,您可以说 -i ''
,因此扩展名不是严格强制的,但 -i
选项确实需要 some 参数。
【参考方案2】:
您不能这样做,因为 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。
【讨论】:
不能这样做的原因:bash 先处理重定向,然后执行命令。所以当 grep 查看 file_name 时,它已经是空的了。 @glennjackman:“进程重定向意味着在 > 的情况下它会打开文件并清除它,而在 >> 的情况下它只会打开它”? 是的,但值得注意的是,在这种情况下,>
重定向将打开文件并在 shell 启动 grep
之前将其截断。
如果您不想使用临时文件,请参阅my answer,但请不要对此评论投赞成票。
应该接受answer using the sponge
command 而不是这个。【参考方案3】:
使用sponge 处理此类任务。它是 moreutils 的一部分。
试试这个命令:
grep -v 'seg[0-9]\1,\\.[0-9]\1\' file_name | sponge file_name
【讨论】:
感谢您的回答。作为一个可能有用的补充,如果您在 Mac 上使用自制软件,可以使用brew install moreutils
。
或sudo apt-get install moreutils
在基于 Debian 的系统上。
该死!感谢您向我介绍 moreutils =) 那里有一些不错的程序!
非常感谢 moreutils 的救援!像老板一样海绵!
请注意,“海绵”具有破坏性,因此如果您的命令有错误,您可以清除输入文件(就像我第一次尝试使用海绵时所做的那样)。如果您尝试迭代以使命令正常工作,请确保您的命令有效,和/或输入文件受版本控制。【参考方案4】:
还有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
【讨论】:
【参考方案5】:一个班轮替代方案 - 将文件的内容设置为变量:
VAR=`cat file_name`; echo "$VAR"|grep -v 'seg[0-9]\1,\\.[0-9]\1\' > file_name
【讨论】:
【参考方案6】:您可以将 slurp 与 POSIX Awk 一起使用:
!/seg[0-9]\1,\\.[0-9]\1\/
q = q ? q RS $0 : $0
END
print q > ARGV[1]
Example
【讨论】:
或许应该指出,“slurp”的意思是“将整个文件读入内存”。如果你有一个大的输入文件,也许你想避免这种情况。【参考方案7】:您不能对同一个文件使用重定向运算符(>
或 >>
),因为它具有更高的优先级,并且它会在调用命令之前创建/截断文件。为避免这种情况,您应该使用适当的工具,例如 tee
、sponge
、sed -i
或任何其他可以将结果写入文件的工具(例如 sort file -o file
)。
基本上将输入重定向到同一个原始文件没有意义,您应该为此使用适当的就地编辑器,例如 Ex 编辑器(Vim 的一部分):
ex '+g/seg[0-9]\1,\\.[0-9]\1\/d' -scwq file_name
地点:
'+cmd'
/-c
- 运行任何 Ex/Vim 命令
g/pattern/d
- 使用 global (help :g
) 删除与模式匹配的行
-s
- 静音模式 (man ex
)
-c wq
- 执行 :write
和 :quit
命令
您可以使用sed
来实现相同的效果(如其他答案中所示),但是 in-place (-i
) 是非标准的 FreeBSD 扩展(在 Unix 之间可能会有所不同) /Linux),基本上它是一个 stream editor,而不是文件编辑器。见:Does Ex mode have any practical use?
【讨论】:
【参考方案8】:试试这个简单的
grep -v 'seg[0-9]\1,\\.[0-9]\1\' file_name | tee file_name
这次你的文件不会是空白的 :) 并且你的输出也会打印到你的终端。
【讨论】:
我喜欢这个解决方案!如果您不希望它在终端中打印,您仍然可以将输出重定向到/dev/null
或类似的地方。
这也会清除此处的文件内容。这是由于 GNU/BSD 的不同吗?我在 macOS 上...
不保证,同***.com/a/51173807/97439【参考方案9】:
您可以使用process-substitution 来做到这一点。
虽然 bash 异步打开所有管道,但我们必须使用 sleep
所以 YMMV 来解决这个问题。
在你的例子中:
grep -v 'seg[0-9]\1,\\.[0-9]\1\' file_name > >(sleep 1 && cat > file_name)
>(sleep 1 && cat > file_name)
创建一个临时文件,接收来自 grep 的输出
sleep 1
延迟一秒钟,让 grep 有时间解析输入文件
最后cat > file_name
写入输出
【讨论】:
【参考方案10】:我通常使用 tee 程序来执行此操作:
grep -v 'seg[0-9]\1,\\.[0-9]\1\' file_name | tee file_name
它自己创建和删除一个临时文件。
【讨论】:
抱歉,tee
不能保证有效。见askubuntu.com/a/752451/335781。【参考方案11】:
试试这个
echo -e "AAA\nBBB\nCCC" > testfile
cat testfile
AAA
BBB
CCC
echo "$(grep -v 'AAA' testfile)" > testfile
cat testfile
BBB
CCC
【讨论】:
简短的解释甚至 cmets 可能会有所帮助。 我认为,它起作用是因为字符串外推在重定向运算符之前执行,但我不知道确切【参考方案12】:由于这个问题是搜索引擎中的最高结果,这里有一个基于https://serverfault.com/a/547331 的单行代码,它使用子shell 而不是sponge
(通常不是像OS X 这样的香草安装的一部分):
echo "$(grep -v 'seg[0-9]\1,\\.[0-9]\1\' file_name)" > file_name
一般情况是:
echo "$(cat file_name)" > file_name
编辑,上述解决方案有一些注意事项:
应使用printf '%s' <string>
而不是echo <string>
,以便包含-n
的文件不会导致不良行为。
命令替换会去除尾随换行符 (this is a bug/feature of shells like bash),因此我们应该在输出中附加一个后缀字符,如 x
,并通过 parameter expansion of a temporary variable 将其从外部删除,如 $v%x
。
使用临时变量 $v
会在当前 shell 环境中删除任何现有变量 $v
的值,因此我们应该将整个表达式嵌套在括号中以保留之前的值。
像 bash 这样的 shell 的另一个错误/功能是命令替换会从输出中去除不可打印的字符,如 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。
一般的解决方案(虽然速度稍慢,内存占用更多,并且仍然会去除不可打印的字符)是:
(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 Simha 和kos 借用了这个答案。
【讨论】:
当然它不适用于大文件。这不可能是一个好的解决方案或一直有效。发生的事情是 bash 首先执行命令,然后加载cat
的标准输出并将其作为echo
的第一个参数。当然,不可打印的变量将无法正确输出并破坏数据。不要试图将文件重定向回它自己,它不会是好的。
这是一个更新/更好的命令,它取代了sponge
,如果你的 shell 安装了perl
,它是跨平台的:***.com/a/69212059/539149cat file_name.txt | grep -v 'seg[0-9]\1,\\.[0-9]\1\' | perl -spe'open(STDOUT, ">", $o)' -- -o=file_name.txt
【参考方案13】:
以下将完成与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://***.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"
【讨论】:
$*
确实需要是 "$@"
。否则,siphon "two words"
变为相同的siphon "two" "words"
。除此之外,这个答案很棒。
...另外,考虑告诉mktemp
在与输出文件所在的目录相同的目录中创建临时文件;如果这两个位置位于不同的文件系统上,mv
将不是原子的。 local tmp=$(mktemp "$1.XXXXXX")
是一种快速/简单的方法。
(另外,考虑让local tmp file
成为自己的行;这样tmp=$(mktemp)
将通过mktemp
的退出状态,因此您可以检测到它失败的情况并采取适当的行动;例如,tmp=$(mktemp) || return
如果mktemp
不成功,则中止函数的其余部分;这不适用于同一行前面的local
,因为local
本身有自己的退出状态并覆盖@ 987654342@).
我还建议mv -- "$tmp" "$file"
,这样以破折号开头的文件名不会被错误地解析为mv
的选项。请参阅pubs.opengroup.org/onlinepubs/9699919799/basedefs/…,准则 10。
想想siphon()
,前面没有function
; function siphon
是不符合 POSIX 的 kshism(它在 bash 中的行为与在 ksh 中的行为不同,它修改了变量声明在函数体中的行为方式)。它比function siphon()
好,后者与要么 POSIX sh 或旧版ksh 不兼容,但比完全没有function
的siphon()
差。【参考方案14】:
这很有可能,您只需要确保在编写输出时,您正在将其写入不同的文件。这可以通过在打开文件描述符之后但在写入文件之前删除文件来完成:
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>&-
我们还可以定义一个 shell 函数来使其更易于使用:
# 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
另外,请注意,这将保留原始文件的完整副本(直到第三个文件描述符关闭)。如果您使用的是 Linux,并且您正在处理的文件太大而无法在磁盘上放置两次,您可以查看this script,它会将文件逐块传送到指定的命令,同时取消分配已经处理过的块。与往常一样,请阅读使用页面中的警告。
【讨论】:
以上是关于如何在命令中使用文件并将输出重定向到同一个文件而不截断它?的主要内容,如果未能解决你的问题,请参考以下文章