我正在尝试使用类似sed
或awk
执行以下操作来编写过滤器:
- 如果输入中不存在给定模式,则将整个输入复制到输出
- 如果输入中存在该模式,则仅将第一次出现之后的行复制到输出
这恰好适用于“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
并将变量设置pat
为5
。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
的-u
nbuffered 模式)。这是因为每个进程仍然会做一些缓冲,但是当它们完成任务时,它们会lseek()
描述符回到他们影响它的最后一点。否则很难让事情正确排列(虽然dd
可以用来实现这个效果)。
并且因为相同的描述符也传递给下一个shell 调用的子进程,并且最后一个子进程已更改其偏移量,则下一个命令将立即从最后一个子进程停止的位置开始输入。所以当我们...
seq 10 >nums
{ grep -m1 5; cat; } <nums
grep
5
仅打印找到的一个匹配项后,退出第一个匹配项的输入,并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
...将每个输入行堆叠在sed
的H
旧空间中,直到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
通常不会与其他软件很好地配合,除非您将其切换到-u
nbuffered 模式,这会对性能产生相当有害的影响。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 并将位置记为结束;打印开始和结束之间的所有内容”。