使用Perl进行多行搜索替换

88

我知道这种问题之前已经被问过很多次了。我再次来到这里的原因是我觉得我可能错过了一些简单而基本的东西。

是否有可能让这种搜索替换例程更好。例如,不打开同一个文件两次。同时,欢迎与速度相关的建议。

请注意,这适用于多行匹配,并替换多行字符串。

#!/bin/perl -w -0777

local $/ = undef;

open INFILE, $full_file_path or die "Could not open file. $!";
$string =  <INFILE>;
close INFILE;

$string =~ s/START.*STOP/$replace_string/sm;

open OUTFILE, ">", $full_file_path or die "Could not open file. $!";
print OUTFILE ($string);
close OUTFILE;

看起来你正在尝试直接编辑文件。也就是说,你想要同时以读写模式打开它。这样对吗? - Nathan Fellman
是的,直接在文件中进行编辑。这对我来说是最常见的用例。 - user44556
我倾向于使用通用解决方案,但有时需要提醒自己,对于像这样的问题,尝试在IDE(例如IntellJ的项目中查找+查找和替换)中解决可能是值得的,而不是通过逐步升级、试错和后续检查脚本解决方案。 - Joshua Goldberg
6个回答

114
这种搜索和替换可以通过一行代码实现,例如 -
perl -i -pe 's/START.*STOP/replace_string/g' file_to_change

如果您希望了解更多实现同样功能的方法,请查看thread。要处理多行搜索,请使用以下命令 -

perl -i -pe 'BEGIN{undef $/;} s/START.*STOP/replace_string/smg' file_to_change

为了将以下代码从一行转换为Perl程序,请查看perlrun文档
如果您确实需要将其转换为可工作的程序,则让Perl为您处理文件的打开/关闭。
#!/usr/bin/perl -pi
#multi-line in place substitute - subs.pl
use strict;
use warnings;

BEGIN {undef $/;}

s/START.*STOP/replace_string/smg;

你可以使用文件名作为第一个参数调用脚本。

$perl subs.pl file_to_change

如果你想要一个更丰富的脚本,可以处理文件的打开/关闭操作(我们难道不喜欢所有那些“die”语句吗?),那么请看perlrun中-i[extension]开关下的示例。

1
请检查修改,BEGIN 块现在确保它也适用于多行匹配。 - aks
1
好的,它可以写成 Perl 代码吗(不是一行代码)?我想知道文件打开/写入例程会发生什么。 - user44556
1
START和STOP分别是您要匹配的正则表达式的开头和结尾。通过取消定义输入记录分隔符('$/ '),我们有效地让Perl一次性将整个文件读入$_中,从而使我们能够进行多行替换。 - aks
29
更简短的版本:perl -i -p0e 's/START.*STOP/replace_string/smg' file_to_change-0将行分隔符设置为null)。 - zbyszek
4
想了解undef $/;是什么的人,它被称为“吞食模式”。更多信息请查看这里 - JBENOIT
显示剩余6条评论

100

从评论中简要获取答案,对于任何寻求快速的一行代码和Perl忽略其命令行中的RegEx选项的原因。

perl -0pe 's/search/replace/gms' file

如果没有-0参数,Perl会逐行处理数据,这会导致多行搜索失败。


5
好的。如果它看起来不起作用,尝试使用\R(匹配所有类型的行尾),而不是\n - ederag
15
对我来说,0开关是至关重要的。谢谢并点赞(+1)。 - Andreas H.
1
在我的macosx上,perl -0777 -i -pe 's/search/replace/' 1.h可以正常工作。 - Princekin
在 Perl 上我工作时,. 不包括 \n,所以我不得不使用 [\s\S]*。我想知道为什么这里没有人提到它。 - Martian2020

2

考虑到您使用以下方式读取整个文件的内容:

local $/ = undef;

open INFILE, $full_file_path or die "Could not open file. $!";
$string =  <INFILE>;
close INFILE;

然后使用$string进行所有处理,文件的处理方式与内容处理方式之间没有任何联系。如果在读取文件完成之前打开文件进行写操作,则会出现问题,因为打开文件进行写操作会创建一个新文件,丢弃先前的内容。

如果你想要简化打开和关闭文件操作,可以像乔纳森·莱弗(Jonathan Leffer)建议的那样做。如果你的问题是关于多行搜索和替换的,请明确具体问题。


这是关于通用多行搜索和替换的问题。即使文件非常大,我重新打开相同的文件指针也没问题吗?在一行代码中似乎不需要两次打开同一个文件。我仍然有些不理解。也许我应该亲自看看Jonathan的例子。 - user44556
创建文件处理程序与文件大小无关,它只是一个指针。打开文件的行为并不意味着读取其内容。 - Nathan Fellman
我认为这可能是我的一个误解。当读取意味着必须通过它查找可能的匹配项时,如何打开同一文件进行读写操作? - user44556
你必须只读一次。当你打开文件进行写入时,你根本没有在读取它。无论文件在打开写入前有多大,因为你无论如何都要丢弃它们。 - Nathan Fellman

0

我知道这个问题已经有答案了,但是我想分享一下我的解决方法。

假设您想要更改UUID,但必须在上一行匹配,因为您有许多属于其他项目的UUID。

在Ubuntu 20中,在bash脚本中调用perl:

