Bash 模板化:如何使用 Bash 从模板构建配置文件?

发布于 2024-09-02 16:25:09 字数 1086 浏览 2 评论 0 原文

我正在编写一个脚本来自动为我自己的网络服务器创建 Apache 和 PHP 的配置文件。我不想使用任何 GUI,如 CPanel 或 ISPConfig。

我有一些 Apache 和 PHP 配置文件的模板。 Bash 脚本需要读取模板,进行变量替换并将解析后的模板输出到某个文件夹中。最好的方法是什么?我可以想出几种办法。哪一种是最好的,或者可能有一些更好的方法可以做到这一点?我想在纯 Bash 中执行此操作(例如,在 PHP 中很容易)

  1. 如何替换文本文件中的 ${} 占位符?

template.txt:

The number is ${i}
The word is ${word}

script.sh:

#!/bin/sh

#set variables
i=1
word="dog"
#read in template one line at the time, and replace variables
#(more natural (and efficient) way, thanks to Jonathan Leffler)
while read line
do
    eval echo "$line"
done < "./template.txt"

顺便说一句,如何将输出重定向到此处的外部文件?如果变量包含引号,我是否需要转义某些内容?

  1. 使用 cat & sed 用它的值替换每个变量:

给定 template.txt (见上文)

命令:

cat template.txt | sed -e "s/\${i}/1/" | sed -e "s/\${word}/dog/"

对我来说似乎很糟糕,因为需要转义许多不同的符号,并且对于许多变量,该行将太长。

您能想到其他优雅且安全的解决方案吗?

I'm writing a script to automate creating configuration files for Apache and PHP for my own webserver. I don't want to use any GUIs like CPanel or ISPConfig.

