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

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

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

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

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

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

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

答案1

如果您的文件不是太大而无法容纳在内存中,您可以使用 perl 来读取文件:

perl -0777pe 's/.*?PAT[^\n]*\n?//s' file

只需更改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

解释

  • -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

解释

  • 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已定义,我们已经在第一遍中打印,因此无需重新处理文件。

答案2

GNUgrep; cat:

{   grep -m1 'pattern' && 
    cat || ! cat ./infile
}   <./infile

POSIXsed; cat:

{ sed -ne'/PATTERN/q;H;1h;$!d;x;p'; cat; } <infile

GNUsed; cat:

{ sed -une'/PATTERN/q;H;1h;$!d;x;p'; cat; } <infile

(只需添加-u


分享很好

上述所有命令之所以有效,是因为它们read()是其父进程的文件描述符 - shell 的文件描述符。它open()及其子级继承了它的描述符。在这里他们都在标准输入上解决它。关于文件描述符的一件事往往与几乎所有其他类型的继承环境不同,那就是子进程影响父环境的文件描述符。

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

并且因为相同的描述符也传递给下一个shell 调用的子进程,并且最后一个子进程已更改其偏移量,则下一个命令将立即从最后一个子进程停止的位置开始输入。所以当我们...

seq 10 >nums
{   grep -m1 5; cat; } <nums

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

5
6
7
8
9
10

另一件事是grep,如果在输入中找到匹配项,它的返回值可以轻松地告诉我们......

{   grep -m1 pattern && 
    cat || ! cat ./infile
}   <./infile

...grep如果找到任何匹配项,则返回 0 && cat||否则将cat复制整个./infile来输出。


一些grep例子


seq 100 >nums
only_after()(
    [ -f "$1" ] && {
    >/dev/null \
    grep -m1 "$2" &&
    cat  ||! cat "$1"
} <"$1")
only_after nums '[89]\{2\}'

89
90
91
92
93
94
95
96
97
98
99
100

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

像这样:

seq 5 >nums
only_after nums 8; echo return: "$?"

1
2
3
4
5
return: 1

一些sed例子

seq 200 >nums
{ sed -une'/190/q;H;1h;$!d;x;p'; cat; } <nums

191
192
193
194
195
196
197
198
199
200

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

seq 10 >nums
{ sed -une'/190/q;H;1h;$!d;x;p'; cat; } <nums

1
2
3
4
5
6
7
8
9
10

不幸的是,它很容易崩溃,具体取决于内存可用性和sed实现。此外,GNUsed通常不会与其他软件很好地配合,除非您将其切换到-unbuffered 模式,这会对性能产生相当有害的影响。sed另一方面,POSIX被指定以这种方式很好地发挥作用,因此它绝对是一个值得探索的途径。

对于一个非lseek()- 可输入(例如管道)以下可以类似地工作:

seq 200 | sed -ne'/195/!{H;1h;$!d;x;:p' -ep -e'};n;bp'

196
197
198
199
200

...或者...

seq 3 | sed -ne'/195/!{H;1h;$!d;x;:p' -ep -e'};n;bp'

1
2
3

就地编辑


如果你想更换./infile- 换句话说就地编辑- 然后你可以实际上首先将其缓冲到临时文件中以将其写入:

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

...如果找不到模式,它根本不会采取任何行动 - 所以绝不./infile不止一次 - 但为了成功匹配,始终完全缓冲已处理的尾部./infile在重写之前输出到临时文件./infile。更具体地说 - infile 中唯一写入 shell 的此处文档的部分是 grep的比赛。grep匹配中消耗的所有输入保持消耗因此,只有缓冲尾端才会最终进入临时缓冲区。

更重要的是,大多数 shell 都会支持它们的此处文档/tmp- 因为在 Linux 系统上/tmp通常是 a tmpfs,这意味着在所述系统上缓冲的部分根本不会出现在磁盘上。不过,公平地说,由于内核处理文件缓存等的方式,将其写入 tmpfs 和将其写入其他任何地方之间可能没有太大区别,前提是您有足够的内存来缓存它。/tmp也许只是更明确一点。外壳还有unlink()是在向其写入字节之前的缓冲区文件 - 因此它仅在其读和/或写描述符保持打开状态时才存在。没有什么需要清理的。


全部包裹起来


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

allor()(
        set -f; unset o z i m;  OPTIND=1 IFS="
"
        op()    while   getopts :i:o:m: O               &&
                        case    $O$OPTARG               in
                        ([$z]*|m*[!0-9]*|[!imo]*) ! :   ;;
                        (o+)    o= O=;; (o-)     O=     ;;
                        esac||! o=${o+${o:-$i}}  m=${m:-1}
                do      eval "z=$z${O:-o #} $O=\$OPTARG"||exit
                done

        op "$@";[ -f "${i:?No input specified!}" ]      ||i=
        exec < "${i:?Input is not a regular file!}"     &&
        shift   $((OPTIND-(${#O}+1)))                   &&
        z=$( !  { {     grep -m$m "$@" 2>&3 |
                   >&4  sed  -ne'$=;$s/^/ /p'
                } 3>&1| grep . >&2;}   4>&1 )           &&
        set     ${z:?No match found!}   ${o:+'>"$o"'}   &&
        case    $((m==$1))$o    in      (0"$i") ! :     ;;
        (0*)    <"$i"   eval "  cat $3     &&   ! :"    ;;
        (1*)    <<-i    eval "  cut -c2-   $3"
                $(      printf %s\\n $2;paste /dev/null -)
                i
        esac
)

...这增加了一些选项解析之类的东西。基本上你可以传递任何grep你想要做的参数 - 所有参数都逐字传递除了-i-o或 的第一个-m

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

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

一个小演示:

seq 20 >nums
allor -inums -m2 5

15
16
17
18
19
20

...和...

seq 10 >nums
allor -inums -m2 5; echo return: "$?"

1
2
3
4
5
6
7
8
9
10
return: 1

...和...

seq 20000 >nums
allor -m1999 -inums -o+ 5$; cat nums

19985
19986
19987
19988
19989
19990
19991
19992
19993
19994
19995
19996
19997
19998
19999
20000

答案3

使用 GNU sed,您可以执行以下操作:

:x;/PATTERN/{s/.*//;:z;N;bz};N;bx

例如,我们使用7作为我们想要匹配的模式并输入由 生成的数据seq,这将打印数字 8 到 20(包括 17):

seq 20 | sed ':x;/7/{s/.*//;:z;N;bz};N;bx'

这将打印 1 到 6:

seq 6 | sed ':x;/7/{s/.*//;:z;N;bz};N;bx'

正如评论中所指出的,这可以有效地将整个文件读取到内存中 - 确保在您的情况下这是一件可以做的事情。

另请注意,目前有一个警告,即 8 到 20 的情况会输出额外的前导换行符 - 我正在尝试找出如何稳健地删除它 - 不确定这对您的应用程序是否重要。

答案4

TXR解决方案,在命令行上:

$ txr -c '@(也许)
@(跳过)
@(预告片)
@/图案/
@(结尾)
@(重复)
@线
@(do(放线线))
@(结尾)' -

pattern示例:未出现的行。代码缩写:

$ txr -c '@(也许)
[...]'-
A
C
d
Ctrl-DEnter
A
C
d

何时pattern发生:

$ txr -c '@(也许)
[...]
@(结尾)' -
A
C
图案
图案
X
X
y
y
z
z
Ctrl-DEnter

正如您所看到的,一旦pattern发生,这些线条就会开始回显。

逻辑非常简单:所包含的材料@(maybe)...@(end)是可选匹配的。我们有一个@(skip)跳过任意数量的行,后面跟着一个@(trailer)意思是“将以下内容匹配为尾随上下文(不消耗它)”。 (此功能及其名称的灵感来自 Lex 中斜杠尾随上下文。)如果我们取出,@(trailer)则与模式匹配的行将从输出中排除。

当然@/pattern/是正则表达式。它是隐式锚定的:它必须匹配整行。因此,要匹配包含的行,abc我们将使用@/.*abc.*/or @(skip)abc@(skip)

在该模式不发生的情况下,skip内部maybe会扫描整个输入,并最终失败。maybe抓住这一失败并将其隐藏起来,从而取得成功。然后将后面的材料maybe与内部maybe失败的原始输入(即流的开始)进行匹配。

最后我们有一个重复的匹配结构 ,@(repeat)它包含一个输出副作用。

一种不同的 TXR 解决方案,它使用@(data ...)指令捕获变量中的当前数据游标(惰性列表指针)start,然后在 EOF 处再次捕获它,并使用古老的 Lisp 函数ldiff来计算输出:

$ txr -c '@(maybe)
@(skip)
@/pattern/
@(end)
@(data start)
@(skip)
@(eof)
@(data end)
@(do (tprint (ldiff start end)))' -

在可选地匹配一些后面跟有模式的行后,捕获开始。如果该模式没有出现,就好像该@(maybe)...@(end)块不存在一样,并且数据的开头被捕获。

简而言之,“也许会跳过一些以模式结尾的行;无论哪种情况,都将位置记为开始;然后跳到 EOF 并将位置记为结束;打印开始和结束之间的所有内容”。

相关内容