在 sed 中使用正则表达式(regex)

在 sed 中使用正则表达式(regex)

这是我无法理解的一般主题的一个具体例子。

多年来,我一直使用 regex 和 sed 递归地查找/替换目录中所有文件中出现的所有字符串,使用如下所示:

#FIND $GLOBALS['timechecks'] and REPLACE with completely_different_string
shopt -s globstar dotglob;
for file in /var/www/**/*; do
  if [[ -f $file ]] && [[ -w $file ]]; then
    sed -i -- 's/\$GLOBALS\['\''timechecks'\''\]/completely_different_string/g' "$file"
  fi
done

问题是,在 bash 中使用正则表达式有一些基本的东西我在不知道的情况下就消失了。因此,我无法找出特定示例的解决方案。

我被困的目标字符串

$GLOBALS['timechecks']=addTimeCheck_sparky($GLOBALS['timechecks'], number_format(microtime(true),6,'.',''), __LINE__, basename(__FILE__));

我想出的正则表达式不起作用

这只是我的脚本中的 sed 行以及我想出的搜索正则表达式,但无济于事。

\$GLOBALS\['\''timechecks'\''\]=addTimeCheck_sparky[(]$GLOBALS\['\''timechecks'\''\][,][ ]number_format[(]microtime[(]true[)][,]6[,]'\''\.'\''[,]'\'''\''[)][,][ ]__LINE__[],[ ]basename[(]__FILE__[)][)][;]

正则表达式调试器

我在这个示例中使用了正则表达式调试器,它显示正则表达式找到我的目标字符串,但它对我不起作用。调试器位于这个链接。这是它显示的找到我的目标字符串的正则表达式:

\$GLOBALS\['timechecks\'\]=addTimeCheck_sparky\(\$GLOBALS\[\'timechecks\'\], number_format\(microtime\(true\),6,\'\.\',''\), __LINE__, basename\(__FILE__\)\)

正则表达式调试器的输出问题:

首先,我在 de 中尝试了我的正则表达式

  1. 我不知道为什么调试器的正则表达式在我在那里运行时起作用,但在我的 bash 脚本中不起作用。
  2. 与我在 bash 和 sed 中学习的正则表达式相比,该正则表达式看起来“错误”
  3. 当我将调试器中的正则表达式插入用于执行此任务的脚本时,它不起作用。
  4. 因为我不明白它,所以我无法修复它

我认为基本问题是我对将调试器中的有效正则表达式转换为在 bash/sed 中工作一无所知。

我搜索了“how to use regex with sed in bash”,但没有找到对这甚至是一个潜在问题这一事实的解释。

相关问题:为什么没有生成器接受目标字符串作为输入并提供可以找到它的正则表达式?

答案1

\$GLOBALS\['\''timechecks'\''\]=addTimeCheck_sparky[(]$GLOBALS
                                                      ^

那里有一个未逃脱的人$

\['\''timechecks'\''\][,][ ]number_format[(]microtime[(]true[)]
[,]6[,]'\''\.'\''[,]'\'''\''[)][,][ ]__LINE__[],[ ]basename[(]__FILE__[)][)][;]
                                              ^^

大概应该是这样[,]

不转义$实际上并不重要(至少对于 GNU sed 而言),但这[],[ ]是内部带有 和 空格的括号表达式[],。但这是一个有效的正则表达式,只是不是您想要的,因此它不会产生任何错误。

但实际上,引用是一件非常痛苦的事情。有时最好避免它。

让我们将模式和替换字符串以及测试文件放入一些文件中:

$ cat pat 
$GLOBALS['timechecks']=addTimeCheck_sparky($GLOBALS['timechecks'], number_format(microtime(true),6,'.',''), __LINE__, basename(__FILE__));
$ cat repl
hello!
$ cat test.txt
foo
$GLOBALS['timechecks']=addTimeCheck_sparky($GLOBALS['timechecks'], number_format(microtime(true),6,'.',''), __LINE__, basename(__FILE__));
bar

然后,用 Perl 替换字符串:

$ pat=$(< pat) repl=$(< repl) perl -i.bak -pe 's/\Q$ENV{pat}/$ENV{repl}/' test.txt
$ cat test.txt
foo
hello!
bar

当从文件中读取字符串时,无需在 shell 命令行上引用。此外,当模式来自变量并\Q使用时,无需转义模式中的特殊字符。在这里,我通过环境将字符串传递给 Perl,因为它-i比命令行参数工作得更好。-p使得perl行为有点像sed它为每个输入行运行给定的脚本,-i.bak就像seds -i

相关问题:为什么没有生成器接受目标字符串作为输入并提供可以找到它的正则表达式?

出色地。通常,正则表达式与旨在匹配多个字符串的模式一起使用,并且程序可能很难知道哪些部分可以变化。尽管如果您总是在寻找固定字符串,那么转义特殊字符会有点简单。但实际上你一开始就不需要正则表达式引擎。只是它们在常见的 Unix 工具中相当普遍。

您在评论中提到:

想想看,如果一行与该字符串匹配,这就是我需要知道的内容来替换它:$GLOBALS['timechecks']=addTimeCheck_sparky

就像是

sed -- -e 's/^.*GLOBALS..timechecks..=addTimeCheck_sparky.*$/hello/' 

可以用来匹配它并替换整行。当然,这也会匹配#GLOBALS_atimecheckses=addTimeCheck_sparky和相关的变体,因为我作弊并只是用 替换了所有特殊字符.。但你明白了。

此外,如果先备份原始文件,您始终可以备份副本,然后运行diff original.txt processed.txt以查看任何更改。

答案2

对我有用:

sed -- 's/\$GLOBALS\['\''timechecks'\''\]/completely_different_string/g' <<'END'
foo
$GLOBALS['timechecks']=addTimeCheck_sparky($GLOBALS['timechecks'], number_format(microtime(true),6,'.',''), __LINE__, basename(__FILE__));
bar
END
foo
completely_different_string=addTimeCheck_sparky(completely_different_string, number_format(microtime(true),6,'.',''), __LINE__, basename(__FILE__));
bar

