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

How can I use a file in a command and redirect output to the same file without truncating it?

提问人:mike 提问时间:7/15/2011 最后编辑:Chris Stryczynskimike 更新时间:7/20/2022 访问量:70819

问:

基本上,我想从文件中获取输入文本,从该文件中删除一行,然后将输出发送回同一文件。如果这样的话,可以更清楚地了解这些内容。

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

但是,当我这样做时,我最终会得到一个空白文件。 有什么想法吗?

bash 重定向 io

评论

1赞 codeforester 4/5/2019
也请看这个:如何使在同一管道中读取和写入相同的文件总是“失败”?在Unix和Linux SO上。
1赞 tripleee 4/3/2022
这里的几个答案是重复的,有几个删除的答案建议添加一个管道,这当然根本没有帮助。在添加新答案之前,请查看现有答案,并在提出任何新解决方案之前对其进行测试。grep 'moo' file | cat >file

答:

23赞 Manny D 7/15/2011 #1

请改用 sed:

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

评论

2赞 c00kiemon5ter 7/15/2011
iirc 是 GNU 唯一的扩展,只是注意。-i
6赞 tripleee 11/9/2017
在 *BSD(以及 OSX)上,您可以这么说扩展不是严格强制性的,但该选项确实需要一些参数。-i ''-i
112赞 c00kiemon5ter 7/15/2011 #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}

像这样,可以考虑使用 来创建 tmpfile,但请注意它不是 POSIX。mktemp

评论

60赞 glenn jackman 7/15/2011
你不能这样做的原因是:bash 首先处理重定向,然后执行命令。所以当 grep 查看file_name时,它已经是空的了。
1赞 Razvan 9/11/2015
@glennjackman:“进程重定向”是指在>的情况下,它会打开文件并清除它,而在>>的情况下,它只会打开它“?
2赞 glenn jackman 9/11/2015
是的,但请注意,在这种情况下,重定向将在 shell 启动之前打开文件并将其截断。>grep
0赞 vlz 2/1/2020
取而代之的是,应该接受使用 sponge 命令的答案
0赞 pistache 5/18/2020
完全可以通过重定向来做到这一点,您只需要在写入文件之前删除文件即可。
136赞 Lynch 7/15/2011 #3

使用海绵完成此类任务。它是 moreutils 的一部分。

请尝试以下命令:

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

评论

8赞 Anthony Panozzo 2/6/2013
谢谢你的回答。作为一个可能有用的补充,如果您在 Mac 上使用自制软件,可以使用 .brew install moreutils
6赞 Jonah 8/16/2014
或者在基于 Debian 的系统上。sudo apt-get install moreutils
4赞 netdigger 5/25/2015
该死的!感谢您向我介绍moreutils =)那里有一些不错的程序!
5赞 user107172 12/28/2016
注意,“海绵”是破坏性的,所以如果你的命令有错误,你可以擦除你的输入文件(就像我第一次尝试海绵一样)。请确保您的命令有效,和/或输入文件处于版本控制之下(如果您尝试迭代以使命令正常工作)。
1赞 Alec Mev 4/8/2018
这里还有一个 的 JavaScript 实现。方便脚本等。spongepackage.json
1赞 nerx 7/15/2011 #4

还有(作为替代方案):edsed -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
6赞 w00t 10/25/2013 #5

一个行备选方案 - 将文件的内容设置为变量:

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

评论

0赞 tripleee 4/3/2022
已经出现了其他几个类似的答案,其中一些对它是如何工作的进行了更全面的讨论,以及一些警告。现代脚本绝对应该更喜欢现代语法而不是反引号,反引号在 2013 年就已经过时了。$(command substitution)
0赞 tripleee 4/3/2022
只是为了重复来自其他地方的反馈,您应该更喜欢这里的鲁棒性;这将丢失任何尾随换行符。printfecho
0赞 Zombo 5/11/2014 #6

您可以将 slurp 与 POSIX Awk 一起使用:

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

评论

1赞 tripleee 11/9/2017
也许应该指出的是,“啜饮”的意思是“将整个文件读入内存”。如果你有一个大的输入文件,也许你想避免这种情况。
8赞 kenorb 4/18/2016 #7

您不能对同一文件使用重定向运算符(或),因为它具有更高的优先级,并且它会在调用命令之前创建/截断文件。为避免这种情况,您应该使用适当的工具,例如 、 或任何其他可以将结果写入文件的工具(例如 )。>>>teespongesed -isort 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- 使用全局删除与模式匹配的线 (help :g)
  • -s- 静音模式 (man ex)
  • -c wq- 执行和命令:write:quit

你可以用它来实现相同的目的(如其他答案中已经显示的那样),但是就地 () 是非标准的 FreeBSD 扩展(在 Unix/Linux 之间可能有不同的工作方式),基本上它是一个 stream editor,而不是一个文件编辑器。请参阅:防爆模式有什么实际用途吗?sed-i

19赞 sailesh ramanam 7/4/2016 #8

试试这个简单的

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

这次您的文件不会为空:)您的输出也会打印到您的终端。

评论

1赞 Frozn 7/18/2016
我喜欢这个解决方案!如果您不希望它在终端中打印,您仍然可以将输出重定向到或类似的地方。/dev/null
9赞 ssc 2/6/2018
这也清除了此处的文件内容。这是由于 GNU/BSD 的差异吗?我在 macOS 上...
2赞 Vic 11/21/2020
不保证,与 stackoverflow.com/a/51173807/97439 相同
0赞 nextloop 8/31/2022
不适用于命令sed
-2赞 Carlos Fanelli 7/4/2018 #9

