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.sh
把test.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
POSIX Sed 不支持 -i
选项。但是 ex 可以编辑文件
到位
Awk 是一个更好的工具,因为数据被分成记录和
字段
无论是 Sed 还是 Awk,您都可以使用换行符或 ;
来完成所有操作
在一次调用中
你有内部没有变量的双引号字符串,不妨使用
单引号
您在没有需要转义的字符时引用了您的文件名
您有几个未引用的变量用法,几乎都不是一个好主意
简单案例
如果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
命令。
有关如何执行通用预转义的信息,请参阅我的 ,但此时您也可以考虑使用其他工具,例如 awk
和 perl
.
[1] GNU Sed 认为选项参数是 -i optional,而 BSD Sed 认为它是 mandatory,这也反映在语法规范中。在相应的 man
页面中:GNU Sed:-i[SUFFIX]
与 BSD Sed -i extension
。
这是我的test.env
RABBITMQ_HOST=127.0.0.1
RABBITMQ_PASS=1234
我想用test.sh
把test.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
POSIX Sed 不支持
-i
选项。但是 ex 可以编辑文件 到位Awk 是一个更好的工具,因为数据被分成记录和 字段
无论是 Sed 还是 Awk,您都可以使用换行符或
;
来完成所有操作 在一次调用中你有内部没有变量的双引号字符串,不妨使用 单引号
您在没有需要转义的字符时引用了您的文件名
您有几个未引用的变量用法,几乎都不是一个好主意
简单案例
如果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 GNUsed
,-i ''
for BSDsed
.(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
命令。
有关如何执行通用预转义的信息,请参阅我的 awk
和 perl
.
[1] GNU Sed 认为选项参数是 -i optional,而 BSD Sed 认为它是 mandatory,这也反映在语法规范中。在相应的 man
页面中:GNU Sed:-i[SUFFIX]
与 BSD Sed -i extension
。