You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用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"
}

使用说明

  1. 将上述函数加入你的POSIX shell脚本中
  2. 调用方式:find_replace 'bar\n' "$(cat inputB.txt)" inputA.txt
  3. 如果需要直接修改原文件,可以将输出重定向到临时文件后覆盖原文件:
    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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 21:34:54