这适用于 Mac 上的默认 BSD sed 和 GNU sed。


术语问题:没有“bash sed”。 bash 是您的交互式 shell,它也是一种编程语言。 sed 是一种不同的编程语言。从 bash 的角度来看,sed 只是在 $PATH 中找到的另一个命令,例如lsor grepor ...

答案3

您需要一个自动化的解决方案,有太多的事情需要引用和跟踪。

两步解决方案(不是 100% 完美(可能存在病态的极端情况))是:

  1. 逐字获取变量中的字符串。

    • 为什么?因为(引用的)变量 ( ) 的内容"$var"永远不会被 shell (再次)修改。
    • 如何?用一个这里-字符串。

    步骤是:

    • 写入:IFS= read -r var <<\END在命令行上
    • 复制并粘贴您要处理的完全相同的字符串,然后按 Enter 键
    • 写入END并再次按 Enter 键。

    然后,变量 var 将包含与您在命令行上复制的完全相同的字符串,没有更改,没有删除引号,什么都没有,只是字符串。

    你应该看到的是:

    $ IFS= read -r var <<\END
    > $GLOBALS['timechecks']=addTimeCheck_sparky($GLOBALS['timechecks'], number_format(microtime(true),6,'.',''), __LINE__, basename(__FILE__));
    > END
    

    完成,是的,真的,这就是所有复杂的部分,复制和粘贴。
    您可以回显该字符串:

    $ echo "$var"
    $GLOBALS['timechecks']=addTimeCheck_sparky($GLOBALS['timechecks'], number_format(microtime(true),6,'.',''), __LINE__, basename(__FILE__));
    

    好吧,你最好使用printf '%s\n' "$var" to avoid issues with some values ofvar that may start with a-`,但在这个例子中 echo 工作正常。

从此时起,您将不需要完成其他打字/输入/“手动转义”。
您只需复制粘贴以下命令即可。

  1. 使用 var 值生成 sed 中使用的精确正则表达式以与其精确匹配。接受的正则表达式sed称为POSIX 的 BRE(基本正则表达式)
    在BRE中,有几个特殊字符\ . [ * * ^ $
    如果所有这些字符都被引用,则正则表达式实际上是原始字符串的逐字字符串。这很容易做到(\.*^$[):

    $ echo "$var" | sed 's#\([\.*^$[]\)#\\\1#g'
    $GLOBALS\['timechecks']=addTimeCheck_sparky($GLOBALS\['timechecks'], number_format(microtime(true),6,'.',''), __LINE__, basename(__FILE__));
    

    引用(转义)了任何存在的反斜杠 ( \)、开头 ( [)、点 ( .)、星号 ( *)、扬抑符 ( ^) 和美元符号 ( $)。这将破坏任何可能的正则表达式构造var并将它们全部转换为一个简单的字符串。它会破坏任何“括号表达式”( [)、任何“任何字符”( .)、任何重复 ( *)、任何锚点 ( ^$) 和任何反斜杠 ( \)。请注意,
    任何()或 不需要转义。如果没有逃脱,它们就会保留下来,因此不像(特殊的)。如果转义 ( ) 它们会变成,也会失去任何特殊价值。{}\(\(\\(

    可能存在我现在看不到的病态极端情况,但 99.2% 的情况下,简单的转换就足够了。

然后,您可以捕获更改后的字符串,并在 sed 中使用它:

$ reg=$(echo "$var" | sed 's#\([\.*^$[]\)#\\\1#g')

$ echo "$var" | sed 's#'"$reg"'# ===any string=== #'
 ===any string=== 

如果转换正确,sed 命令应该捕获整个初始字符串并将其替换为右侧字符串。

当然,如果您想要匹配字符串的较短部分,只需从要匹配的部分开始即可。

额外的 如果您想查看应该编写哪种字符串才能在变量中获取正确的字符串(这需要额外的引用层),您可以使用(bash 4.3+):

$ myvar=$(echo "${var}" | sed 's#\([\.*^$[]\)#\\\1#g')
$ echo "${myvar@Q}"
'\$GLOBALS\['\''timechecks'\'']=addTimeCheck_sparky(\$GLOBALS\['\''timechecks'\''], number_format(microtime(true),6,'\''\.'\'','\'''\''), __LINE__, basename(__FILE__));'

如果你写这样的东西:

$ myvar='\$GLOBALS\['\''timechecks'\'']=addTimeCheck_sparky(\$GLOBALS\['\''timechecks'\''], number_format(microtime(true),6,'\''\.'\'','\'''\''), __LINE__, basename(__FILE__));'

一级引用被删除,您就可以进入myvar需要使用的字符串。

您可以与最初的尝试进行比较,看看哪里出了问题:

Bad:     \$GLOBALS\['\''timechecks'\''\]=addTimeCheck_sparky[(]$GLOBALS\['\''timechecks'\''\][,][ ]number_format[(]microtime[(]true[)][,]6[,]'\''\.'\''[,]'\'''\''[)][,][ ]__LINE__[],[ ]basename[(]__FILE__[)][)][;]
Good:   '\$GLOBALS\['\''timechecks'\'']=addTimeCheck_sparky(\$GLOBALS\['\''timechecks'\''], number_format(microtime(true),6,'\''\.'\'','\'''\''), __LINE__, basename(__FILE__));'

希望这能为您提供一个通用的万无一失的程序来引用任何内容。

笔记:我为 sed 的基本 BRE 正则表达式构建了上述过程。这些是 sed 理解的所有正则表达式(默认情况下)。如果 sed 被调用为则使用sed -E扩展正则表达式 ( )。 EREERE 有一些变化。特殊字符列表增长为:.[\()*+?{|^$,因此,转义应该是(不,我们不能在这里使用扩展正则表达式,因为它们不允许反向引用):

sed 's@\([\.()*+?{|^$[]\)@\\\1@g'

你可以看到它是如何工作的我准备的这一页

我并不是在讨论 PCRE (Perl) JavaScript、PHP 或任何其他正则表达式风格sed 不能使用它们,期间,没有用。

有关的:

BRE——POSIX 基本正则表达式

相关内容