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
行上方插入了空行。这是我想要做的:
- 搜索模式
REF*ZZ*SO
- 如果找到,则将前面的
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。正则表达式 .*
确保匹配将从集合的结尾而不是集合的开头回溯。
我有一个包含以下数据的文件:
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
行上方插入了空行。这是我想要做的:
- 搜索模式
REF*ZZ*SO
- 如果找到,则将前面的
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。正则表达式 .*
确保匹配将从集合的结尾而不是集合的开头回溯。