如何替换字符串中的多行?

How can I replace multiple lines in a string?

提问人:santy 提问时间:7/29/2023 最后编辑:brian d foysanty 更新时间:7/31/2023 访问量:154

问:

我想使用 sed/awk/perl 替换多行字符串,如下所示

    #-------------------------------------------------------------------------------
    # supply connections
    #-------------------------------------------------------------------------------
    
        connect_supply_net VSS      -ports $port(VSSE)
        connect_supply_net VDDS_CPU -ports $port(VDDP)
        connect_supply_net VDD_CPU  -ports $port(VDDPE)

#-------------------------------------------------------------------------------
# Update states
#-------------------------------------------------------------------------------

上面是输入文件,我想在输出文件中将上面的 3 行替换为下面

    #-------------------------------------------------------------------------------
    # supply connections
    #-------------------------------------------------------------------------------
 
    if { [llength $port(VSSE)] > 0 } {
      connect_supply_net VSS -ports $port(VSSE)
    }
    if { [llength $port(VDDP)] > 0 } {
      connect_supply_net VDDS_CPU -ports $port(VDDP)
    }
    if { [llength $port(VDDPE)] > 0 } {
      connect_supply_net VDD_CPU -ports $port(VDDPE)
    }

    #-------------------------------------------------------------------------------
    # Update states
    #-------------------------------------------------------------------------------

有什么比这更简单的方法呢?

我尝试使用传递变量,然后使用 sed 命令,但它不起作用。我尝试了以下方法 -

old_string="connect_supply_net VSS      -ports $port(VSSE)
connect_supply_net VDDS_CPU -ports $port(VDDP)
connect_supply_net VDD_CPU  -ports $port(VDDPE)"

new_string="if { [llength $port(VSSE)] > 0 } {
  connect_supply_net VSS -ports $port(VSSE)
}
if { [llength $port(VDDP)] > 0 } {
  connect_supply_net VDDS_CPU -ports $port(VDDP)
}
if { [llength $port(VDDPE)] > 0 } {
  connect_supply_net VDD_CPU -ports $port(VDDPE)
}"

sed -i ":a;N;$!ba;s/${old_string}/${new_string}/g" file.txt

但是,上述方法没有奏效。有什么比这更简单的方法呢?

与字符串 语言无关的 多行

评论

0赞 markp-fuso 7/29/2023
old_string="connect ..."不会像你想象的那样工作;因为你已经用双引号将字符串括起来,所以组件将被替换为名为 的变量的内容,如果变量未定义,则将保存在变量中,作为 ;任务也是如此;在这种情况下,您希望将两组字符串括在单引号中,以便赋值将引用视为文本后跟字符串$portbashportport$port(VSSE)(VSSE)new_string$port$port
1赞 markp-fuso 7/29/2023
您需要转换所有条目还是仅转换部分条目?您是否需要转换除条目之外的其他条目,如果是这样,请考虑使用更具代表性的数据集更新问题connect_supply_netconnect_suppy_net
1赞 F. Hauri - Give Up GitHub 7/30/2023
在不考虑多行的情况下,只需尝试以下命令:sedsed 's/^\([[:space:]]*\)connect_supply_net .*port(\([^)]\+\))[[:space:]]*/\1if { [llength $port(\2)] > 0 } {\n\1 &\n\1}/'

答:

2赞 zdim 7/29/2023 #1

如果你在shell中定义这些变量是可以的,那么你需要以某种方式使它们可供使用它们的程序使用,并且该程序需要将整个文件读入一个变量中,以便能够找到这些多行模式。使用 Perl

$ export old_string="..."
$ export new_string="..."

$ perl -0777 -i -wnE's/$ENV{old_string}/$ENV{new_string}/g; print' filename

以上方法在我的测试中有效,但是在终端上复制多行文本块非常挑剔,并且对空白细节很敏感。那些标签在那里吗?究竟有多少领先空间?相反,此文本在脚本中更容易控制。

我们可以输入要匹配的确切文本,然后通过复制粘贴替换,也许最好使用 here-doc 引用。但是,空格问题仍然存在 - 这种对齐是用制表符完成的吗?复制粘贴会将它们丢失到空格中。再说一次,我粘贴这些空格是否正确?

相反,为该文本块形成一个正则表达式模式,以使其与其各种空格相匹配。这是一种方法。

复制这些行并处理每个行:分成单词,在每个单词中使用 quotemeta 转义正则表达式特殊字符(至少有和 parens),然后将单词与 连接起来,然后在每个这样的字符串前面添加字符串(空格或制表符用于那些前导空格,但不是换行符,因此不能使用);然后将它们全部加入 .完全$'\s+''[ \t]+'\s+'\n'

