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



这是我的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 . 

我该如何解决它?

tl;博士

使用BSDSed,例如在macOS上也可以找到的,您必须使用-i ''而不仅仅是-i(用于不创建备份文件)来使您的命令正常工作;例如:

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

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

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

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


可选背景信息

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

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

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

可悲的是,这不适用于GNUsed,因为它只在直接附加到-i时识别选项参数,并将单独的''解释为单独的参数,即脚本

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

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

有四个基本选项:

  • (a) 如果你知道你只会使用 GNUBSDsed,请相应地构建-i选项:-i用于 GNUsed-i ''用于 BSDsed

  • (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调用)并使其更加健壮(变量用双引号引起来):

#!/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]vs. BSD Sed-i extension

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

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

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

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

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

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

简单案例

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

printf "RABBITMQ_HOST=%snRABBITMQ_PASS=%sn" 
"${RABBITMQ_HOST}" "${RABBITMQ_PASS}" > "$Deploy_path"

修复不带引号的变量并优化 SED 命令

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

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文件是可信的,我建议加载变量,修改它们并打印到输出文件:

(
# 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中的以下行:

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肯定是安全的。然而,这是我的看法。

相关内容

  • 没有找到相关文章

最新更新