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


95

基本上,我想将文件中的文本作为输入文本,从该文件中删除一行,然后将输出发送回相同的文件。如果可以更清楚地理解这些方面的话。

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

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


Answers:


84

您不能这样做,因为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。


47
之所以不能这样做,是因为bash首先处理重定向,然后执行命令。因此,当grep查看file_name时,它已经为空。
glenn jackman 2011年

1
@glennjackman:通过“处理重定向,您的意思是在>的情况下打开文件并清除它,在>>的情况下仅打开文件”?
拉兹万2015年

2
是的,但是在这种情况下,值得注意的是,>重定向将在外壳启动之前打开文件并截断​​它grep
glenn jackman

1
如果您不想使用临时文件,请参阅我的回答,但是请不要对此评论打分。
扎克·莫里斯

取而代之的是,应该接受使用sponge命令答案
vlz

95

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

试试这个命令:

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

4
感谢你的回答。作为可能有用的补充,如果您在Mac上使用自制软件,可以使用brew install moreutils
Anthony Panozzo

2
sudo apt-get install moreutils在基于Debian的系统上。
约拿(Jonah)2014年

3
该死的!感谢您向我介绍moreutils =)一些不错的程序!
netigger 2015年

非常感谢,moreutils的救助!海绵像老板!
aqquadro

3
请注意,“海绵”是破坏性的,因此,如果您的命令中有错误,则可以清除输入文件(就像我第一次尝试海绵一样)。如果您尝试迭代使命令起作用,请确保您的命令起作用,并且/或者输入文件受版本控制。
user107172 '16

18

使用sed代替:

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

1
iirc -i是GNU唯一的扩展,只是注意。
c00kiemon5ter 2011年

3
在* BSD(以及OSX)上,您可以说-i ''扩展名不是严格必需的,但是该-i选项确实需要一些参数。
三胞胎

13

试试这个简单的

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

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


1
我喜欢这个解决方案!而且,如果您不希望将其打印在终端中,仍然可以将输出重定向到/dev/null或类似的地方。
Frozn

4
这也会清除此处的文件内容。那是由于GNU / BSD的不同吗?我正在使用macOS ...
ssc

7

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

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


6

一种衬板替代方案-将文件的内容设置为变量:

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

4

由于此问题是搜索引擎中排名最高的结果,因此这是一个基于https://serverfault.com/a/547331的单行代码,它使用子外壳而不是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不会引起不良行为。
  • 命令替换条尾随换行符(这是诸如bash壳的一个错误/特征),所以我们应该追加一个后缀字符像x的输出,并通过删除它在外面临时变量的参数扩展${v%x}
  • 使用临时变量$v$v在当前shell环境中破坏任何现有变量的值,因此我们应将整个表达式嵌套在括号中以保留先前的值。
  • 像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指出的那样,此答案应用于二进制文件或任何使用不可打印字符的东西。

通用的解决方案(稍微慢一些,占用更多的内存,并且仍然剥离不可打印的字符)是:

(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

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

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


2
当然,它不适用于大文件。这不可能是一个好的解决方案,也不能一直工作。发生的情况是bash首先执行命令,然后加载stdout cat并将其作为第一个参数echo。当然,不可打印的变量将无法正确输出并破坏数据。不要尝试将文件重定向回自身,这根本不好。
林奇

1

还有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

1

您可以使用process-substitution做到这一点。

虽然bash异步打开所有管道,但是这有点hack,我们必须使用sleepYMMV 来解决它。

在您的示例中:

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写输出

1

您可以在POSIX Awk中使用slurp:

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


1
也许应该指出,“ slurp”的意思是“将整个文件读入内存”。如果输入文件很大,也许您想避免这种情况。
Tripleee

0

试试这个

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

cat testfile
AAA
BBB
CCC

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

简短的解释甚至评论可能会有所帮助。
Rich

我想,这工作,因为串外推重定向操作符之前执行,但我不知道究竟
ВикторПупкин

0

以下将完成相同的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

function siphon
{
    local tmp=$(mktemp)
    local file="$1"
    shift
    $* < "$file" > "$tmp"
    mv "$tmp" "$file"
}

0

这是很有可能的,您只需要确保在编写输出时就将其写入另一个文件即可。这可以通过在打开文件描述符之后但在写入文件之前删除文件来完成:

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


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.