如何使用 grep、awk 或 sed 获取一行的子字符串直到字符串文字?

如何使用 grep、awk 或 sed 获取一行的子字符串直到字符串文字?

我正在尝试处理一个文本文件并省略某个字符串文字(如果它出现在行尾)。例如:

来源:

ABC 123
DEF, characters I don't want
GHI, these characters are ok

期望的输出:

ABC 123
DEF
GHI, these characters are ok

如果我这样做grep -v ', characters I don't want$',它会忽略整行。

我不能做一个简单的awk列,因为我想要, these characters are ok子字符串

我无法使用cut分隔符进行分割,因为分隔符需要是多个字符 ( , characters I don't want)。

使用Python,这将非常简单,例如:string.split(", characters I don't want", 1)[0]

(顺便说一句,我想知道在像这样的复杂情况下,当 Python 更具可读性和可维护性时,在哪些用例中使用 grep、awk 或 sed 与 Python 相比确实更可取。)

答案1

这里最明显的是使用sed

<source sed "s/, characters I don't want\$//"

当我们在 shell 中转义的s行末尾找到该字符串时,替换该字符串(作为未来的证明,以防将来在 shell 中出现某些内容)。$\$$/

要删除该字符串后面的任何内容(如果有),请将 替换为\$.*尽管我们需要更改 C 的区域设置以保证.*匹配直到结尾的所有内容,即使这在用户区域设置中不是有效文本:

<source LC_ALL=C sed "s/, characters I don't want.*//"

对于 GNUgrep或兼容版本,当使用类似 perl 的正则表达式支持构建时,可能是:

<source LC_ALL=C grep -Po "^.*?(?=(, characters I don't want)?\$)"

或者也删除该字符串之后的所有内容(如果有):

<source LC_ALL=C grep -Po "^.*?(?=, characters I don't want|\$)"

或者pcregrep(当在 GNU 中启用类似 perl 的正则表达式支持时grep,实际上是通过 libpcre 来实现的,它作为示例应用程序提供pcregrep,但具有超出 GNU 的功能grep):

<source pcregrep -o1 "^(.*?)(, characters I don't want)?\$"

或者也删除该字符串之后的所有内容(如果有):

<source pcregrep -o1 "^(.*?)(, characters I don't want|\$)"

如果要删除的文本可能包含任何内容,包括/或 正则表达式运算符(但不是没有意义的换行符,也不是可以在命令参数或环境变量中传递的 NUL 字符)并且存储在 shell 变量中,您可以这样做不是想要使用,sed "s/$string\$//"因为这会使其成为命令注入漏洞。

对于 perl-grep,您可以使用:

string='/.*\^$'
<source LC_ALL=C grep -Po "^.*?(?=(\Q$string)?\$)"
<source pcregrep -o1 "^(.*?)(\Q$string\E)?\$"

或者也删除该字符串之后的所有内容(如果有):

<source LC_ALL=C grep -Po "^.*?(?=\Q$string|\$)"
<source pcregrep -o1 "^(.*?)(\Q$string\E|\$)"

这仍然对$string包含 的 s造成窒息\E,尽管不会产生像 那样严重的后果sed

或者您可以perl直接使用它具有sed带有-p选项的模式,具有传递任意字符串的机制(此处用于-s粗略的选项传递,但您也可以@ARGV直接使用(相当于 python sys.argv)或环境变量(映射到%ENV关联数组)) ,并且可以\Q在正则表达式中引用字符串(这里的\Ein$string不是问题):

<source perl -spe 's/\Q$string\E$//' -- -string="$string"

或者也删除该字符串之后的所有内容(如果有):

<source perl -spe 's/\Q$string\E.*$//' -- -string="$string"

perl默认情况下,将输入视为字节,而不是像在用户的区域设置字符集中编码的那样,因此我们不需要更改那里的区域设置。

请注意,与 相反sed,默认情况下,行分隔符包含在模式空间中(默认情况$_下在perl其中起作用),并且其正则表达式运算符匹配主题末尾或主题末尾的行分隔符之前,因此能够处理定界线和未定界线。s///$

答案2

使用任何 awk:

$ awk 'n=index($0 RS,", characters I don\047t want" RS){$0=substr($0,1,n-1)} 1' file
ABC 123
DEF
GHI, these characters are ok

这是进行文字字符串比较,因此即使您尝试与包含的正则表达式元字符匹配的字符串也可以工作,例如使用以下输入:

$ cat file2
ABC 123
DEF, .*, .*
GHI, .* ok

我们得到预期的输出:

$ awk 'n=index($0 RS,", .*" RS){$0=substr($0,1,n-1)} 1' file2
ABC 123
DEF, .*
GHI, .* ok

如果您不关心正则表达式元字符,您可以这样做:

$ awk '{sub(/, characters I don\047t want$/,"")} 1' file
ABC 123
DEF
GHI, these characters are ok

但随后你会得到意想不到的输出:

$ awk '{sub(/, .*$/,"")} 1' file2
ABC 123
DEF
GHI

并且您必须转义元字符以使它们成为文字才能获得预期的输出:

$ awk '{sub(/, \.\*$/,"")} 1' file2
ABC 123
DEF, .*
GHI, .* ok

考虑到您真正想要的只是文字字符串比较,这变得很笨拙。

http://awk.freeshell.org/PrintASingleQuote为什么我使用\047而不是'.

至于为什么使用 awk 而不是 python - awk 是一个强制性的 POSIX 工具,因此保证存在于所有 POSIX 兼容的 Unix 安装上,而 python 则不然,并且使用 awk 操作文本通常需要比使用 awk 少得多的代码Python。我怀疑我们必须就哪个更容易阅读和维护达成共识。

答案3

当预先知道行尾的内容时,在支持类似变量扩展功能的 Bash 和 shell 中过滤掉这些内容是相当容易的。例如:

#!/usr/bin/env bash
line='DEF, characters I do not want'
echo "${line%, characters I do not want}"

将打印:

DEF

该语法返回从末尾删除内容后的字符串${var%string}的内容。在此示例中,要删除的字符串是“ ”。如果该字符串不在末尾,则返回 的完整内容。有多种从变量开头删除字符串的变体,以及可以替换内容中间的字符串或将其删除的替换。$var%, characters I do not want$line

我承认在上面的示例中更改don't-> 是do not为了避免在将字符串分配给变量时使用单引号引起的复杂情况$line

这种方法的优点是您的脚本不需要调用外部命令来执行简单的过滤。 但它能取代Python的力量吗?。可能不会,但可能还有其他因素促使您使用 shell 脚本而不是 python 来完成此任务。

相关内容