use warnings;
use strict;
use feature 'say';

die "Usage: $0 filename\n" if not @ARGV;

my $patt = 
    join '\n',  
    map { '[ \t]+' . join '\s+', map { quotemeta } split }
        'connect_supply_net VSS      -ports $port(VSSE)',
        'connect_supply_net VDDS_CPU -ports $port(VDDP)',
        'connect_supply_net VDD_CPU  -ports $port(VDDPE)';

my $repl = <<'EOS';
    if { [llength $port(VSSE)] > 0 } {
      connect_supply_net VSS -ports $port(VSSE)
    }
    if { [llength $port(VDDP)] > 0 } {
      connect_supply_net VDDS_CPU -ports $port(VDDP)
    }
    if { [llength $port(VDDPE)] > 0 } {
      connect_supply_net VDD_CPU -ports $port(VDDPE)
    }
EOS

local $^I = '.bak';  # edit "in-place" -- change the file. keep backup

local $/;            # read the file all at once

while (<>) {         # whole file read into $_
    s/$patt/$repl/g;
    print;
}

这是通过在命令行上向其传递文件名来运行的。它应该对空白细节更加可靠,即使它不再那么简单了。

通过设置就地编辑 () like command-lline 选项,并取消设置输入记录分隔符 () 来处理文件,以便将文件一次性“啜饮”到标量中。在这里,我们使用 <> 运算符读取它。默认情况下,这被分配给 $_ 变量,默认情况下正则表达式绑定到该变量。进入文件,通过(没有任何东西打印到屏幕上)。$^I-i$/print$^I

评论

0赞 santy 7/29/2023
正如您提到的,我尝试了带有导出的 bash 脚本,但它不起作用
0赞 zdim 7/29/2023
@santy“用带有导出功能的 bash 脚本尝试了这个......没用“——哦?我测试了它,它有效。我不知道你如何为这些 bash 变量分配数据?如果你想这样做,那么每个空格都很重要,换行符(换行符)尤其重要。你坚持使用 bash 变量吗?有更简洁的方法 -- 例如,将其作为一个程序,并很好地定义要使用的字符串
0赞 santy 7/29/2023
正如您建议的那样,我正在将数据分配给 bash 变量,但是是的,数据不在单行中,有换行符并且其中有 $,但我将所有内容都放在双引号中。我更喜欢 bash 变量,因为我认为它只是变量 (old_string) 而不是变量 (new_string) 替换。
0赞 zdim 7/29/2023
@santy 然后你必须小心如何输入数据。我将文本复制粘贴到提示符 () 中,它起作用了,但可能很棘手。使用一个简短的程序,人们可以很好地以任何方式输入该数据,这要容易得多(或者如果愿意,也可以从命令行读取数据)。如果你愿意,我可以发布它,这很简单export v1="copy-here"
0赞 santy 7/29/2023
是的,请发布您的建议。还有什么是短节目?
1赞 Hai Vu 7/29/2023 #2

如果您已经在 :$old_string

new_string=$(awk '{ printf "if { [llength %s] > 0 } {\n  %s\n}\n", $NF, $0 }' <<< $old_string)

以上是有效的,但是如果我们将脚本放在一个单独的文件中

# script.awk
{
    printf "if { [llength %s] > 0 } {\n", $NF
    print " " $0
    print "}"
}

然后命令会更干净:

new_string=$(awk -f script.awk <<< $old_string)

评论

0赞 santy 7/29/2023
我不明白这是怎么回事。举个例子,可能与我的用例有关
2赞 markp-fuso 7/29/2023 #3

示例文件:

$ cat file.txt
some line
connect_supply_net VSS      -ports $port(VSSE)
another line
connect_supply_net VDDS_CPU -ports $port(VDDP)
yet another line
connect_supply_net VDD_CPU  -ports $port(VDDPE)
one more line
connect_supply_net ABCD  -ports $port(XYZ)
last line

$ cat replace.txt
connect_supply_net VSS      -ports $port(VSSE)
connect_supply_net VDDS_CPU -ports $port(VDDP)
connect_supply_net VDD_CPU  -ports $port(VDDPE)

注意:假设 in 中的条目与 in 中的行相同匹配(以在字段之间包含相同数量的空格),否则我们需要添加更多代码replace.txtfile.txt

一个想法:awk

awk '
FNR == NR     { replace[$0]; next }
$0 in replace { $3 = $3                          # squeeze multiple white spaces into single spaces throughout entire line
                $0 = "if { [llength " $4 "] > 0 } {" ORS "  " $0 ORS "}"
              }
1
' replace.txt file.txt

注意:如果 OP 必须使用变量,则替换为old_stringreplace.txt<(echo "${old_string}")

