关于bash:shell脚本替换文件中的变量-使用Sed的-i选项进行就地更新时出错

shell script replace variables in file - error with Sed's -i option for in-place updating

这是我的test.env

1
2
RABBITMQ_HOST=127.0.0.1
RABBITMQ_PASS=1234

我想使用test.shtest.env中的值替换为:

1
2
RABBITMQ_HOST=rabbitmq1
RABBITMQ_PASS=12345

这是我的test.sh

1
2
3
4
5
6
7
8
9
#!/bin/bash
echo"hello world"

RABBITMQ_HOST=rabbitmq1
RABBITMQ_PASS=12345
Deploy_path="./config/test.env"

sed -i 's/RABBITMQ_HOST=.*/RABBITMQ_HOST='$RABBITMQ_HOST'/'  $Deploy_path
sed -i 's/RABBITMQ_PASS=.*/RABBITMQ_PASS='$RABBITMQ_HOST'/'  $Deploy_path

但是我有错误

1
2
sed: 1:"./config/test.env": invalid command code .
sed: 1:"./config/test.env": invalid command code .

我该如何解决?


tl; dr:

使用BSD Sed(例如在macOS上也可以找到),必须使用-i ''而不是仅仅使用-i(用于不创建备份文件)才能使命令起作用;例如:

1
sed -i '' 's/RABBITMQ_HOST=.*/RABBITMQ_HOST='"$RABBITMQ_HOST"'/' "$Deploy_path"

要使您的命令与GNU和BSD Sed一起使用,请指定一个非空选项参数(创建备份)并将其直接附加到-i

1
2
sed -i'.bak' 's/RABBITMQ_HOST=.*/RABBITMQ_HOST='"$RABBITMQ_HOST"'/' "$Deploy_path" &&
  rm"$Deploy_path.bak" # remove unneeded backup copy

背景信息,(更多)便携式解决方案和命令的改进可以在下面找到。

可选背景信息

听起来好像您正在使用BSD / macOS sed,其-i选项需要一个选项参数,该参数指定要创建的备份文件的后缀。
因此,是您的sed脚本(违反您的期望)被解释为-i的选项参数(备份后缀),而您的输入文件名被解释为该脚本,显然失败了。

相比之下,您的命令使用GNU sed语法,其中-i可以单独使用,以指示不保留要原位更新的输入文件的备份文件。

等效的BSD sed选项是-i ''-请注意技术上需要使用单独的参数来指定选项参数'',因为它是空字符串(如果您使用的是-i'',则是shell会在sed看到之前简单地剥离''-i''实际上与-i相同)。

遗憾的是,这将不适用于GNU sed,因为它仅在直接附加到-i时才识别选项参数,并且会将单独的''解释为单独的参数,即作为脚本。

这种行为上的差异源于实现-i选项背后的根本不同的设计决策,并且由于向后兼容的原因,它可能不会消失。[1]

如果您不希望创建备份文件,则没有适用于BSD和GNU sed的单一-i语法。

有四个基本选项:

  • (a)如果您只使用GNU或BSD sed,则相应地构造-i选项:对于GNU sed-i,对于BSD sed-i ''

  • (b)指定一个非空后缀作为-i的选项参数,如果直接将其附加到-i选项,则该后缀可用于两种实现;例如-i'.bak'。尽管这总是创建后缀为.bak的备份文件,但是您可以随后将其删除。

  • (c)在运行时确定要处理的是哪个sed实现,并相应地构造-i选项。

  • (d)完全省略-i(不兼容POSIX),并使用临时文件成功替换原始文件:sed '...'"$Deploy_path" > tmp.out && mv tmp.out"$Deploy_path"
    请注意,这实质上是-i在后台执行的操作,这可能会产生意外的副作用,尤其是作为符号链接的输入文件被常规文件替换;但是,-i确实保留了原始文件的某些属性:请参见我的答案的下半部分。

这是(c)的bash实现,该实现还简化了原始代码(使用2个替换的单个sed调用)并使它更健壮(变量被双引号引起了):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#!/bin/bash

RABBITMQ_HOST='rabbitmq1'
RABBITMQ_PASS='12345'
Deploy_path="test.env"

# Construct the Sed-implementation-specific -i option-argument.
# Caveat: The assumption is that if the `sed` is not GNU Sed, it is BSD Sed,
#         but there are Sed implementations that don't support -i at all,
#         because, as Steven Penny points out, -i is not part of POSIX.
suffixArg=()
sed --version 2>/dev/null | grep -q GNU || suffixArg=( '' )

