shell 脚本替换文件中的变量 - Sed 的就地更新 -i 选项出错

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

这是我的test.env

RABBITMQ_HOST=127.0.0.1
RABBITMQ_PASS=1234

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

RABBITMQ_HOST=rabbitmq1
RABBITMQ_PASS=12345

这是我的test.sh

#!/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 

但是我有错误

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

我该如何解决?

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

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

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

  4. 你有内部没有变量的双引号字符串,不妨使用 单引号

  5. 您在没有需要转义的字符时引用了您的文件名

  6. 您有几个未引用的变量用法,几乎都不是一个好主意

简单案例

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

printf "RABBITMQ_HOST=%s\nRABBITMQ_PASS=%s\n" \
  "${RABBITMQ_HOST}" "${RABBITMQ_PASS}" > "$Deploy_path"

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

尝试按如下方式修复您的命令:

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

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

为什么 SED 不适合这项任务(在我看来)?

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

备选

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

(
  # 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 变量赋值通常是一项简单的任务,但它比仅采购现成的 "script" (test.env 更具风险)。例如,考虑 test.env 中的以下行:

declare RABBITMQ_HOST=${MYVAR:=rabbitmq1}

export RABBITMQ_HOST=host

所有当前建议的解决方案,除了使用 source 的代码外,假设您将变量分配为 RABBITMQ_HOST=...。一些解决方案甚至假设 RABBIT_HOST 位于行首。啊,那你可能会修复正则表达式,对吧?只针对这个案例...

因此,source 的风险与来源文件不受信任的程度一样大。想想 C 中的 #include <file>,或 PHP 中的 include "file.php"。这些指令也将源代码包含到当前源代码中。所以不要盲目地将采购文件视为反模式。这一切都取决于具体情况。如果您的 test.env 是正在部署的存储库的一部分,那么调用 source test.env 肯定是安全的。不过,这是我的看法。

tl;博士:

使用BSD Sed,比如在macOS上也可以找到,你 必须使用 -i '' 而不是仅 -i (因为不创建备份文件)才能使您的命令生效;例如:

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

要使您的命令GNU 和 BSD Sed一起工作,请指定一个nonempty 选项参数(创建备份)和直接将其附加-i

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选项参数 (备份后缀),你输入的filename被解释为script,这显然失败了。

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

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

遗憾的是,这将不适用于 GNU sed,因为它仅在 直接附加 [=156] 时识别选项参数=] 到 -i,并将单独的 '' 解释为单独的参数,即 script.

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

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

有四个基本选项:

  • (a) 如果您知道您只会使用 GNU BSD sed,相应地构建 -i 选项:-i for GNU sed-i '' for BSD sed.

  • (b) 指定一个 nonempty 后缀作为 -i 的选项参数,如果附加它 直接 -i 选项,适用于 both 实现;例如,-i'.bak'。虽然这总是会创建一个后缀为 .bak 的备份文件,但您可以稍后将其删除。

  • (c) 在运行时确定您正在处理哪个 sed 实现并相应地构建 -i 选项。

  • (d) 完全省略 -i(不符合 POSIX),并使用一个临时文件来替换原始文件:sed '...' "$Deploy_path" > tmp.out && mv tmp.out "$Deploy_path" .
    请注意,这本质上是 -i 在幕后所做的,这可能会产生意想不到的副作用,特别是 符号链接 的输入文件被 常规文件; -i,但是,保留原始文件的某些属性:参见我的 this answer 的下半部分。

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

#!/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\)=.*/='"$RABBITMQ_HOST"'/
 s/^\(RABBITMQ_PASS\)=.*/='"$RABBITMQ_PASS"'/
' "$Deploy_path"

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


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