这将生成:

some line
if { [llength $port(VSSE)] > 0 } {
  connect_supply_net VSS -ports $port(VSSE)
}
another line
if { [llength $port(VDDP)] > 0 } {
  connect_supply_net VDDS_CPU -ports $port(VDDP)
}
yet another line
if { [llength $port(VDDPE)] > 0 } {
  connect_supply_net VDD_CPU -ports $port(VDDPE)
}
one more line
connect_supply_net ABCD  -ports $port(XYZ)
last line

评论

0赞 santy 7/29/2023
没有替换 .txt,只有 file.txt。我想在替换这三行后覆盖文件 .txt
0赞 markp-fuso 7/29/2023
@santy - 由于您正在手动创建内容,因此我假设您愿意通过任何有效的方法(例如,文件)提供行;我已经更新了答案,以包括用作源的方法old_stringold_string
0赞 markp-fuso 7/29/2023
至于用修改后的行覆盖,最简单的方法是将输出定向到类似的东西,然后运行;如果您正在使用,有一种方法可以覆盖源文件,但这需要禁用/启用模块......可行,但不如使用中间文件那样“清晰”(例如,file.txttmp.txtmv tmp.txt file.txtGNU awkawkinplacetmp.txt)
1赞 ufopilot 7/29/2023 #4
awk -v mystring="connect_supply_net" '
    $0 ~ mystring{
    printf "if { [llength %s] > 0 } {\n %s \n} \n", $NF, $0; next }
    {print}
' file

评论

0赞 santy 7/29/2023
有没有通用的解决方案,例如传递变量?我有多个需要替换的多行字符串。
0赞 ufopilot 7/29/2023
@santy你可以通过..mystring="connect_supply_net"
0赞 Ed Morton 7/29/2023
您没有使用任何填充的变量,因此您不需要调用它,您可以执行 .match()$0 ~ mystring
0赞 Kaz 7/29/2023 #5

使用 TXR,我们可以修复所有命令以具有长度保护,并将语句置于与原始语句相同的缩进位置。connect_supply_netif

我们还可以修复“更新状态”块注释的错误缩进。

$ txr fix.txr data
    #-------------------------------------------------------------------------------
    # supply connections
    #-------------------------------------------------------------------------------

        if { [llength $port(VSSE)] > 0 } {
            connect_supply_net VSS -ports $port(VSSE)
        }
        if { [llength $port(VDDP)] > 0 } {
            connect_supply_net VDDS_CPU -ports $port(VDDP)
        }
        if { [llength $port(VDDPE)] > 0 } {
            connect_supply_net VDD_CPU -ports $port(VDDPE)
        }

    #-------------------------------------------------------------------------------
    # Update states
    #-------------------------------------------------------------------------------

法典:

@(repeat)
@  (cases)
@{indent}connect_supply_net @arg -ports $port(@port)
@    (output)
@{indent}if { [llength $port(@port)] > 0 } {
@{indent}    connect_supply_net @arg -ports $port(@port)
@{indent}}
@    (end)
@  (or)
#---@dashes
# Update states
#---@dashes
@    (output)
    #---@dashes
    # Update states
    #---@dashes
@    (end)
@  (or)
@line
@    (do (put-line line))
@  (end)
@(end)

Vim 编写的语法:

Syntax colored by Vim

0赞 Daweo 7/29/2023 #6

使用 sed/awk/perl 替换多行字符串

默认情况下,这些是逐行工作的,但是它们具有允许您解决此问题的功能。

以演示为目的,让内容成为file.txt

Able
Baker
Charlie
Dog

和期望的输出是

Able
Charlie
Baker
Dog

GNU sed 有选项可以-z

将输入视为一组行,每行以零字节( ASCII 'NUL' 字符)而不是换行符。

因此,如果您的文件不包含字节,它将被视为一行,因此命令将是\000

sed -z 's|Baker\nCharlie|Charlie\nBaker|' file.txt

GNU 有 (row-separator) 和 (output row-separator) 内置变量,将它们设置为在文件中从不匹配的模式,并将空字符串设置为一行行为,例如 字节(与上述示例相同)AWKRSORS\000sed

awk 'BEGIN{RS="\000";ORS=""}{sub(/Baker\nCharlie/,"Charlie\nBaker");print}' file.txt

perl运动啜饮模式 参与 和 可以与模式参与 一起工作 ,观察语法与示例相同-0777sed-p -esed

perl -0777 -p -e 's|Baker\nCharlie|Charlie\nBaker|' file.txt

(测试在 GNU sed 4.8、GNU Awk 5.1.0、perl 5、版本 34、subversion 0 中)