我通常使用 tee 程序来做到这一点:

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

它自行创建和删除临时文件。

评论

1赞 studgeek 12/13/2018
对不起,不能保证有效。请参见 askubuntu.com/a/752451/335781tee
-2赞 Виктор Пупкин 8/30/2018 #10

试试这个

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

cat testfile
AAA
BBB
CCC

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

评论

0赞 Rich 8/30/2018
简短的解释甚至评论可能会有所帮助。
0赞 Виктор Пупкин 8/30/2018
我认为,它之所以有效,是因为字符串外推在重定向运算符之前执行,但我不知道确切
0赞 tripleee 4/3/2022
这是前面几个答案的重复,其中一个对警告进行了更全面的讨论。简而言之,由于几个原因,这是有问题的。
7赞 Zack Morris 9/19/2018 #11

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

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
  • 命令替换去掉尾随换行符(这是 bash 等 shell 的错误/功能),因此我们应该在输出中附加一个后缀字符,并通过临时变量(如 )的参数扩展将其删除。x${v%x}
  • 使用临时变量会踩踏当前 shell 环境中任何现有变量的值,因此我们应该将整个表达式嵌套在括号中以保留以前的值。$v$v
  • 像 bash 这样的 shell 的另一个错误/功能是命令替换会从输出中去除不可打印的字符。我通过调用并用 .但被剥离了。因此,正如 Lynch 指出的那样,这个答案不应该用于二进制文件或任何使用不可打印字符的东西。nulldd if=/dev/zero bs=1 count=1 >> file_namecat file_name | xxd -pecho $(cat file_name) | xxd -p

一般的解决方案(尽管速度稍慢,内存占用更多,并且仍在剥离不可打印的字符)是:

(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)那里借用了这个答案。

评论

2赞 Lynch 9/19/2018
当然,它不适用于大文件。这不可能是一个好的解决方案,也不可能一直有效。发生的事情是 bash 首先执行命令,然后加载 的 stdout 并将其作为第一个参数放入 .当然,不可打印的变量将无法正确输出并损坏数据。不要试图将文件重定向回它自己,它不可能很好。catecho
0赞 Zack Morris 10/8/2021
下面是一个更新/更好的命令,如果您的 shell 已安装,它将取代并且是跨平台的: stackoverflow.com/a/69212059/539149spongeperl cat file_name.txt | grep -v 'seg[0-9]\{1,\}\.[0-9]\{1\}' | perl -spe'open(STDOUT, ">", $o)' -- -o=file_name.txt
3赞 Mike Nakis 4/12/2019 #12

以下内容将完成与此相同的操作,而无需:spongemoreutils

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

该部件在不进行任何洗牌的情况下进行操作,因此它将在不更改输入的情况下缓冲您的输入。--random-source=/dev/zeroshuf

但是,出于性能原因,最好使用临时文件。所以,这是我编写的一个函数,它将以通用的方式为您做到这一点:

# 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"
}

评论

1赞 Charles Duffy 1/21/2021
$*真的需要.否则,将变得相同。除此之外,这个答案很棒。"$@"siphon "two words"siphon "two" "words"
1赞 Charles Duffy 1/21/2021
...另外,考虑告诉在与输出文件所在的目录相同的目录中创建临时文件;如果这两个位置位于不同的文件系统上,则不会是原子的。 是一种快速/简单的方法。mktempmvlocal tmp=$(mktemp "$1.XXXXXX")
0赞 Charles Duffy 1/21/2021
(另外,考虑创建自己的行;这种方式将通过 的退出状态,这样你就可以检测到它失败的情况并采取适当的行动;例如,如果不成功,则中止函数的其余部分;这与同一行上的 preceding 不起作用,因为它本身有自己的退出状态并覆盖)。local tmp filetmp=$(mktemp)mktemptmp=$(mktemp) || returnmktemplocallocal$?
0赞 Charles Duffy 1/21/2021
我还建议不要错误地将以破折号开头的文件名解析为 .见 pubs.opengroup.org/onlinepubs/9699919799/basedefs/...,准则10。mv -- "$tmp" "$file"mv
0赞 Charles Duffy 1/21/2021
想想没有先例;是一个不符合 POSIX 的 kshism(它在 bash 中的行为方式与在 ksh 中的行为方式不同,它修改了变量声明在函数体中的行为方式)。它比 ,它不兼容 POSIX sh 或旧版 ksh,但比完全不兼容更糟糕。siphon() {functionfunction siphon {function siphon() {siphon() {function
7赞 pistache 5/18/2020 #13

这是很有可能的,你只需要确保在你编写输出时,你正在把它写到一个不同的文件中。这可以通过在打开文件描述符后但在写入文件之前删除文件来完成:

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,并且您正在处理的文件太大而无法在磁盘上容纳两次,则可以查看此脚本,该脚本将逐个块地将文件通过管道传递到指定的命令,同时取消分配已处理的块。与往常一样,请阅读使用情况页面中的警告。

2赞 Alice M. 7/20/2022 #14

在我遇到的大多数情况下,这都做得很好:

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

请注意,虽然去掉了尾随换行符,但确保了最终的换行符,因此通常结果是令人满意的。 (如果您想了解更多信息,请在其中查找“Here Strings”。$(…)<<<man bash

完整示例:

#! /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
}

评论

2赞 Henrique Capozzi 9/13/2022
我真的很喜欢这个答案,非常感谢!