从文件中删除行直到模式,除非模式不存在

Gre*_*ill 6 sed awk text-processing filter

我正在尝试使用类似sedawk执行以下操作编写过滤器:

  • 如果输入中不存在给定模式,则将整个输入复制到输出
  • 如果输入中存在模式,则仅将第一次出现后的行复制到输出中

这恰好适用于“git clean”过滤器,但这可能并不重要。重要的方面是这需要作为过滤器来实现,因为输入是在 stdin 上提供的。

我知道如何使用sed删除模式的行,例如。1,/pattern/d但是如果/pattern/在任何地方都不匹配,则会删除整个输入。

我可以想象编写一个完整的 shell 脚本来创建一个临时文件,执行某个操作grep -q或其他操作,然后决定如何处理输入。如果可能的话,我更愿意在不乱创建临时文件的情况下执行此操作。这需要高效,因为 git 可能会频繁调用它。

ter*_*don 6

如果您的文件不是太大而无法放入内存,您可以使用 perl 来处理文件:

perl -0777pe 's/.*?PAT[^\n]*\n?//s' file
Run Code Online (Sandbox Code Playgroud)

只需更改PAT为您所追求的任何模式即可。例如,给定这两个输入文件和模式5

$ cat file
1
2
3
4
5
11
12
13
14
15
$ cat file1 
foo
bar
$ perl -0777pe 's/.*?5[^\n]*\n?//s' file
11
12
13
14
15
$ perl -0777pe 's/.*?10[^\n]*\n?//s' file1
foo
bar
Run Code Online (Sandbox Code Playgroud)

解释

  • -pe: 逐行读取输入文件,将给出的脚本应用-e到每一行并打印。
  • -0777 : 将整个文件放入内存。
  • s/.*?PAT[^\n]*\n?//s: 删除所有内容,直到第一次出现PAT和直到行尾。

对于较大的文件,我看不出有任何方法可以避免读取文件两次。就像是:

awk -vpat=5 '{
              if(NR==FNR){
                if($0~pat && !a){a++; next} 
                if(a){print}
              }
              else{ 
                if(!a){print}
                else{exit} 
              }
             }' file1 file1
Run Code Online (Sandbox Code Playgroud)

解释

  • awk -vpat=5: 运行awk并将变量设置pat5.
  • if(NR==FNR){} : 如果这是第一个文件。
  • if($0~pat && !a){a++; next}: 如果这一行与 和 的值相匹配,pat并且a未定义,则加a一并跳到下一行。
  • if(a){print}: 如果a已定义(如果此文件与模式匹配),则打印该行。
  • else{ } :如果这不是第一个文件(所以它是第二遍)。
  • if(!a){print}如果a没有定义,我们想要整个文件,所以打印每一行。
  • else{exit}: 如果a已定义,我们已经在第一遍打印,因此无需重新处理文件。


mik*_*erv 5

GNUgrep; cat :

{   grep -m1 'pattern' && 
    cat || ! cat ./infile
}   <./infile
Run Code Online (Sandbox Code Playgroud)

POSIXsed; cat :

{ sed -ne'/PATTERN/q;H;1h;$!d;x;p'; cat; } <infile
Run Code Online (Sandbox Code Playgroud)

GNUsed; cat :

{ sed -une'/PATTERN/q;H;1h;$!d;x;p'; cat; } <infile
Run Code Online (Sandbox Code Playgroud)

