Shell脚本中使用sed时如何避免变量内$符号被展开
咱们一步一步拆解你的问题,先搞清楚为什么之前的脚本不管用,再给你靠谱的解决方案。
最初的问题根源:用eval拼接命令
你最开始用eval来执行拼接的sed命令,这是大忌!当参数里带$时,Shell会在eval执行前就把这些$当成变量展开——如果对应的变量没定义,就会被替换成空字符串,直接导致sed拿到错误的替换内容,自然执行失败。所以永远不要用eval来处理带特殊字符的命令,直接调用sed才是正确的思路。
你更新后的脚本为什么没生效?
看你的执行日志和脚本,主要有两个问题:
sed的
-iE参数兼容性问题:
不同系统的sed语法不一样:GNU sed(比如Linux上的)支持-iE这种合并写法,但BSD sed(比如macOS上的)要求-i后面必须跟备份后缀(如果不需要备份,就写-i ''),再单独加-E。如果你在macOS上用-iE,sed会把E当成备份文件的后缀,只会生成filename.txtE备份文件,原文件根本没被修改!这就是第一种测试情况没成功的核心原因。不必要的
$转义+双引号解析问题:
sed的替换字符串里,$本身并不是特殊字符(只有\和&需要转义),你用${2//'$'/'\$'}把$替换成\$,再放在双引号里,Shell会把\$解析回$,虽然最终sed拿到的内容是对的,但这种转义是多余的。另外你执行第二个测试时,Shell的调试输出把带$的字符串用单引号转义显示,看起来像是命令出错了,但实际执行的命令是对的,只是输出格式的问题——真正的问题还是-i参数的兼容性导致文件没被修改。
正确的解决方案
下面这个脚本既解决了$符号的问题,又兼容不同系统的sed,还能确保参数里的特殊字符不被Shell展开:
#!/bin/bash set -xv fp="/asd/filename.txt" # 转义sed替换字符串中的特殊字符(仅&和\需要转义,$不需要) escape_sed() { printf '%s' "$1" | sed 's/[&\\]/\\&/g' } # 处理sed的-i参数兼容性 if sed --version >/dev/null 2>&1; then # GNU sed(Linux) sed_opts=(-i -E) else # BSD sed(macOS) sed_opts=(-i '' -E) fi # 转义搜索和替换字符串 escaped_search=$(escape_sed "$2") escaped_replace=$(escape_sed "$3") # 执行sed替换 sed "${sed_opts[@]}" "s/(${escaped_search}).*/${escaped_search}:${escaped_replace}/g" "$fp"
测试验证
普通参数测试:
执行./test.sh update hello WORLD,脚本会生成正确的sed命令:sed -i -E 's/(hello).*/hello:WORLD/g' /asd/filename.txt原文件的
hello:world会被替换成hello:WORLD。含
$的参数测试:
执行./test.sh update hello '$apr1$hosgaxyv$D0KXp5dCyZ2BUYCS9BmHu1'(注意用单引号包裹第三个参数,避免Shell提前展开$),脚本会把$当作字面量处理,最终文件内容会变成:hello:$apr1$hosgaxyv$D0KXp5dCyZ2BUYCS9BmHu1
关键注意事项
- 执行脚本时,必须用单引号包裹含
$的参数,否则Shell会在脚本接收参数前就把$当成变量展开,脚本拿到的就是错误的内容。 - 永远避免用
eval处理命令,这会引入大量安全隐患和不可控的字符展开问题。
内容的提问来源于stack exchange,提问作者Andromeda