I have some templates of Apache and PHP configuration files. Bash script needs to read templates, make variable substitution and output parsed templates into some folder. What is the best way to do that? I can think of several ways. Which one is the best or may be there are some better ways to do that? I want to do that in pure Bash (it's easy in PHP for example)

  1. How to replace ${} placeholders in a text file?

template.txt:

The number is ${i}
The word is ${word}

script.sh:

#!/bin/sh

#set variables
i=1
word="dog"
#read in template one line at the time, and replace variables
#(more natural (and efficient) way, thanks to Jonathan Leffler)
while read line
do
    eval echo "$line"
done < "./template.txt"

BTW, how do I redirect output to external file here? Do I need to escape something if variables contain, say, quotes?

  1. Using cat & sed for replacing each variable with its value:

Given template.txt (see above)

Command:

cat template.txt | sed -e "s/\${i}/1/" | sed -e "s/\${word}/dog/"

Seems bad to me because of the need to escape many different symbols and with many variables the line will be tooooo long.

Can you think of some other elegant and safe solution?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(26

意中人 2024-09-09 16:25:09

尝试 envsubst

$ cat envsubst-template.txt
Variable FOO is (${FOO}).
Variable BAR is (${BAR}).

$ FOO=myfoo

$ BAR=mybar

$ export FOO BAR

$ cat envsubst-template.txt | envsubst
Variable FOO is (myfoo).
Variable BAR is (mybar).

Try envsubst

$ cat envsubst-template.txt
Variable FOO is (${FOO}).
Variable BAR is (${BAR}).

$ FOO=myfoo

$ BAR=mybar

$ export FOO BAR

$ cat envsubst-template.txt | envsubst
Variable FOO is (myfoo).
Variable BAR is (mybar).
梦亿 2024-09-09 16:25:09

heredoc 是一种构建 conf 文件模板的内置方法。

STATUS_URI="/hows-it-goin";  MONITOR_IP="10.10.2.15";

cat >/etc/apache2/conf.d/mod_status.conf <<EOF
<Location ${STATUS_URI}>
    SetHandler server-status
    Order deny,allow
    Deny from all
    Allow from ${MONITOR_IP}
</Location>
EOF

关于yottsa的回答: envsubst 对我来说是新的。极好的。

A heredoc is a builtin way to template a conf file.

STATUS_URI="/hows-it-goin";  MONITOR_IP="10.10.2.15";

cat >/etc/apache2/conf.d/mod_status.conf <<EOF
<Location ${STATUS_URI}>
    SetHandler server-status
    Order deny,allow
    Deny from all
    Allow from ${MONITOR_IP}
</Location>
EOF

Regarding yottsa's answer: envsubst was new to me. Fantastic.

浅蓝的眸勾画不出的柔情 2024-09-09 16:25:09

您可以使用以下命令:

perl -p -i -e 's/\$\{([^}]+)\}/defined $ENV{$1} ? $ENV{$1} : 
amp;/eg' < template.txt

将所有 ${...} 字符串替换为相应的环境变量(不要忘记在运行此脚本之前导出它们)。

对于纯 bash,这应该可以工作(假设变量不包含 ${...} 字符串)

#!/bin/bash
while read -r line ; do
    while [[ "$line" =~ (\$\{[a-zA-Z_][a-zA-Z_0-9]*\}) ]] ; do
        LHS=${BASH_REMATCH[1]}
        RHS="$(eval echo "\"$LHS\"")"
        line=${line//$LHS/$RHS}
    done
    echo "$line"
done

:如果 RHS 引用某个引用自身的变量,则解决方案不会挂起:

#!/bin/bash
line="$(cat; echo -n a)"
end_offset=${#line}
while [[ "${line:0:$end_offset}" =~ (.*)(\$\{([a-zA-Z_][a-zA-Z_0-9]*)\})(.*) ]] ; do
    PRE="${BASH_REMATCH[1]}"
    POST="${BASH_REMATCH[4]}${line:$end_offset:${#line}}"
    VARNAME="${BASH_REMATCH[3]}"
    eval 'VARVAL="

警告:我不知道如何正确处理 bash 中的 NUL 输入或保留尾随换行符的数量。最后一个变体之所以如此呈现,是因为 shell“喜欢”二进制输入:

  1. read 将解释反斜杠。
  2. read -r 不会解释反斜杠,但如果最后一行不以换行符结尾,仍然会删除最后一行。
  3. "$(...)" 将删除尽可能多的尾随换行符,因此我以 结束 ...; echo -n a 并使用 echo -n "${line:0:-1}":这会删除最后一个字符(即 a)并保留输入中有多少个尾随换行符(包括没有)。
$VARNAME'"' line="$PRE$VARVAL$POST" end_offset=${#PRE} done echo -n "${line:0:-1}"

警告:我不知道如何正确处理 bash 中的 NUL 输入或保留尾随换行符的数量。最后一个变体之所以如此呈现,是因为 shell“喜欢”二进制输入:

  1. read 将解释反斜杠。
  2. read -r 不会解释反斜杠,但如果最后一行不以换行符结尾,仍然会删除最后一行。
  3. "$(...)" 将删除尽可能多的尾随换行符,因此我以 结束 ...; echo -n a 并使用 echo -n "${line:0:-1}":这会删除最后一个字符(即 a)并保留输入中有多少个尾随换行符(包括没有)。

You can use this:

perl -p -i -e 's/\$\{([^}]+)\}/defined $ENV{$1} ? $ENV{$1} : 
amp;/eg' < template.txt

to replace all ${...} strings with corresponding enviroment variables (do not forget to export them before running this script).

For pure bash this should work (assuming that variables do not contain ${...} strings):

#!/bin/bash
while read -r line ; do
    while [[ "$line" =~ (\$\{[a-zA-Z_][a-zA-Z_0-9]*\}) ]] ; do
        LHS=${BASH_REMATCH[1]}
        RHS="$(eval echo "\"$LHS\"")"
        line=${line//$LHS/$RHS}
    done
    echo "$line"
done

. Solution that does not hang if RHS references some variable that references itself:

#!/bin/bash
line="$(cat; echo -n a)"
end_offset=${#line}
while [[ "${line:0:$end_offset}" =~ (.*)(\$\{([a-zA-Z_][a-zA-Z_0-9]*)\})(.*) ]] ; do
    PRE="${BASH_REMATCH[1]}"
    POST="${BASH_REMATCH[4]}${line:$end_offset:${#line}}"
    VARNAME="${BASH_REMATCH[3]}"
    eval 'VARVAL="

WARNING: I do not know a way to correctly handle input with NULs in bash or preserve the amount of trailing newlines. Last variant is presented as it is because shells “love” binary input:

  1. read will interpret backslashes.
  2. read -r will not interpret backslashes, but still will drop the last line if it does not end with a newline.
  3. "$(…)" will strip as many trailing newlines as there are present, so I end with ; echo -n a and use echo -n "${line:0:-1}": this drops the last character (which is a) and preserves as many trailing newlines as there was in the input (including no).
$VARNAME'"' line="$PRE$VARVAL$POST" end_offset=${#PRE} done echo -n "${line:0:-1}"

WARNING: I do not know a way to correctly handle input with NULs in bash or preserve the amount of trailing newlines. Last variant is presented as it is because shells “love” binary input:

  1. read will interpret backslashes.
  2. read -r will not interpret backslashes, but still will drop the last line if it does not end with a newline.
  3. "$(…)" will strip as many trailing newlines as there are present, so I end with ; echo -n a and use echo -n "${line:0:-1}": this drops the last character (which is a) and preserves as many trailing newlines as there was in the input (including no).
女中豪杰 2024-09-09 16:25:09

我同意使用 sed:它是搜索/替换的最佳工具。这是我的方法:

$ cat template.txt
the number is ${i}
the dog's name is ${name}

$ cat replace.sed
s/${i}/5/g
s/${name}/Fido/g

$ sed -f replace.sed template.txt > out.txt

$ cat out.txt
the number is 5
the dog's name is Fido

I agree with using sed: it is the best tool for search/replace. Here is my approach:

$ cat template.txt
the number is ${i}
the dog's name is ${name}

$ cat replace.sed
s/${i}/5/g
s/${name}/Fido/g

$ sed -f replace.sed template.txt > out.txt

$ cat out.txt
the number is 5
the dog's name is Fido
罗罗贝儿 2024-09-09 16:25:09

我有一个像 mogsie 这样的 bash 解决方案,但使用heredoc而不是herestring来避免转义双引号

eval "cat <<EOF
$(<template.txt)
EOF
" 2> /dev/null

I have a bash solution like mogsie but with heredoc instead of herestring to allow you to avoid escaping double quotes

eval "cat <<EOF
$(<template.txt)
EOF
" 2> /dev/null
冬天旳寂寞 2024-09-09 16:25:09

尝试 eval

我认为 eval 效果非常好。它处理带有换行符、空格和各种 bash 内容的模板。当然,如果您可以完全控制模板本身:

$ cat template.txt
variable1 = ${variable1}
variable2 = $variable2
my-ip = \"$(curl -s ifconfig.me)\"

$ echo $variable1
AAA
$ echo $variable2
BBB
$ eval "echo \"$(<template.txt)\"" 2> /dev/null
variable1 = AAA
variable2 = BBB
my-ip = "11.22.33.44"

当然,应该谨慎使用此方法,因为 eval 可以执行任意代码。以 root 身份运行它几乎是不可能的。模板中的引号需要转义,否则会被eval吃掉。

如果您更喜欢 cat 而不是 echo,您也可以使用此处文档

$ eval "cat <<< \"$(<template.txt)\"" 2> /dev/null

@plockc 提供了一个避免 bash 引号转义问题的解决方案:

$ eval "cat <<EOF
$(<template.txt)
EOF
" 2> /dev/null

编辑: 删除了部分关于使用 sudo 以 root 身份运行它...

编辑: 添加了有关如何转义引号的评论,添加了 plockc 的解决方案!

Try eval

I think eval works really well. It handles templates with linebreaks, whitespace, and all sorts of bash stuff. If you have full control over the templates themselves of course:

$ cat template.txt
variable1 = ${variable1}
variable2 = $variable2
my-ip = \"$(curl -s ifconfig.me)\"

$ echo $variable1
AAA
$ echo $variable2
BBB
$ eval "echo \"$(<template.txt)\"" 2> /dev/null
variable1 = AAA
variable2 = BBB
my-ip = "11.22.33.44"

This method should be used with care, of course, since eval can execute arbitrary code. Running this as root is pretty much out of the question. Quotes in the template need to be escaped, otherwise they will be eaten by eval.

You can also use here documents if you prefer cat to echo

$ eval "cat <<< \"$(<template.txt)\"" 2> /dev/null

@plockc provoded a solution that avoids the bash quote escaping issue:

$ eval "cat <<EOF
$(<template.txt)
EOF
" 2> /dev/null

Edit: Removed part about running this as root using sudo...

Edit: Added comment about how quotes need to be escaped, added plockc's solution to the mix!

无力看清 2024-09-09 16:25:09

编辑 2017 年 1 月 6 日

我需要在配置文件中保留双引号,因此使用 sed 进行双转义双引号会有所帮助:

render_template() {
  eval "echo \"$(sed 's/\"/\\\\"/g' $1)\""
}

我无法想到保留尾随新行,但会保留中间的空行。


虽然这是一个老话题,但我在这里找到了更优雅的解决方案: http://pempek.net/articles/2013/07/08/bash-sh-as-template-engine/

#!/bin/sh

# render a template configuration file
# expand variables + preserve formatting
render_template() {
  eval "echo \"$(cat $1)\""
}

user="Gregory"
render_template /path/to/template.txt > path/to/configuration_file

所有功劳都归功于格雷戈里·帕科斯

Edit Jan 6, 2017

I needed to keep double quotes in my configuration file so double escaping double quotes with sed helps:

render_template() {
  eval "echo \"$(sed 's/\"/\\\\"/g' $1)\""
}

I can't think of keeping trailing new lines, but empty lines in between are kept.


Although it is an old topic, IMO I found out more elegant solution here: http://pempek.net/articles/2013/07/08/bash-sh-as-template-engine/

#!/bin/sh

# render a template configuration file
# expand variables + preserve formatting
render_template() {
  eval "echo \"$(cat $1)\""
}

user="Gregory"
render_template /path/to/template.txt > path/to/configuration_file

All credits to Grégory Pakosz.

活雷疯 2024-09-09 16:25:09

与其重新发明轮子,不如使用 envsubst
几乎可以在任何场景中使用,例如从 Docker 容器中的环境变量构建配置文件。

如果在 Mac 上,请确保您有 homebrew 然后从 gettext 链接它:

brew install gettext
brew link --force gettext

./template.cfg

# We put env variables into placeholders here
this_variable_1 = ${SOME_VARIABLE_1}
this_variable_2 = ${SOME_VARIABLE_2}

./.env:

SOME_VARIABLE_1=value_1
SOME_VARIABLE_2=value_2

./ configure.sh

#!/bin/bash
cat template.cfg | envsubst > whatever.cfg

现在只需使用它:

# make script executable
chmod +x ./configure.sh
# source your variables
. .env
# export your variables
# In practice you may not have to manually export variables 
# if your solution depends on tools that utilise .env file 
# automatically like pipenv etc. 
export SOME_VARIABLE_1 SOME_VARIABLE_2
# Create your config file
./configure.sh

Instead of reinventing the wheel go with envsubst
Can be used in almost any scenario, for instance building configuration files from environment variables in docker containers.

If on mac make sure you have homebrew then link it from gettext:

brew install gettext
brew link --force gettext

./template.cfg

# We put env variables into placeholders here
this_variable_1 = ${SOME_VARIABLE_1}
this_variable_2 = ${SOME_VARIABLE_2}

./.env:

SOME_VARIABLE_1=value_1
SOME_VARIABLE_2=value_2

./configure.sh

#!/bin/bash
cat template.cfg | envsubst > whatever.cfg

Now just use it:

# make script executable
chmod +x ./configure.sh
# source your variables
. .env
# export your variables
# In practice you may not have to manually export variables 
# if your solution depends on tools that utilise .env file 
# automatically like pipenv etc. 
export SOME_VARIABLE_1 SOME_VARIABLE_2
# Create your config file
./configure.sh
庆幸我还是我 2024-09-09 16:25:09

我会这样做,可能效率较低,但更容易阅读/维护。

TEMPLATE='/path/to/template.file'
OUTPUT='/path/to/output.file'

while read LINE; do
  echo $LINE |
  sed 's/VARONE/NEWVALA/g' |
  sed 's/VARTWO/NEWVALB/g' |
  sed 's/VARTHR/NEWVALC/g' >> $OUTPUT
done < $TEMPLATE

I'd have done it this way, probably less efficient, but easier to read/maintain.

TEMPLATE='/path/to/template.file'
OUTPUT='/path/to/output.file'

while read LINE; do
  echo $LINE |
  sed 's/VARONE/NEWVALA/g' |
  sed 's/VARTWO/NEWVALB/g' |
  sed 's/VARTHR/NEWVALC/g' >> $OUTPUT
done < $TEMPLATE
稍尽春風 2024-09-09 16:25:09

如果您想使用 Jinja2 模板,请参阅此项目:j2cli

它支持:

  • 来自 JSON、INI、YAML 文件和输入流的模板
  • 来自环境变量的模板

If you want to use Jinja2 templates, see this project: j2cli.

It supports:

  • Templates from JSON, INI, YAML files and input streams
  • Templating from environment variables
倾`听者〃 2024-09-09 16:25:09

已接受答案的更长但更强大的版本:

perl -pe 's;(\\*)(\$([a-zA-Z_][a-zA-Z_0-9]*)|\$\{([a-zA-Z_][a-zA-Z_0-9]*)\})?;substr($1,0,int(length($1)/2)).($2&&length($1)%2?$2:$ENV{$3||$4});eg' template.txt

这会扩展 $VAR ${VAR} 的所有实例> 到它们的环境值(或者,如果它们未定义,则为空字符串)。

它正确地转义反斜杠,并接受反斜杠转义的 $ 来抑制替换(与 envsubst 不同,事实证明,它不这样做)。

因此,如果您的环境是:

FOO=bar
BAZ=kenny
TARGET=backslashes
NOPE=engi

并且您的模板是:

Two ${TARGET} walk into a \\$FOO. \\\\
\\\$FOO says, "Delete C:\\Windows\\System32, it's a virus."
$BAZ replies, "\${NOPE}s."

结果将是:

Two backslashes walk into a \bar. \\
\$FOO says, "Delete C:\Windows\System32, it's a virus."
kenny replies, "${NOPE}s."

如果您只想在 $ 之前转义反斜杠(您可以在模板中不变地写“C:\Windows\System32”),请使用此稍微修改的版本:

perl -pe 's;(\\*)(\$([a-zA-Z_][a-zA-Z_0-9]*)|\$\{([a-zA-Z_][a-zA-Z_0-9]*)\});substr($1,0,int(length($1)/2)).(length($1)%2?$2:$ENV{$3||$4});eg' template.txt

A longer but more robust version of the accepted answer:

perl -pe 's;(\\*)(\$([a-zA-Z_][a-zA-Z_0-9]*)|\$\{([a-zA-Z_][a-zA-Z_0-9]*)\})?;substr($1,0,int(length($1)/2)).($2&&length($1)%2?$2:$ENV{$3||$4});eg' template.txt

This expands all instances of $VAR or ${VAR} to their environment values (or, if they're undefined, the empty string).

It properly escapes backslashes, and accepts a backslash-escaped $ to inhibit substitution (unlike envsubst, which, it turns out, doesn't do this).

So, if your environment is:

FOO=bar
BAZ=kenny
TARGET=backslashes
NOPE=engi

and your template is:

Two ${TARGET} walk into a \\$FOO. \\\\
\\\$FOO says, "Delete C:\\Windows\\System32, it's a virus."
$BAZ replies, "\${NOPE}s."

the result would be:

Two backslashes walk into a \bar. \\
\$FOO says, "Delete C:\Windows\System32, it's a virus."
kenny replies, "${NOPE}s."

If you only want to escape backslashes before $ (you could write "C:\Windows\System32" in a template unchanged), use this slightly-modified version:

perl -pe 's;(\\*)(\$([a-zA-Z_][a-zA-Z_0-9]*)|\$\{([a-zA-Z_][a-zA-Z_0-9]*)\});substr($1,0,int(length($1)/2)).(length($1)%2?$2:$ENV{$3||$4});eg' template.txt
厌倦 2024-09-09 16:25:09

这是另一个解决方案:生成一个包含所有变量和模板文件内容的 bash 脚本,该脚本将如下所示:

word=dog           
i=1                
cat << EOF         
the number is ${i} 
the word is ${word}

EOF                

如果我们将此脚本输入 bash,它将产生所需的输出:

the number is 1
the word is dog

以下是如何生成该脚本和将该脚本输入 bash:

(
    # Variables
    echo word=dog
    echo i=1

    # add the template
    echo "cat << EOF"
    cat template.txt
    echo EOF
) | bash

讨论

  • 括号打开一个子 shell,其目的是将生成的所有输出组合在一起
  • 在子 shell 中,我们生成所有变量声明
  • 同样在子 shell 中,我们生成 cat 命令与 HEREDOC
  • 最后,我们将子 shell 输出提供给 bash 并生成所需的输出
  • 如果您想将此输出重定向到文件中,请将最后一行替换为:

    <前><代码>) |重击>输出.txt

Here is another solution: generate a bash script with all the variables and the contents of the template file, that script would look like this:

word=dog           
i=1                
cat << EOF         
the number is ${i} 
the word is ${word}

EOF                

If we feed this script into bash it would produce the desired output:

the number is 1
the word is dog

Here is how to generate that script and feed that script into bash:

(
    # Variables
    echo word=dog
    echo i=1

    # add the template
    echo "cat << EOF"
    cat template.txt
    echo EOF
) | bash

Discussion

  • The parentheses opens a sub shell, its purpose is to group together all the output generated
  • Within the sub shell, we generate all the variable declarations
  • Also in the sub shell, we generate the cat command with HEREDOC
  • Finally, we feed the sub shell output to bash and produce the desired output
  • If you want to redirect this output into a file, replace the last line with:

    ) | bash > output.txt
    
浪菊怪哟 2024-09-09 16:25:09

这是另一个纯 bash 解决方案:

  • 它使用heredoc,所以:
    • 复杂性不会因为额外所需的语法而增加
    • 模板可以包含 bash 代码
      • 这还允许您正确缩进内容。见下文。
  • 它不使用 eval,所以:
    • 尾随空行的渲染没有问题
    • 模板中的引号没有问题

$ cat code

#!/bin/bash
LISTING=$( ls )

cat_template() {
  echo "cat << EOT"
  cat "$1"
  echo EOT
}

cat_template template | LISTING="$LISTING" bash

输入:
$ cat template (带有尾随换行符和双引号)

<html>
  <head>
  </head>
  <body> 
    <p>"directory listing"
      <pre>
$( echo "$LISTING" | sed 's/^/        /' )
      <pre>
    </p>
  </body>
</html>

输出:

<html>
  <head>
  </head>
  <body> 
    <p>"directory listing"
      <pre>
        code
        template
      <pre>
    </p>
  </body>
</html>

Here's another pure bash solution:

  • it's using heredoc, so:
    • complexity doesn't increase because of additionaly required syntax
    • template can include bash code
      • that also allows you to indent stuff properly. See below.
  • it doesn't use eval, so:
    • no problems with the rendering of trailing empty lines
    • no problems with quotes in the template

$ cat code

#!/bin/bash
LISTING=$( ls )

cat_template() {
  echo "cat << EOT"
  cat "$1"
  echo EOT
}

cat_template template | LISTING="$LISTING" bash

Input:
$ cat template (with trailing newlines and double quotes)

<html>
  <head>
  </head>
  <body> 
    <p>"directory listing"
      <pre>
$( echo "$LISTING" | sed 's/^/        /' )
      <pre>
    </p>
  </body>
</html>

Output:

<html>
  <head>
  </head>
  <body> 
    <p>"directory listing"
      <pre>
        code
        template
      <pre>
    </p>
  </body>
</html>
谁把谁当真 2024-09-09 16:25:09

使用纯 bash 从 ZyX 获取答案,但使用新样式的正则表达式匹配和间接参数替换,它变成:

#!/bin/bash
regex='\$\{([a-zA-Z_][a-zA-Z_0-9]*)\}'
while read line; do
    while [[ "$line" =~ $regex ]]; do
        param="${BASH_REMATCH[1]}"
        line=${line//${BASH_REMATCH[0]}/${!param}}
    done
    echo $line
done

Taking the answer from ZyX using pure bash but with new style regex matching and indirect parameter substitution it becomes:

#!/bin/bash
regex='\$\{([a-zA-Z_][a-zA-Z_0-9]*)\}'
while read line; do
    while [[ "$line" =~ $regex ]]; do
        param="${BASH_REMATCH[1]}"
        line=${line//${BASH_REMATCH[0]}/${!param}}
    done
    echo $line
done
陈年往事 2024-09-09 16:25:09

如果使用Perl是一种选择,并且您满足于仅基于环境变量进行基础扩展(而不是所有shell 变量),考虑Stuart P. Bentley 的稳健答案

这个答案旨在提供一个仅 bash 的解决方案,尽管使用了eval - 应该可以安全使用

目标是:

  • 支持${name}$name变量引用的扩展。
  • 阻止所有其他扩展:
    • 命令替换($(...) 和旧语法 `...`
    • 算术替换($((...)) 和旧语法 $[...])。
  • 允许通过添加前缀 \ (\${name}) 来选择性地抑制变量扩展。
  • 保留特殊字符。在输入中,特别是 "\ 实例。
  • 允许通过参数或标准输入输入。

函数 expandVars()

expandVars() {
  local txtToEval=$* txtToEvalEscaped
  # If no arguments were passed, process stdin input.
  (( $# == 0 )) && IFS= read -r -d '' txtToEval
  # Disable command substitutions and arithmetic expansions to prevent execution
  # of arbitrary commands.
  # Note that selectively allowing $((...)) or $[...] to enable arithmetic
  # expressions is NOT safe, because command substitutions could be embedded in them.
  # If you fully trust or control the input, you can remove the `tr` calls below
  IFS= read -r -d '' txtToEvalEscaped < <(printf %s "$txtToEval" | tr '`([' '\1\2\3')
  # Pass the string to `eval`, escaping embedded double quotes first.
  # `printf %s` ensures that the string is printed without interpretation
  # (after processing by by bash).
  # The `tr` command reconverts the previously escaped chars. back to their
  # literal original.
  eval printf %s "\"${txtToEvalEscaped//\"/\\\"}\"" | tr '\1\2\3' '`(['
}

示例:

$ expandVars '\$HOME="$HOME"; `date` and $(ls)'
$HOME="/home/jdoe"; `date` and $(ls)  # only $HOME was expanded

$ printf '\$SHELL=${SHELL}, but "$(( 1 \ 2 ))" will not expand' | expandVars
$SHELL=/bin/bash, but "$(( 1 \ 2 ))" will not expand # only ${SHELL} was expanded
  • 出于性能原因,该函数将 stdin 输入一次性读取到内存中,但也可以轻松地将函数调整为逐行方法。
  • > 支持非基本变量扩展,例如${HOME:0:10},只要它们不包含嵌入式命令或算术替换,例如${首页:0:$(回显10)}
    • 此类嵌入替换实际上会破坏函数(因为所有 $(` 实例都会被盲目转义)。
    • 同样,格式错误的变量引用,例如 ${HOME(缺少结束 })会破坏函数。
  • 由于 bash 对双引号字符串的处理,反斜杠的处理方式如下:
    • \$name 防止扩展。
    • 后面不跟 $ 的单个 \ 将按原样保留。
    • 如果要表示多个相邻 \ 实例,则必须将它们加倍;例如:
      • \\ -> \ - 与 \
      • 相同

      • \\\\ -> <代码>\\
    • 输入内容不得包含以下(很少使用)字符,这些字符用于内部用途:0x10x20x3 .
  • 很大程度上假设的担忧是,如果 bash 应引入新的扩展语法,此函数可能无法阻止此类扩展 - 请参阅下面的不使用 eval 的解决方案。

如果您正在寻找限制性更强的解决方案,支持${name}扩展 - 即强制 > 大括号,忽略 $name 引用 - 请参阅我的这个答案


这是来自已接受的答案<的仅 bash、eval-free 解决方案的改进版本/a>

改进如下:

  • 支持扩展 ${name}$name 变量引用。
  • 支持 \ - 转义不应扩展的变量引用。
  • 与上面基于 eval 的解决方案不同,
    • 非基本扩展将被忽略
    • 格式错误的变量引用将被忽略(它们不会破坏脚本)
 IFS= read -d '' -r lines # read all input from stdin at once
 end_offset=${#lines}
 while [[ "${lines:0:end_offset}" =~ (.*)\$(\{([a-zA-Z_][a-zA-Z_0-9]*)\}|([a-zA-Z_][a-zA-Z_0-9]*))(.*) ]] ; do
      pre=${BASH_REMATCH[1]} # everything before the var. reference
      post=${BASH_REMATCH[5]}${lines:end_offset} # everything after
      # extract the var. name; it's in the 3rd capture group, if the name is enclosed in {...}, and the 4th otherwise
      [[ -n ${BASH_REMATCH[3]} ]] && varName=${BASH_REMATCH[3]} || varName=${BASH_REMATCH[4]}
      # Is the var ref. escaped, i.e., prefixed with an odd number of backslashes?
      if [[ $pre =~ \\+$ ]] && (( ${#BASH_REMATCH} % 2 )); then
           : # no change to $lines, leave escaped var. ref. untouched
      else # replace the variable reference with the variable's value using indirect expansion
           lines=${pre}${!varName}${post}
      fi
      end_offset=${#pre}
 done
 printf %s "$lines"

If using Perl is an option and you're content with basing expansions on environment variables only (as opposed to all shell variables), consider Stuart P. Bentley's robust answer.

This answer aims to provide a bash-only solution that - despite use of eval - should be safe to use.

The goals are:

  • Support expansion of both ${name} and $name variable references.
  • Prevent all other expansions:
    • command substitutions ($(...) and legacy syntax `...`)
    • arithmetic substitutions ($((...)) and legacy syntax $[...]).
  • Allow selective suppression of variable expansion by prefixing with \ (\${name}).
  • Preserve special chars. in the input, notably " and \ instances.
  • Allow input either via arguments or via stdin.

Function expandVars():

expandVars() {
  local txtToEval=$* txtToEvalEscaped
  # If no arguments were passed, process stdin input.
  (( $# == 0 )) && IFS= read -r -d '' txtToEval
  # Disable command substitutions and arithmetic expansions to prevent execution
  # of arbitrary commands.
  # Note that selectively allowing $((...)) or $[...] to enable arithmetic
  # expressions is NOT safe, because command substitutions could be embedded in them.
  # If you fully trust or control the input, you can remove the `tr` calls below
  IFS= read -r -d '' txtToEvalEscaped < <(printf %s "$txtToEval" | tr '`([' '\1\2\3')
  # Pass the string to `eval`, escaping embedded double quotes first.
  # `printf %s` ensures that the string is printed without interpretation
  # (after processing by by bash).
  # The `tr` command reconverts the previously escaped chars. back to their
  # literal original.
  eval printf %s "\"${txtToEvalEscaped//\"/\\\"}\"" | tr '\1\2\3' '`(['
}

Examples:

$ expandVars '\$HOME="$HOME"; `date` and $(ls)'
$HOME="/home/jdoe"; `date` and $(ls)  # only $HOME was expanded

$ printf '\$SHELL=${SHELL}, but "$(( 1 \ 2 ))" will not expand' | expandVars
$SHELL=/bin/bash, but "$(( 1 \ 2 ))" will not expand # only ${SHELL} was expanded
  • For performance reasons, the function reads stdin input all at once into memory, but it's easy to adapt the function to a line-by-line approach.
  • Also supports non-basic variable expansions such as ${HOME:0:10}, as long as they contain no embedded command or arithmetic substitutions, such as ${HOME:0:$(echo 10)}
    • Such embedded substitutions actually BREAK the function (because all $( and ` instances are blindly escaped).
    • Similarly, malformed variable references such as ${HOME (missing closing }) BREAK the function.
  • Due to bash's handling of double-quoted strings, backslashes are handled as follows:
    • \$name prevents expansion.
    • A single \ not followed by $ is preserved as is.
    • If you want to represent multiple adjacent \ instances, you must double them; e.g.:
      • \\ -> \ - the same as just \
      • \\\\ -> \\
    • The input mustn't contain the following (rarely used) characters, which are used for internal purposes: 0x1, 0x2, 0x3.
  • There's a largely hypothetical concern that if bash should introduce new expansion syntax, this function might not prevent such expansions - see below for a solution that doesn't use eval.

If you're looking for a more restrictive solution that only supports ${name} expansions - i.e., with mandatory curly braces, ignoring $name references - see this answer of mine.


Here is an improved version of the bash-only, eval-free solution from the accepted answer:

The improvements are:

  • Support for expansion of both ${name} and $name variable references.
  • Support for \-escaping variable references that shouldn't be expanded.
  • Unlike the eval-based solution above,
    • non-basic expansions are ignored
    • malformed variable references are ignored (they don't break the script)
 IFS= read -d '' -r lines # read all input from stdin at once
 end_offset=${#lines}
 while [[ "${lines:0:end_offset}" =~ (.*)\$(\{([a-zA-Z_][a-zA-Z_0-9]*)\}|([a-zA-Z_][a-zA-Z_0-9]*))(.*) ]] ; do
      pre=${BASH_REMATCH[1]} # everything before the var. reference
      post=${BASH_REMATCH[5]}${lines:end_offset} # everything after
      # extract the var. name; it's in the 3rd capture group, if the name is enclosed in {...}, and the 4th otherwise
      [[ -n ${BASH_REMATCH[3]} ]] && varName=${BASH_REMATCH[3]} || varName=${BASH_REMATCH[4]}
      # Is the var ref. escaped, i.e., prefixed with an odd number of backslashes?
      if [[ $pre =~ \\+$ ]] && (( ${#BASH_REMATCH} % 2 )); then
           : # no change to $lines, leave escaped var. ref. untouched
      else # replace the variable reference with the variable's value using indirect expansion
           lines=${pre}${!varName}${post}
      fi
      end_offset=${#pre}
 done
 printf %s "$lines"
野味少女 2024-09-09 16:25:09

为了跟进本页上的plockc的答案,这里有一个适合破折号的版本,适合那些希望避免bashisms的人。

eval "cat <<EOF >outputfile
$( cat template.in )
EOF
" 2> /dev/null

To follow up on plockc's answer on this page, here is a dash-suitable version, for those of you looking to avoid bashisms.

eval "cat <<EOF >outputfile
$( cat template.in )
EOF
" 2> /dev/null
闻呓 2024-09-09 16:25:09

尝试 shtpl

shtpl 的完美案例。 (我的项目,所以它没有被广泛使用并且缺乏文档。但是这里是它提供的解决方案。也许你想测试它。)

只需执行:

$ i=1 word=dog sh -c "$( shtpl template.txt )"

结果是:

the number is 1
the word is dog

玩得开心。

Try shtpl

Perfect case for shtpl. (project of mine, so it is not widely in use and lacks in documentation. But here is the solution it offers anyhow. May you want to test it.)

Just execute:

$ i=1 word=dog sh -c "$( shtpl template.txt )"

Result is:

the number is 1
the word is dog

Have fun.

冷弦 2024-09-09 16:25:09

本页描述了使用 awk 的答案

awk '{while(match($0,"[$]{[^}]*}")) {var=substr($0,RSTART+2,RLENGTH -3);gsub("[$]{"var"}",ENVIRON[var])}}1' < input.txt > output.txt

This page describes an answer with awk

awk '{while(match($0,"[$]{[^}]*}")) {var=substr($0,RSTART+2,RLENGTH -3);gsub("[$]{"var"}",ENVIRON[var])}}1' < input.txt > output.txt
沫雨熙 2024-09-09 16:25:09
# Usage: template your_file.conf.template > your_file.conf
template() {
        local IFS line
        while IFS=

这是纯 bash 函数,可根据您的喜好进行调整,在生产中使用,并且不应因任何输入而中断。
如果它坏了 - 请告诉我。

\n\r' read -r line ; do line=${line//\\/\\\\} # escape backslashes line=${line//\"/\\\"} # escape " line=${line//\`/\\\`} # escape ` line=${line//\$/\\\$} # escape $ line=${line//\\\${/\${} # de-escape ${ - allows variable substitution: ${var} ${var:-default_value} etc # to allow arithmetic expansion or command substitution uncomment one of following lines: # line=${line//\\\$\(/\$\(} # de-escape $( and $(( - allows $(( 1 + 2 )) or $( command ) - UNSECURE # line=${line//\\\$\(\(/\$\(\(} # de-escape $(( - allows $(( 1 + 2 )) eval "echo \"${line}\""; done < "$1" }

这是纯 bash 函数,可根据您的喜好进行调整,在生产中使用,并且不应因任何输入而中断。
如果它坏了 - 请告诉我。

# Usage: template your_file.conf.template > your_file.conf
template() {
        local IFS line
        while IFS=

This is the pure bash function adjustable to your liking, used in production and should not break on any input.
If it breaks - let me know.

\n\r' read -r line ; do line=${line//\\/\\\\} # escape backslashes line=${line//\"/\\\"} # escape " line=${line//\`/\\\`} # escape ` line=${line//\$/\\\$} # escape $ line=${line//\\\${/\${} # de-escape ${ - allows variable substitution: ${var} ${var:-default_value} etc # to allow arithmetic expansion or command substitution uncomment one of following lines: # line=${line//\\\$\(/\$\(} # de-escape $( and $(( - allows $(( 1 + 2 )) or $( command ) - UNSECURE # line=${line//\\\$\(\(/\$\(\(} # de-escape $(( - allows $(( 1 + 2 )) eval "echo \"${line}\""; done < "$1" }

This is the pure bash function adjustable to your liking, used in production and should not break on any input.
If it breaks - let me know.

梦开始←不甜 2024-09-09 16:25:09

您还可以使用bashible(它内部使用上面/下面描述的评估方法)。

有一个示例,如何从多个部分生成 HTML:

https://github .com/mig1984/bashible/tree/master/examples/templates

You can also use bashible (which internally uses the evaluating approach described above/below).

There is an example, how to generate a HTML from multiple parts:

https://github.com/mig1984/bashible/tree/master/examples/templates

葮薆情 2024-09-09 16:25:09

在这里查看简单的变量替换 python 脚本: https://github.com/jeckep/vsubst

它是使用非常简单:

python subst.py --props secure.properties --src_path ./templates --dst_path ./dist

Look at simple variables substitution python script here: https://github.com/jeckep/vsubst

It is very simple to use:

python subst.py --props secure.properties --src_path ./templates --dst_path ./dist
他是夢罘是命 2024-09-09 16:25:09

您可以使用带有环境变量的模板文件并运行

cat template-file.yaml |envsubst

You can use a template file with environment variables and run

cat template-file.yaml |envsubst
小帐篷 2024-09-09 16:25:09

这是一个保留空格的 bash 函数:

# Render a file in bash, i.e. expand environment variables. Preserves whitespace.
function render_file () {
    while IFS='' read line; do
        eval echo \""${line}"\"
    done < "${1}"
}

Here's a bash function that preserves whitespace:

# Render a file in bash, i.e. expand environment variables. Preserves whitespace.
function render_file () {
    while IFS='' read line; do
        eval echo \""${line}"\"
    done < "${1}"
}
孤单情人 2024-09-09 16:25:09

这是一个基于其他一些答案的修改后的 perl 脚本:

perl -pe 's/([^\\]|^)\$\{([a-zA-Z_][a-zA-Z_0-9]*)\}/$1.$ENV{$2}/eg' -i template

功能(基于我的需要,但应该很容易修改):

  • 跳过转义参数扩展(例如 \${VAR})。
  • 支持 ${VAR} 形式的参数扩展,但不支持 $VAR。
  • 如果没有 VAR envar,则将 ${VAR} 替换为空白字符串。
  • 名称中仅支持 az、AZ、0-9 和下划线字符(不包括第一个位置的数字)。

Here's a modified perl script based on a few of the other answers:

perl -pe 's/([^\\]|^)\$\{([a-zA-Z_][a-zA-Z_0-9]*)\}/$1.$ENV{$2}/eg' -i template

Features (based on my needs, but should be easy to modify):

  • Skips escaped parameter expansions (e.g. \${VAR}).
  • Supports parameter expansions of the form ${VAR}, but not $VAR.
  • Replaces ${VAR} with a blank string if there is no VAR envar.
  • Only supports a-z, A-Z, 0-9 and underscore characters in the name (excluding digits in the first position).
撩人痒 2024-09-09 16:25:09

您还可以使用 printf 来填充模板。

#!/bin/bash

IFS='' read -rd '' TEMPL <<-'EOB'
The number is %d
The word is "%s"
Birds of Massachusetts:
    %s




EOB

N=12
WORD="Bird"
MULTILINE="Eastern Bluebirds
Common Grackles"

echo "START"
printf "${TEMPL}" ${N} ${WORD} "${MULTILINE}"
echo "END"

这是输出,引号和空格完好无损:

START
The number is 12
The word is "Bird"
Birds of Massachusetts:
    Eastern Bluebirds
Common Grackles




END

You can also use printf to fill a template.

#!/bin/bash

IFS='' read -rd '' TEMPL <<-'EOB'
The number is %d
The word is "%s"
Birds of Massachusetts:
    %s




EOB

N=12
WORD="Bird"
MULTILINE="Eastern Bluebirds
Common Grackles"

echo "START"
printf "${TEMPL}" ${N} ${WORD} "${MULTILINE}"
echo "END"

Here's the output, with quotes and whitespace intact:

START
The number is 12
The word is "Bird"
Birds of Massachusetts:
    Eastern Bluebirds
Common Grackles




END
李不 2024-09-09 16:25:09

我一直在寻找一个仅标准实用程序的解决方案,而且我对评估任意模板感到不舒服——这很容易出错。最终我想出了以下方法。

模板文件:

$ cat addusers.template.sh 
# Available variables:
# $USER_NAME
# $USER_ID
# $USER_GROUP_ID

adduser --uid $USER_ID --gid $USER_GROUP_ID --gecos '' --disabled-password $USER_NAME
cp -v /tmp/bash.rc /home/$USER_NAME/.bashrc
chown -v $USER_ID:$USER_GROUP_ID /home/$USER_NAME/.bashrc
chmod 444 /home/$USER_NAME/.bashrc

Bash 脚本:

$ cat cook-addusers.sh
# Read template file skipping comments and empty lines
TEMPLATE="$(grep -vE -e '^[[:blank:]]*#' -e '^[[:blank:]]*

以及运行它的结果:

$ ./cook-addusers.sh
$ cat generated.sh 
adduser --uid 1001 --gid 101 --gecos '' --disabled-password admin
cp -v /tmp/bash.rc /home/admin/.bashrc
chown -v 1001:101 /home/admin/.bashrc
chmod 444 /home/admin/.bashrc
adduser --uid 1010 --gid 101 --gecos '' --disabled-password superuser
cp -v /tmp/bash.rc /home/superuser/.bashrc
chown -v 1010:101 /home/superuser/.bashrc
chmod 444 /home/superuser/.bashrc
adduser --uid 1100 --gid 101 --gecos '' --disabled-password supervisor
cp -v /tmp/bash.rc /home/supervisor/.bashrc
chown -v 1100:101 /home/supervisor/.bashrc
chmod 444 /home/supervisor/.bashrc

它相当粗糙,但似乎在典型的 Linux 和 BSD 上都按预期工作......抱歉,我的意思是 macOS ;-)

addusers.template.sh)" # Cook regex template with the list of substituted variables for VAR in USER_NAME USER_ID USER_GROUP_ID; do RE="${RE}"'s/\

以及运行它的结果:


它相当粗糙,但似乎在典型的 Linux 和 BSD 上都按预期工作......抱歉,我的意思是 macOS ;-)

$VAR'/

以及运行它的结果:


它相当粗糙,但似乎在典型的 Linux 和 BSD 上都按预期工作......抱歉,我的意思是 macOS ;-)

$VAR'/g\; ' done # Fill in the template for a list of users for USER_NAME in admin superuser supervisor; do USER_ID=$(id -u "$USER_NAME") USER_GROUP_ID=$(id -g "$USER_NAME") sed "$(eval echo "$RE")" <<<"${TEMPLATE}" done >generated.sh

以及运行它的结果:


它相当粗糙,但似乎在典型的 Linux 和 BSD 上都按预期工作......抱歉,我的意思是 macOS ;-)

I was looking for a standard-utils-only solution and also I was not comfortable with evaling an arbitrary template – this can go wrong only too easily. Eventually I came up with the following approach.

Template file:

$ cat addusers.template.sh 
# Available variables:
# $USER_NAME
# $USER_ID
# $USER_GROUP_ID

adduser --uid $USER_ID --gid $USER_GROUP_ID --gecos '' --disabled-password $USER_NAME
cp -v /tmp/bash.rc /home/$USER_NAME/.bashrc
chown -v $USER_ID:$USER_GROUP_ID /home/$USER_NAME/.bashrc
chmod 444 /home/$USER_NAME/.bashrc

Bash script:

$ cat cook-addusers.sh
# Read template file skipping comments and empty lines
TEMPLATE="$(grep -vE -e '^[[:blank:]]*#' -e '^[[:blank:]]*

And the result of running it:

$ ./cook-addusers.sh
$ cat generated.sh 
adduser --uid 1001 --gid 101 --gecos '' --disabled-password admin
cp -v /tmp/bash.rc /home/admin/.bashrc
chown -v 1001:101 /home/admin/.bashrc
chmod 444 /home/admin/.bashrc
adduser --uid 1010 --gid 101 --gecos '' --disabled-password superuser
cp -v /tmp/bash.rc /home/superuser/.bashrc
chown -v 1010:101 /home/superuser/.bashrc
chmod 444 /home/superuser/.bashrc
adduser --uid 1100 --gid 101 --gecos '' --disabled-password supervisor
cp -v /tmp/bash.rc /home/supervisor/.bashrc
chown -v 1100:101 /home/supervisor/.bashrc
chmod 444 /home/supervisor/.bashrc

It's rather crude though seems to be working as intended both on typical Linux and BSD… sorry, I mean macOS ;-)

addusers.template.sh)" # Cook regex template with the list of substituted variables for VAR in USER_NAME USER_ID USER_GROUP_ID; do RE="${RE}"'s/\

And the result of running it:


It's rather crude though seems to be working as intended both on typical Linux and BSD… sorry, I mean macOS ;-)

$VAR'/

And the result of running it:


It's rather crude though seems to be working as intended both on typical Linux and BSD… sorry, I mean macOS ;-)

$VAR'/g\; ' done # Fill in the template for a list of users for USER_NAME in admin superuser supervisor; do USER_ID=$(id -u "$USER_NAME") USER_GROUP_ID=$(id -g "$USER_NAME") sed "$(eval echo "$RE")" <<<"${TEMPLATE}" done >generated.sh

And the result of running it:


It's rather crude though seems to be working as intended both on typical Linux and BSD… sorry, I mean macOS ;-)

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文