(只需添加-u


分享很好

上述所有命令都有效,因为它们read()是它们的父进程的文件描述符——shell 的。它确实open()和它的孩子继承了它的描述符。在这里,他们都在标准输入上解决了这个问题。关于文件描述符与几乎所有其他类型的继承环境不同的一件事是子进程可以影响父环境的文件描述符。

这些都需要一个常规的,lseek()-able ./infile- (不包括 GNUsed-unbuffered模式)。这是因为这些进程中的每一个仍然会做一些缓冲,但是当他们完成他们的任务时,他们会将lseek()描述符返回到他们影响它的最后一点。否则很难让事情正确排列(尽管dd可以用来实现这一点)

并且因为相同的描述符也被传递给shell 调用的下一个子进程,并且最后一个子进程已经改变了它的偏移量,所以下一个命令将在最后一个停止的地方立即开始它的输入。所以当我们...

seq 10 >nums
{   grep -m1 5; cat; } <nums
Run Code Online (Sandbox Code Playgroud)

grep5在仅打印找到的匹配项后退出第一个匹配项的输入,并cat在 5 后面的换行符之后开始将 stdin 复制到 stdout。

5
6
7
8
9
10
Run Code Online (Sandbox Code Playgroud)

另一件事grep是,如果在输入中找到匹配项,它的返回值可以很容易地向我们揭示......

5
6
7
8
9
10
Run Code Online (Sandbox Code Playgroud)

... where grep's 如果找到任何匹配项,则返回 0&& cat从那里接管,||否则不同的cat副本将整个./infile输出。


一些grep例子


{   grep -m1 pattern && 
    cat || ! cat ./infile
}   <./infile
Run Code Online (Sandbox Code Playgroud)
89
90
91
92
93
94
95
96
97
98
99
100
Run Code Online (Sandbox Code Playgroud)

grep的返回向我们揭示了它是否已经消耗了所有的标准输入。如果它返回真,那么机会非常好cat还有一些剩余的工作(唯一不会的情况是如果grep在最后一行输入中找到它的第一个匹配项,在这种情况下,根据您的规则,它应该无论如何都不要打印任何东西)。但是如果它消耗整个流寻找匹配并失败,它将返回 false,因此第二个||cat将只打印整个文件。

像这样:

seq 100 >nums
only_after()(
    [ -f "$1" ] && {
    >/dev/null \
    grep -m1 "$2" &&
    cat  ||! cat "$1"
} <"$1")
only_after nums '[89]\{2\}'
Run Code Online (Sandbox Code Playgroud)
89
90
91
92
93
94
95
96
97
98
99
100
Run Code Online (Sandbox Code Playgroud)

一些sed例子

seq 5 >nums
only_after nums 8; echo return: "$?"
Run Code Online (Sandbox Code Playgroud)
191
192
193
194
195
196
197
198
199
200
Run Code Online (Sandbox Code Playgroud)

...它在sedH旧空间中堆叠每个输入行,直到PATTERN找到并sed完全退出输入以将其余部分留给cat,或者$找到最后一行,此时会sed p打印它保存的所有内容。像这样:

1
2
3
4
5
return: 1
Run Code Online (Sandbox Code Playgroud)
1
2
3
4
5
6
7
8
9
10
Run Code Online (Sandbox Code Playgroud)

不幸的是,根据内存可用性和sed实现,它很容易内爆。此外,sed除非您将 GNU切换到-unbuffered 模式,否则它通常不会与其他人很好地配合,这会对性能产生非常有害的影响。sed另一方面,POSIX被规范以这种方式很好地发挥作用,因此它绝对是一条值得探索的途径。

对于不可lseek()输入(例如管道),以下可以类似地工作:

seq 200 >nums
{ sed -une'/190/q;H;1h;$!d;x;p'; cat; } <nums
Run Code Online (Sandbox Code Playgroud)
196
197
198
199
200
Run Code Online (Sandbox Code Playgroud)

...或者...

191
192
193
194
195
196
197
198
199
200
Run Code Online (Sandbox Code Playgroud)
1
2
3
Run Code Online (Sandbox Code Playgroud)

就地编辑


如果您想替换./infile- 换句话说,就地编辑- 那么您实际上可以通过首先将其缓冲到临时文件来覆盖它:

{   g=$(grep -m1  pattern) &&
    cut -c2- <<IN >./infile
$(  printf " %s\n" "$g"    &&
    paste -d\  /dev/null -  )
IN
} <./infile
Run Code Online (Sandbox Code Playgroud)

...如果找不到模式,它根本不会采取任何行动 - 因此永远不会读取./infile超过一次 - 但对于成功的匹配,./infile在写入之前总是将处理过的尾部完全缓冲到临时文件中./infile。更具体地说 - infile 中唯一被写出到 shell 的 here-document 的部分是匹配之后 的部分grepgrep在匹配中消耗的所有输入都保持消耗状态,因此只有缓冲的尾端最终进入临时缓冲区。

更重要的是,大多数 shell 会支持他们的 here-documents /tmp- 因为在 Linux 系统上/tmp通常是tmpfs一个,这意味着在所述系统上,缓冲部分根本不会在磁盘上结束。但是,公平地说,由于内核处理文件缓存等的方式,将它写入 tmpfs 和将它写入其他任何地方可能没有太大区别,只要您有内存来缓存它。/tmp可能只是更明确一点。在unlink()向其写入字节之前,shell 也是缓冲区文件——因此,只要它的读和/或写描述符保持打开状态,它就完全存在。没有什么可以清理的。


全部包起来


我写了一个小程序来做到这一点......

seq 10 >nums
{ sed -une'/190/q;H;1h;$!d;x;p'; cat; } <nums
Run Code Online (Sandbox Code Playgroud)

...这增加了一些选项解析等等。基本上,您可以将任何参数传递给grep您可能喜欢做的事情 -除了-ior-o或 or 中的第一个之外,所有参数都逐字传递-m

您使用-i及其参数来指定输入文件。您可以使用-o-写入标准输出 - 无论如何这是默认行为 - 或就地-o+编辑-i文件,或-o任何可写路径名。您用于-m指定匹配计数 - 也就是说,您可以在-m计数匹配后获取所有文件,或者如果在输入中找不到那么多匹配,则仅获取所有文件。第一个不是有效-[iom]开关的所有参数,或第二个出现的所有参数-[io]都直接传递给grep.

它在尝试写入之前测试请求的匹配是否成功以及输出应该去哪里。例如,如果匹配不成功并且输出被定向返回,./infile则它根本不会做任何事情而不管./infile。如果匹配成功并且 outfile 和 infile 相同,则会缩短 infile。但是如果匹配不成功并且输出被定向到其他任何地方,它只会cat输入到输出。

一个小演示:

seq 20 >nums
allor -inums -m2 5
Run Code Online (Sandbox Code Playgroud)
1
2
3
4
5
6
7
8
9
10
Run Code Online (Sandbox Code Playgroud)

...和...

seq 10 >nums
allor -inums -m2 5; echo return: "$?"
Run Code Online (Sandbox Code Playgroud)
seq 200 | sed -ne'/195/!{H;1h;$!d;x;:p' -ep -e'};n;bp'
Run Code Online (Sandbox Code Playgroud)

...和...

seq 20000 >nums
allor -m1999 -inums -o+ 5$; cat nums
Run Code Online (Sandbox Code Playgroud)
196
197
198
199
200
Run Code Online (Sandbox Code Playgroud)