使用Bash脚本从模板创建新文件

63

我需要创建很相似的配置文件和init.d文件。这些文件允许在我的服务器上部署新的HTTP服务。这些文件都是相同的,只有一些参数在不同的文件中改变(如listen_port、域名和服务器上的路径)。

由于任何错误都会导致服务失效,我想使用Bash脚本来创建这些文件。

例如:

generate_new_http_service.sh 8282 subdomain.domain.example /home/myapp/rootOfHTTPService

我正在寻找一种模板化模块,可以在bash中使用。这个模板化模块将使用一些通用的confinit.d脚本来创建新的脚本。

我可以使用Python模板引擎吗?

12个回答

105

您可以使用heredoc来实现。例如:

generate.sh:

#!/bin/sh

#define parameters which are passed in.
PORT=$1
DOMAIN=$2

#define the template.
cat  << EOF
This is my template.
Port is $PORT
Domain is $DOMAIN
EOF

输出:

$ generate.sh 8080 domain.example

This is my template.
Port is 8080
Domain is domain.example

或将其保存到文件中:

$ generate.sh 8080 domain.example > result

7
在 heredoc 中的文件可以是一个单独的文件吗? - Jonathan Allard
不容易:https://dev59.com/6GYq5IYBdhLWcg3wfwtx - Bryan Larsen
3
非常容易。这个技巧的名称是“eval”。请查看我的答案。 - FooF
@JonathanAllard 你可以使用 https://unix.stackexchange.com/a/88492,将输出重定向到脚本内的新文件。 - Jonaias
循环和分支怎么样? - x-yuri

48

bash的模板模块?使用sed,卢克!这里是数百万种可能的方法之一的示例:

$ cat template.txt 
#!/bin/sh

echo Hello, I am a server running from %DIR% and listening for connection at %HOST% on port %PORT% and my configuration file is %DIR%/server.conf

$ cat create.sh 
#!/bin/sh

sed -e "s;%PORT%;$1;g" -e "s;%HOST%;$2;g" -e "s;%DIR%;$3;g" template.txt > script.sh

$ bash ./create.sh 1986 example.com /tmp
$ bash ./script.sh 
Hello, I am a server running from /tmp and listening for connection at example.com on port 1986 and my configuration file is /tmp/server.conf
$ 

2
一般来说,这种方法存在问题。例如,如果$1的值包含%HOST%,那么第二次替换将无意中替换它。因此,我建议使用另一种方法。 - Alexey

28

你可以直接在bash中完成此操作,甚至不需要使用sed。编写如下脚本:

#!/bin/bash

cat <<END
this is a template
with $foo
and $bar
END

然后这样调用:

foo=FOO bar=BAR ./template 

19

对于简单的文件生成,基本上只需要进行以下操作

 . "${config_file}"
 template_str=$(cat "${template_file}")
 eval "echo \"${template_str}\""
在这里,${config_file} 包含以 shell 解析格式表示的配置变量,${template_file} 是一个看起来像 shell 常规文件的模板文件。第一行将文件 ${config_file} 引入,第二行将文件 ${template_file} 的内容放入 shell 变量 template_str 中。最后在第三行中,我们构建了 shell 命令 echo "${template_str}"(其中双引号表达式 "${template_str}" 被扩展),并对其进行评估。
有一些限制需要注意,如需要进行 shell 转义。如果模板文件是外部生成的,出于安全考虑,在执行之前需要实现适当的过滤,以避免例如在模板文件中注入著名的 $(rm -rf /) 导致文件丢失。请参考https://serverfault.com/a/699377/120756以获取这两个文件的示例内容。

@JaysonRaymond - 看到你的评论,我添加了三行 shell 脚本如何工作的说明。请注意,这与 bash 无关; 据我理解,我认为这应该适用于任何合理标准的 Bourne shell (ash, dash, zsh, 非自由 Unix 系统中的 /bin/sh)。我曾在嵌入式 Linux 系统中使用 busybox ash 来使用此技巧。 - FooF
这对我来说非常完美,应该被接受! - Nam G VU
明显无法生成 Bash 脚本。 - milahu
@MilaNautikus - 通过这种方法生成bash脚本,您可以使用反斜杠转义您不想展开的内容。但很快就会变得混乱。更好的方式是在此方法的顶部添加分层,以某种方式标记要展开的内容。但更为恰当的可能是重新考虑整个方法。 - FooF
已解决 ; ) - milahu

