Contents

sed变量替换

1. 概述

sed 是 Linux 命令行中一个强大的文本处理工具。我们经常使用紧凑的sed单行代码进行文本替换。他们很方便。

然而,当我们用 shell 变量执行 sed替换时,有一些我们应该注意的陷阱

在本教程中,我们将仔细研究使用带有 shell 变量的sed替换所犯的一些常见错误,并提供一些解决方案。

2. 示例问题

为了更轻松地重现这些常见错误并讨论如何修复它们,让我们做一个示例问题。 假设我们有一个文件test.txt

$ cat test.txt
CURRENT_TIME = # fill the current date and time
JAVA_HOME = # fill the JAVA_HOME path

我们要写一个shell脚本,把当前时间和当前系统的JAVA_HOME路径填入上面的文件中。

任务看起来很简单。但是,存在一些潜在的问题。

让我们使用GNU sed 一起编写脚本。

3. 我们应该使用哪些报价?

根据问题的需要,我们需要执行两个替换:当前时间和JAVA_HOME路径。

首先,让我们在正确的地方填写当前时间。我们可以使用 date 命令获取当前时间:

$ cat solution.sh 
#!/bin/sh
MY_DATE=$(date)
sed -i -r 's/^(CURRENT_TIME =).*/\1 $MY_DATE/' test.txt

上面的脚本并不难理解。让我们快速浏览一下。

我们首先从命令替换 中获取当前日期和时间 ,并将其保存在变量MY_DATE 中。

获取日期后,我们使用sed替换将其填充到文件中。我们使用GNU sed命令的 -i选项进行就地编辑

让我们执行我们的脚本并检查它是否按我们预期的那样工作:

$ ./solution.sh
$ cat test.txt 
CURRENT_TIME = $MY_DATE
JAVA_HOME = # fill the JAVA_HOME path

如上面的输出所示,带有“ CURRENT_TIME = ”的行已被替换。但是,填充的不是当前日期和时间,而是文字“ $MY_DATE ”。

发生这种情况是因为我们在 sed命令中使用了单引号。Shell 变量不会在单引号内扩展

因此,快速修复是在sed命令中使用双引号 以允许 shell 变量扩展

$ cat solution.sh
#!/bin/sh
MY_DATE=$(date)
sed -i -r "s/^(CURRENT_TIME =).*/\1 $MY_DATE/" test.txt

现在,让我们 再次测试solution.sh脚本:

$ ./solution.sh 
$ cat test.txt 
CURRENT_TIME = Wed Jan 27 10:02:05 PM CET 2021
JAVA_HOME = # fill the JAVA_HOME path

好的!我们已经在正确的地方填写了日期和时间。

接下来,让我们 在文件中填写JAVA_HOME 路径。

4. 我们应该使用哪个定界符?

现在我们有了当前时间的替代工作,我们可能会认为JAVA_HOME部分或多或少是一个复制和粘贴的工作。

真的那么简单吗?让我们 在solution.sh脚本中再添加一个sed命令 :

$ cat solution.sh
...
sed -i -r "s/^(CURRENT_TIME =).*/\1 $MY_DATE/" test.txt
sed -i -r "s/^(JAVA_HOME =).*/\1 $JAVA_HOME/" test.txt

是时候测试脚本了:

$ ./solution.sh 
sed: -e expression #1, char 24: unknown option to `s'

哎呀!新添加的sed命令不起作用。如果我们仔细检查它,它与其他有效的sed命令非常相似,只有变量不同。

这是怎么回事?

4.1. 选择一个不包含在变量中的定界符

要了解发生了什么,让我们首先检查环境变量*$JAVA_HOME*中的内容:

$ echo $JAVA_HOME 
/usr/lib/jvm/default

我们已经了解到 shell 变量将在双引号内展开。因此,变量展开后,我们的第二条sed命令就变成了:

sed -i -r "s/^(JAVA_HOME =).*/\1 /usr/lib/jvm/default/" test.txt

好吧,上面的 sed命令显然不会起作用,因为变量值中的斜杠 ( / ) 会干扰 ‘ s ’ 命令( s/pattern/replacement/ )。

**幸运的是,我们可以选择其他字符作为’ s’命令的分隔符

让我们稍微 修改一下第二个sed命令,并使用“#”作为s命令的分隔符:

sed -i -r "s#^(JAVA_HOME =).*#\1 $JAVA_HOME#" test.txt

现在,让我们再次测试脚本:

$ ./solution.sh
$ cat test.txt 
CURRENT_TIME = Wed Jan 27 10:36:57 PM CET 2021
JAVA_HOME = /usr/lib/jvm/default

伟大的!问题已经解决了——或者是吗?

4.2. 更好的解决方案

事实上,我们的 solution.sh在大多数情况下都可以工作。但是,值得一提的是,在大多数 *nix 文件系统上,’#’ 是文件名中的有效字符

这意味着,如果有一天我们在JAVA_HOME设置为*/opt/#jvm#*的系统上执行我们的脚本,例如,脚本将再次失败。

为了使我们的脚本适用于所有情况,我们可以:

  • 首先,为sed 的 s命令选择一个分隔符。假设我们将“#”作为分隔符以提高可读性
  • 二、转义变量内容中的所有分隔符
  • 最后在 sed命令中拼装转义后的内容

我们可以使用Bash 替换 来转义定界符。例如,我们可以转义变量*$VAR*中的所有“#”字符:

$ VAR="foo#bar#blah"
$ echo "${VAR//#/\\#}"
foo\#bar\#blah

让我们将它应用到我们的脚本中:

$ cat solution.sh 
#!/bin/sh
MY_DATE=$(date)
sed -i -r "s/^(CURRENT_TIME =).*/\1 $MY_DATE/" test.txt
sed -i -r "s#^(JAVA_HOME =).*#\1 ${JAVA_HOME//#/\\#}#" test.txt

接下来,让我们使用模拟的JAVA_HOME变量执行我们的脚本,并检查它是否按我们预期的那样工作:

$ JAVA_HOME=/opt/#/:/@/-/_/$/jvm ./solution.sh
$ cat test.txt
CURRENT_TIME = Thu Jan 28 11:23:07 AM CET 2021
JAVA_HOME = /opt/#/:/@/-/_/$/jvm

如输出所示,**即使我们的JAVA_HOME 变量包含许多特殊字符,**我们的脚本也能正常工作。