_UUID=$(uuidgen | sed 's/-//g')
export _UUID
perl -0777 -pi.back -e 's/(<stringProp\sname="Argument\.name">_BINARYVIDEOTEMPURL<\/stringProp>\n.*<stringProp\sname="Argument\.value">)[a-zA-Z0-9]{32}(<\/stringProp>)/$1$ENV{_UUID}$2/g;' test.txt

你的test.txt文件内容如下:(虽然不是有效的XML,但请创建它)

<?xml version="1.0" encoding="UTF-8"?> <jmeterTestPlan version="1.2" properties="5.0" jmeter="5.2.1">
  <hashTree>
<TestPlan guiclass="TestPlanGui" testclass="TestPlan" testname="K8S Load Test Plan" enabled="true">      
  <stringProp name="TestPlan.user_define_classpath"></stringProp>
</TestPlan>
      <collectionProp name="Arguments.arguments">
        <elementProp name="_SESSIONID" elementType="Argument">
          <stringProp name="Argument.name">_SESSIONID</stringProp>
          <stringProp name="Argument.value">7c096b65-84b6-40c9-be93-a5891ec0394d</stringProp>
          <stringProp name="Argument.metadata">=</stringProp>
        </elementProp>
        <elementProp name="_BINARYVIDEOTEMPURL" elementType="Argument">
          <stringProp name="Argument.name">_BINARYVIDEOTEMPURL</stringProp>
          <stringProp name="Argument.value">64e1886127fa41c4a58e59fe2bb098e1</stringProp>
          <stringProp name="Argument.metadata">=</stringProp>
        </elementProp>
      </collectionProp>

这里发生了很多事情,让我解释一下。

  1. 创建一个新的UUID来替换旧的。
  2. 导出UUID,因为 Perl 将在环境变量中拾取它。
  3. 调用Perl来处理搜索和替换
  • -077使perl能够使用多行并完成多行前瞻和后瞻。我无法告诉您perl如何工作。
  • -pi.back基本上是内联编辑和备份文件。
  • -e基本上是 's / reaplcethis / withthis / g',但它包含需要匹配的换行符的正则表达式。此外,它展示了如何使用环境变量和分组来重新创建字符串。

总之,希望这对某人有所帮助。


0

结合bash脚本和perl -pi -e是无与伦比的 - 这是一个bash函数的例子,可以直接在EOF标签之前键入搜索和替换字符串:

# usage put into foobar.sh file, source foobar.sh file
# call directly into the shell do_multiline_srch_and_replace
do_multiline_srch_and_replace(){

                test -z $dir_to_work && {
         echo "You must export dir_to_work=<<the-dir>> - it is empty !!!"; exit 1;
      }
                test -d $dir_to_work || {
         echo "The dir to work on: \"$dir_to_work\" is not a dir !!!"; exit 1;
      }

                echo "INFO dir_to_work: $dir_to_work" ; sleep 1
                echo "INFO START :: searching and replacing in the non-binary files only"

                while read -r file ; do (
                        echo "DEBUG working on the following file: $file"

         # those pattern in the file names we want to skip usually - git, not , py files
         case "$file" in
            *.git*)
            continue ;;
            *node_modules*)
            continue ;;
            *.venv*)
            continue ;;
         esac
         # note the string should be exactly between the s|| and the replace str between the ||gs
         # the 'EOF' guarantees that no special chars from the shell will affect the result
                        perl -pi - <<'EOF' "$file"
BEGIN{undef $/;}
s|a multiline
string|the multiline
string to replace|gs
EOF
                );
                done < <(find $dir_to_work -type f -not -exec file {} \; | grep text | cut -d: -f1)

                echo "INFO STOP  :: search and replace in non-binary files"

}

0

您可能想要查看我的Perl脚本,它经过了实战检验(在生产中广泛使用),并具有许多功能,例如:

  • 执行多个搜索替换或查询搜索替换操作
  • 可以在命令行上给出搜索替换表达式,也可以从文件中读取
  • 处理多个输入文件
  • 递归进入目录,并对所有文件执行多个搜索/替换操作
  • 对每个输入文件的每行应用用户定义的perl表达式
  • 可选运行段落模式(用于多行搜索/替换)
  • 交互模式
  • 批处理模式
  • 可选备份文件和备份编号
  • 以root身份运行时保留模式/所有者
  • 忽略符号链接、空文件、无法写入的文件、套接字、命名管道和目录名
  • 可选仅替换与给定正则表达式匹配/不匹配的行

https://github.com/tilo/replace_string


-1 这不是一个答案,因为你没有告诉 OP 如何解决问题,而只是指向了你的代码。如果你解释一下你的代码中解决 OP 问题的关键部分,那将是一个更好的答案。 - Lou
@Lou,我提供了一个更通用的工具,也是使用Perl编写的。你看过它的源代码了吗?也许你可以在那里找到答案.. ;) 鉴于有一个方便的工具可以进行多文件搜索/替换操作,最好使用它,而不是试图手动编码。 - Tilo
如果您能在答案中解释一下您的通用解决方案如何解决问题,那就太好了。链接(或存储库)可能会失效,那么将来的读者就不会知道您的通用解决方案如何帮助任何人进行多行搜索和替换。另请参见:您的答案在另一个城堡中以及类似问题的this answer - Lou

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接