11
这是我最终采用的解决问题的方法。相较于其他方法,我发现这个方法更加灵活,并且避免了一些引号相关的问题。 fill.sh:
#!/usr/bin/env sh

config="$1"
template="$2"
destination="$3"

cp "$template" "$destination"

while read line; do
    setting="$( echo "$line" | cut -d '=' -f 1 )"
    value="$( echo "$line" | cut -d '=' -f 2- )"

    sed -i -e "s;%${setting}%;${value};g" "$destination"
done < "$config"

模板:

Template full of important %THINGS%

"Note that quoted %FIELDS% are handled correctly"

If I need %NEWLINES% then I can add them as well.

配置:

THINGS=stuff
FIELDS="values work too!"
NEWLINES="those\\nnifty\\nlinebreaks"

结果: 充满重要内容的模板

"Note that quoted "values work too!" are handled correctly"

If I need those
nifty
linebreaks then I can add them as well.

8

[编辑] 我更改了我的答案,那是几年前的事了。

我喜欢上面FooF的答案: https://dev59.com/l2025IYBdhLWcg3wHR4Y#30872526

然而,我不想要一个中间变量将模板文件的整个内容存储在内存中。

. "${config_file}"
eval "echo \"$(cat "${template_file}")\""

示例

创建一个模板文件。我们把它叫做example.tpl

Hello, ${NAME}!
Today, the weather is ${WEATHER}. Enjoy!

创建一个配置文件来存储您的变量。我们将其称为good.conf:
NAME=John
WEATHER=good

现在,在你想要呈现模板的脚本中,你可以写入以下内容:
#!/usr/bin/env bash

template_file=example.tpl
config_file=good.conf

. "${config_file}"
eval "echo \"$(cat "${template_file}")\""

# Or store the output in a file
eval "echo \"$(cat "${template_file}")\"" > out

你应该能看到这个美妙的输出 :)
Hello, John!
Today, the weather is good. Enjoy!

谨慎使用eval

当您使用eval时,如果模板文件包含某些指令,它们将被执行,这可能是危险的。例如,让我们将上面的example.tpl更改为以下内容:

Hello, ${NAME}!
Today, the weather is ${WEATHER}. Enjoy!

I'm a hacker, hu hu! Look, fool!
$(ls /)

现在,如果您渲染模板文件,您将看到以下内容:
Hello, John!
Today, the weather is good. Enjoy!

I'm a hacker, hu hu! Look, fool!
bin
boot
dev
etc
home
lib
lib64
media
mnt
opt
proc
root
run
sbin
srv
sys
tmp
usr
var

现在请编辑您的文件good.conf,并将其内容改为以下内容:
NAME=$(ls -l /var)
WEATHER=good

并渲染模板。您应该看到类似于这样的内容:

Hello, total 8
drwxr-xr-x.  2 root root    6 Apr 11 04:59 adm
drwxr-xr-x.  5 root root   44 Sep 11 18:04 cache
drwxr-xr-x.  3 root root   34 Sep 11 18:04 db
drwxr-xr-x.  3 root root   18 Sep 11 18:04 empty
drwxr-xr-x.  2 root root    6 Apr 11 04:59 games
drwxr-xr-x.  2 root root    6 Apr 11 04:59 gopher
drwxr-xr-x.  3 root root   18 May  9 13:48 kerberos
drwxr-xr-x. 28 root root 4096 Oct  8 00:30 lib
drwxr-xr-x.  2 root root    6 Apr 11 04:59 local
lrwxrwxrwx.  1 root root   11 Sep 11 18:03 lock -> ../run/lock
drwxr-xr-x.  8 root root 4096 Oct  8 04:55 log
lrwxrwxrwx.  1 root root   10 Sep 11 18:03 mail -> spool/mail
drwxr-xr-x.  2 root root    6 Apr 11 04:59 nis
drwxr-xr-x.  2 root root    6 Apr 11 04:59 opt
drwxr-xr-x.  2 root root    6 Apr 11 04:59 preserve
lrwxrwxrwx.  1 root root    6 Sep 11 18:03 run -> ../run
drwxr-xr-x.  8 root root   87 Sep 11 18:04 spool
drwxrwxrwt.  4 root root  111 Oct  9 09:02 tmp
drwxr-xr-x.  2 root root    6 Apr 11 04:59 yp!
Today, the weather is good. Enjoy!

