Linux bash:如何根据 another/different 行中的模式替换行中的字符串?

Linux bash: How do I replace a string on a line based on a pattern on another/different line?

我有一个包含以下数据的文件:

GS*PO*112233*445566*20211006*155007*2010408*X*004010~

ST*850*0001~
BEG*00*DS*A-112233**20211005~
REF*K6*Drop Ship Order~
REF*ZZ*SO168219~
REF*DC*ABC~

ST*850*0002~
BEG*00*DS*A-44556**20211005~
REF*K6*Drop Ship Order~
REF*ZZ*PO54361~

ST*850*0003~
BEG*00*DS*A-12345**20211005~
REF*K6*Drop Ship Order~
REF*DC*XYZ~
REF*ZZ*SO897654~

为清楚起见,我在每个 ST*850 行上方插入了空行。这是我想要做的:

  1. 搜索模式 REF*ZZ*SO
  2. 如果找到,则将前面的 ST*850 行替换为 ST*850C

因此生成的文件将如下所示:

GS*PO*112233*445566*20211006*155007*2010408*X*004010~

ST*850C*0001~
BEG*00*DS*A-112233**20211005~
REF*K6*Drop Ship Order~
REF*ZZ*SO168219~
REF*DC*ABC~

ST*850*0002~
BEG*00*DS*A-44556**20211005~
REF*K6*Drop Ship Order~
REF*ZZ*PO54361~

ST*850C*0003~
BEG*00*DS*A-12345**20211005~
REF*K6*Drop Ship Order~
REF*DC*XYZ~
REF*ZZ*SO897654~

这是我尝试过的:

sed -i -n '/^REF\*ZZ\*SO/!{x;s/ST\*850\*/ST\*850C\*/;x};x;1!p;${x;p}' file

这会将所有三行 ST*850 替换为 ST*850C 而不仅仅是第一行和第三行。我做错了什么?

尽管标签中不包含 perl,但 perl 解决方案怎么样。

perl -0777 -aF'(?=ST\*850)' -ne '
    print map {/REF\*ZZ\*SO/ && s/ST\*850/$&C/; $_} @F;
' file

输出:

GS*PO*112233*445566*20211006*155007*2010408*X*004010~

ST*850C*0001~
BEG*00*DS*A-112233**20211005~
REF*K6*Drop Ship Order~
REF*ZZ*SO168219~
REF*DC*ABC~

ST*850*0002~
BEG*00*DS*A-44556**20211005~
REF*K6*Drop Ship Order~
REF*ZZ*PO54361~

ST*850C*0003~
BEG*00*DS*A-12345**20211005~
REF*K6*Drop Ship Order~
REF*DC*XYZ~
REF*ZZ*SO897654~
  • -0777 选项告诉 perl 一次 slurp 整个文件。
  • -a 选项启用 auto split 模式,然后拆分 片段存储在数组 @F.
  • -F 选项指定分割输入的模式。
  • 正则表达式 (?=ST\*850) 是正向后视匹配 在字符串的开头 ST*850.
  • -ne 选项基本上等同于 sed
  • map {..} @F函数根据@F的所有元素转换 大括号内的语句。
  • 语句/REF\*ZZ\*SO/ && s/ST\*850/$&C/翻译为: "如果@F 的元素与模式 /REF*ZZ*SO/ 匹配,则执行 元素的替换 s/ST*850/$&C/。
  • 最后的 $_ 是 perl 的默认变量,类似于 sed 的 pattern space 并且将是 map 函数的 return 值。

假设ST本质上是一个记录分隔符,你可以使用一个简单的awk脚本来收集当前记录中的行,如果条件合适,打印一个修改过的不同的行。

awk 'BEGIN { ORS = RS = "\nST" }
    /REF\*ZZ\*SO/ { sub(/^\*850/, "*<850C") }1' filename

BEGIN 子句将记录分隔符 (RS) 和输出记录分隔符 (ORS) 设置为字符串 ST,前面有换行符。 (尝试包含星号变得很复杂,所以我避免了。)最后的 1 是常见的 Awk shorthand 用于“打印到达此处的所有内容”。

sed 对于除了简单的基于行的替换之外的任何事情都相当笨拙;我想你会发现切换到更高级的语言会提高可维护性。

你的解决方案替换所有出现的原因是你没有附加行,你只是在模式和保持 space 之间来回交换。您需要的是一种缓冲,直到遇到 special 行中的一个或另一个。这通常通过将模式 space 附加到保留 space 直到满足条件来完成。

使用 sed(使用 GNU sed 测试):

sed -n '/^ST\*850\*/{x;1!p;b};
        /^REF\*ZZ\*SO/{1!{H;x};s/ST\*850\*/ST*850C*/;p;b};
        1{h;b};H;${x;p}' file
  • 如果是 ST*850* 行,交换模式并保持 spaces。然后,如果它不是第一行,则打印。开始新的循环。 hold space 包含 ST*850* 行。存储在保留 space 中的前面的行(如果有)已被打印。
  • 否则,如果它是 REF*ZZ*SO 行,交换模式并保持 spaces 并进行替换。然后,如果它不是第一行,则打印。开始新的循环。 hold space 包含 REF*ZZ*SO 行。存储在 hold space 中的前面的行(如果有)已被打印(修改后)。
  • 否则,如果它是第一行,则用模式 space 替换保留 space 并开始新的循环。因此,保留 space 包含第一行。
  • 否则将模式 space 附加到保留 space。如果它是最后一行交换模式并保持 spaces 并打印。

纯 Bash:更冗长,但希望不需要任何额外解释。

#! /bin/bash

init_chunk()
{
  prefix=
  suffix=
  chunk=()
  refzzso=
}

print_chunk()
{
  if [[ ${#chunk[@]} > 0 ]]; then
    if [[ $refzzso == true ]]; then
      printf '%sC%s\n' "$prefix" "$suffix"
    else
      printf '%s%s\n' "$prefix" "$suffix"
    fi
    printf '%s\n' "${chunk[@]}"
  fi
}

init_chunk
while read -r line; do
  # Check for header.
  if [[ $line =~ ^(ST\*850)(.*) ]]; then
    # Print previous chunk.
    print_chunk
    # Begin new chunk.
    init_chunk "${BASH_REMATCH[1]}" "${BASH_REMATCH[2]}"
    continue
  fi
  # Check if in a chunk.
  if [[ $prefix ]]; then
    # Check for modifier.
    if [[ $line =~ ^REF\*ZZ\*SO ]]; then
      refzzso=true
    fi
    chunk+=("$line")
  else
    printf '%s\n' "$line"
  fi
done
# Print last chunk.
print_chunk

使用 sed 进行预处理以插入新行,然后将每个块视为 awk 记录,例如:

sed 's/^ST\*850/\n&/' | awk '/REF\*ZZ\*SO/ { sub(/ST\*850/, "&C") } 1' RS=

这可能适合您 (GNU sed):

sed '/ST\*850/{:a;/REF\*ZZ\*SO/!{N;ba};s/.*ST\*850/&C/}' file

如果一行包含 ST*850,则开始收集行。

在匹配包含 REF*ZZ*SO 的行时使用贪婪将 C 附加到最新的 ST*850 字符串。

N.B。正则表达式 .* 确保匹配将从集合的结尾而不是集合的开头回溯。