如何用POSIX awk/sed/ed实现固定多行字符串替换功能?
在POSIX /bin/sh环境中用非GNU扩展的awk、sed或ed实现固定字符串查找替换
sd和fastmod这类工具可以轻松实现固定字符串的查找替换,但如何在POSIX /bin/sh 环境中,仅使用无GNU扩展的 awk、sed 和/或 ed 复现该功能?
输入输出示例
输入A
export NL=' ' printf '%s\n' 'foo' echo "can bar${NL}hazmuch$NL" # randomly a comment
输入B
export NL=' ' printf '%s\n' 'far' echo "out man${NL}wow$NL" # weird
输出
在输入A中查找bar后跟换行符('bar\n'),并将其替换为完整的输入B内容,得到如下输出:
export NL=' ' printf '%s\n' 'foo' echo "can export NL=' ' printf '%s\n' 'far' echo "out man${NL}wow$NL" # weirdhazmuch$NL" # randomly a comment
现有尝试及问题
ed尝试代码(执行后生成空文件)
convert_newlines() { printf '%s' "$1" | awk '{ gsub(/\\n/, "\n") printf "%s", $0 }' } choose_delim() { for delim in @ % + - _ = : / \| \# \\; do case "$1$2" in *"$delim"*) continue ;; *) printf '%s' "$delim" return 0 ;; esac done >&2 printf 'Error: Unable to find suitable delimiter.\n' exit 1 } escape_search() { printf '%s' "$1" | sed -e 's/[.\[\*^$]/\\&/g' -e "s/[$delim\\\\]/\\\\&/g" } escape_replace() { printf '%s' "$1" | sed -e 's/[&]/\\&/g' -e "s/[$delim\\\\]/\\\\&/g" -e 's/$/\\n/g' -e 's/\\\\n$//' } find_replace() { if [ "$#" -ne 3 ]; then >&2 printf "Usage: find_replace 'search_string' 'replacement_string' filename" exit 1 fi search_string="$1" replacement_string="$2" filename="$3" if [ ! -w "$filename" ]; then >&2 printf 'Error: Cannot write to file "%s".\n' "${filename}" exit 1 fi search_plain="$(convert_newlines "$search_string")" replace_plain="$(convert_newlines "$replacement_string")" delim="$(choose_delim "$search_plain" "$replace_plain")" search_escaped="$(escape_search "$search_plain")" replace_escaped="$(escape_replace "$replace_plain")" ed_script=$(printf '1,$s%s%s%s%s%sg\nw\n' "$delim" "$search_escaped" "$delim" "$replace_escaped" "$delim") if ! printf '%s' "${ed_script}" | ed -s "${filename}"; then >&2 printf 'Error: Failed to process "%s"\n' "${filename}" exit 1 fi }
awk尝试代码(未正确读取文件内容)
find_replace() { # you can add err handling from ^ search_string="$1" replacement_string="$2" filename="$3" awk_script=' BEGIN { search = ARGV[1] replace = ARGV[2] delete ARGV[1] delete ARGV[2] # Initialize variables result = "" pos = 1 search_len = length(search) content_len = length(file_content) while (pos <= content_len) { idx = index(substr(file_content, pos), search) if (idx == 0) { result = result substr(file_content, pos) break } else { idx = idx + pos - 1 result = result substr(file_content, pos, idx - pos) replace pos = idx + search_len } } printf "%s", result } ' new_content="$(mktemp)" if awk "${awk_script}" "${search_string}" "${replacement_string}" "${filename}" > "${new_content}"; then cat -- "${new_content}" rm -- "${new_content}" else >&2 printf 'Error: Failed to process "%s"\n' "${filename}" exit 1 fi }
解决方案
基于awk的实现(支持跨换行的固定字符串替换)
核心思路是先将整个文件读入内存,再进行全局固定字符串替换,避免逐行处理的局限性:
find_replace() { if [ "$#" -ne 3 ]; then >&2 printf "用法: find_replace '搜索字符串' '替换字符串' 文件名\n" exit 1 fi local search="$1" local replace="$2" local file="$3" # 检查文件是否可读 if [ ! -r "$file" ]; then >&2 printf "错误: 文件 %s 不可读\n" "$file" exit 1 fi awk -v search="$search" -v replace="$replace" ' { # 将所有行拼接成完整内容,保留原始换行 content = content $0 "\n" } END { # 移除最后多余的换行(因为最后一行本身可能不带换行) if (length(content) > 0) { content = substr(content, 1, length(content)-1) } result = "" pos = 1 search_len = length(search) content_len = length(content) while (pos <= content_len) { # 查找当前位置起的搜索字符串 idx = index(substr(content, pos), search) if (idx == 0) { # 没找到,拼接剩余内容 result = result substr(content, pos) break } else { # 找到,拼接前面的内容+替换字符串,移动位置 result = result substr(content, pos, idx-1) replace pos = pos + idx + search_len - 1 } } printf "%s", result }' "$file" }
使用说明
- 将上述函数加入你的POSIX shell脚本中
- 调用方式:
find_replace 'bar\n' "$(cat inputB.txt)" inputA.txt - 如果需要直接修改原文件,可以将输出重定向到临时文件后覆盖原文件:
find_replace 'bar\n' "$(cat inputB.txt)" inputA.txt > temp.txt && mv temp.txt inputA.txt
为什么之前的尝试失败
- ed尝试:
ed的substitute命令默认逐行处理,无法直接匹配跨换行的字符串;另外转义替换内容时,\n的处理不符合ed语法要求,导致脚本执行出错清空文件。 - awk尝试:原代码中
file_content变量未赋值,根本没有读取文件内容,自然无法进行替换操作。
内容的提问来源于stack exchange,提问作者Samuel Marks
相关产品推荐
相关产品推荐