I'm a hacker, hu hu! Look, fool!
bin
boot
dev
etc
home
lib
lib64
media
mnt
opt
proc
root
run
sbin
srv
swapfile
sys
tmp
usr
var

如您所见,配置文件模板文件中都存在命令注入的可能性,因此您需要格外小心:
  • 确保模板文件的内容正确:检查是否存在命令注入。
  • 确保配置文件的内容正确:同样检查是否存在命令注入。如果配置文件来自他人,则在渲染模板之前,您需要知道并信任该人。

想象一下,您是一个没有密码限制的sudo用户,渲染模板文件可能会用一条巧妙的rm -rf命令毁坏您的系统。

只要您掌控这些文件的内容,使用eval模板就可以了。

如果您有一个外部(不受信任的)配置文件,则应该寻找一个模板引擎,以隔离此类注入。例如,Jinja2模板在Python中非常出名。


我不是那个给你点踩的人,但这看起来极其脆弱且容易出错。你绝对需要同时使用eval(仅出于安全和可维护性原因而被反对)和两个嵌套的带有命令替换的cat调用吗?如果你能在I think四级评估中保持头脑清醒,那就很好,但即使如此,你也应该解释一下这到底为你带来了什么,并警告其他人不要尝试这样做。 - tripleee
很好的解释,对于我的情况来说,这正是我所需要的,它帮了我很大的忙。非常感谢。 - VISHAL DAGA

2

我最终使用了可用的 envsubst 工具。它在大多数 Linux 系统中的 gettext 包中广泛可用。我使用这个工具是因为它会处理选择性替换变量,所以我不需要像使用 eval 一样在模板中转义它们。下面是一个例子。

$ cat dbtemplate.xml
<DB>
        <hostname>$DBHOST</hostname>
        <port>$DBPORT</port>
        <database>$DBNAME</database>
</DB>

$ export DBHOST=mydbhost1 DBPORT=1234 DBNAME=mydb; envsubst< dbtemplate.xml
<DB>
        <hostname>mydbhost1</hostname>
        <port>1234</port>
        <database>mydb</database>
</DB>

请参见https://dev59.com/7XE85IYBdhLWcg3wKwKD#11050943 - milahu

1
您可以使用Python类 string.Template
$ echo 'before $X after' > template.txt

$ python  -c 'import string; print(string.Template(open("template.txt").read()).substitute({"X":"A"}))'

before A after

或者

$  python  -c 'import string, sys; print(string.Template(open("template.txt").read()).substitute({"X":sys.argv[1]}))' "A"

在模板中,$X是一个占位符,{"X":"A"}是将占位符映射到值的映射。在Python代码中,我们从文件中读取模板文本,创建一个模板,然后用命令行参数替换占位符。
另外,如果您的计算机安装了Ruby,您也可以使用Ruby的ERB。
$ echo "before <%= ENV['X'] %> after" > template.txt

$ X=A erb template.txt

before A after

这里的<%= ENV['X'] %>是一个占位符。ENV['X']从环境变量中读取值。X=A将环境变量设置为所需的值。

1

使用 perl 的一行简洁解决方案

我使用 perl 将变量替换为它们的值:

export world=World beautiful=wonderful
echo 'I love you, $world! You are $beautiful.' >my_template.txt
perl -pe 's|\$([A-Za-z_]+)|$ENV{$1}|g' my_template.txt

输出:我爱你,世界!你很棒my_template.txt 可以包含以 $ 为前缀的变量。

0

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接