sed -i"${suffixArg[@]}" '
 s/^\\(RABBITMQ_HOST\\)=.*/\\1='
"$RABBITMQ_HOST"'/
 s/^\\(RABBITMQ_PASS\\)=.*/\\1='
"$RABBITMQ_PASS"'/
'
"$Deploy_path"

请注意,使用上面为$RABBITMQ_HOST$RABBITMQ_PASS定义的特定值,可以将它们直接直接拼接到sed脚本中,但是如果这些值包含&/\\或换行符,需要先进行转义,以免破坏sed命令。
有关如何执行通用预转义的信息,请参见我的答案,但是此时您还可以考虑使用其他工具,例如awkperl

[1] GNU Sed认为-i的选项参数是可选的,而BSD Sed则认为它是强制性的,这也反映在语法规范中。在相应的man页面中:GNU Sed:-i[SUFFIX]与BSD Sed -i extension


1
2
3
4
ex -sc '%!awk"\\
\\$1 == "RABBITMQ_HOST" && \\$2 = "rabbitmq1"\\
\\$1 == "RABBITMQ_PASS" && \\$2 = 12345\\
" FS== OFS=='
-cx file
  • POSIX Sed不支持-i选项。但是ex可以编辑文件
    就位

  • Awk是一个更好的工具,因为数据被分为记录和
    字段

  • 无论是Sed还是Awk,您都可以使用换行符或;来完成所有操作
    一次调用

  • 您在双引号中没有任何变量的字符串,不妨使用
    单引号

  • 当文件名中没有需要转义的字符时,您引用了文件名

  • 您在变量中使用了几种未加引号的用法,几乎从来都不是一个好主意


  • 简单案例

    如果test.env仅包含两个变量,则可以简单地创建一个新文件或覆盖现有文件:

    1
    2
    3
    4
    printf"RABBITMQ_HOST=%s\
    RABBITMQ_PASS=%s\
    "
    \\
     "${RABBITMQ_HOST}""${RABBITMQ_PASS}">"$Deploy_path"

    修复未引用的变量并优化SED命令

    尝试如下修复您的命令:

    1
    2
    3
    sed -i -e 's/\\(RABBITMQ_HOST=\\).*/\\1'"$RABBITMQ_HOST"'/' \\
      -e 's/\\(RABBITMQ_PASS=\\).*/\\1'"$RABBITMQ_PASS"'/' \\
     "$Deploy_path"

    您应该将变量用双引号引起来,否则shell将解释其内容。在双引号中的内容中,shell将仅解释$(将变量替换为其内容),反引号和\\(转义)。另请注意,使用了多个-e选项。

    为什么SED对此任务不利(我认为)?

    但是,正如@ mklement0的回答所说,-i在BSD系统上可能无法以这种形式工作。同样,如果在$Deploy_path文件中定义了两个变量(如果文件存在),则该命令仅修改这两个变量。不会将新变量添加到文件中。警告,变量直接嵌入替换中,通常应根据SED规则对它们的值进行转义!

    替代

    如果test.env文件是受信任的,我建议加载变量,修改它们并打印到输出文件:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    (
      # Load variables from test.env
      source test.env

      # Override some variables
      RABBITMQ_HOST=rabbitmq1
      RABBITMQ_PASS=12345

      # Print all variables prefixed with"RABBITMQ_".
      # In POSIX mode, `set` will not output defines and functions
      set -o posix
      set | grep ^RABBITMQ_
    ) >"$Deploy_path"

    考虑调整test.env的文件系统权限。我想,源文件是一个受信任的模板。

    在我看来,没有SED的解决方案更好,因为SED的实现可能会有所不同,并且就地选项可能无法在不同平台上正常工作。

    但是,source有风险吗?

    虽然解析shell变量分配通常是一件容易的事,但与仅提供现成的"脚本"(test.env)相比,它具有更大的风险。例如,考虑test.env中的以下行:

    1
    declare RABBITMQ_HOST=${MYVAR:=rabbitmq1}

    1
    export RABBITMQ_HOST=host

    所有当前建议的解决方案(使用source的代码除外)均假定您将变量分配为RABBITMQ_HOST=...。有些解决方案甚至假定RABBIT_HOST位于行首。嗯,那么您可以修复正则表达式,对吗?仅在这种情况下...

    因此,source的风险与源文件不受信任的风险一样大。考虑一下C中的#include <file>或PHP中的include"file.php"。这些说明也将源包括在当前源中。因此,不要盲目考虑将文件作为反模式。这完全取决于特定的情况。如果您的test.env是要部署的存储库的一部分,则一定可以安全地调用source test.env。不过,这是我的意见。