之前写的一个脚手架为了方便 Docker 部署写了一个 Dockerfile 和 build.sh 的脚本,大概长这样:
            
            
              Dockerfile
              
              
            
          
          FROM openjdk:17
# 设置容器名称和重启策略
ARG CONTAINER_NAME=quick-spring
ARG RESTART="always"
# 暴露端口
EXPOSE 8888
# 挂载本地文件系统到容器中
ADD ./core-0.0.1.jar app.jar
# 设置环境变量
ENV TZ=Asia/Shanghai
# 启动命令
CMD java -jar app.jar
        
            
            
              shell
              
              
            
          
          docker rm -f quick-spring
docker build . -t quick-spring
docker run -d -p 8888:8888 --name quick-spring quick-spring
        每次需要部署的时候只需要把 Dockefile 和 build.sh 以及 jar 包上传到服务就可以运行 build.sh 一键部署了。
但是每当我用这个脚手架新开一个项目的时候,总是需要重新替换一下 Dockerfile 和 build.sh 中的名字,虽然工作量不大,但是改多了也会烦,而且有时候甚至会忘记修改。
程序员都是爱偷懒的,我也不例外。
于是我便想着把这个操作通用化,写一个 shell 脚本完成所有的步骤,实现真正的一键部署。
期间用到了许多实用的知识点,于是便写了这篇文章记录一下。
Sed命令
首先想到的是通过 pom.xml 来获取项目名以及版本号:
            
            
              ini
              
              
            
          
          project_name = $(cat pom.xml |  sed -n 's/.*<artifactId>\(.*\)<\/artifactId>.*$/\1/p')
        先来介绍一下 sed 命令
sed 是一个流式文本编辑器,用于在文本流中进行替换、删除、插入等操作,可以很方便地和正则表达式配合使用。
它的格式是:
            
            
              arduino
              
              
            
          
          sed OPTIONS 'COMMAND' FILE
        一个最基本的例子是:
            
            
              arduino
              
              
            
          
          sed 's/hello/world/' file
        其中 s 表示替换命令,即本次操作进行的是替换操作,将 hello 替换为 world,然后打印出来,如:

但是默认情况下只会替换每一行的第一个匹配项:

需要加上 g 命令才能替换所有匹配项:

如果我只想要打印发生替换的行,那么就加上 p 命令(打印模板块的行)和 -n 选项(仅显示处理后的结果)即:

回到上面的问题:
            
            
              arduino
              
              
            
          
          sed -n 's/.*<artifactId>\(.*\)<\/artifactId>.*$/\1/p'
        现在来看的话就清晰多了,首先,这里的 -n 和 p 是为了确保只输出替换后的行。
至于中中间的字符串其实应该这么看:
            
            
              arduino
              
              
            
          
          's/   .*<artifactId>\(.*\)<\/artifactId>.*$/   \1   /p'
        .*<artifactId>\(.*\)<\/artifactId>.*$/ 这个表示的就是要被替换的文本的正则表达式,而 \1 则表示第一个捕获组,也就是这里的 \(.*\) 。
值得注意的是这里捕获组的括号用到了转义字符,这是因为 sed 命令默认使用的是最原始的正则表达式,捕获组的括号必须是 \(\) 而不是 () 。
而这里的 \1 则表示第一个捕获组,也就是 \(.*\) 捕获到的这个 .* 内容,也就是如果我们匹配到的项目名是 quick-spring ,那么这里的 \1 则是 quick-spring 。
所以可想而知这里最后输出的其实就是尖括号中间的字符串。
但是上面的写法会有一个问题:

不难看出所有的 artifactId 都被匹配了。
于是便用了一个取巧的方法,只读取前10行:
            
            
              bash
              
              
            
          
          head -n 10 pom.xml |  sed -n 's/.*<artifactId>\(.*\)<\/artifactId>.*$/\1/p'
        虽然不适用所有情况(比如 artifactId 写到后面去了),但是在这个脚手架里面还是可以解决问题的(而且一般 artifactId 都是写在前十行):

然后用占位符来替换掉原来的硬编码:
            
            
              ini
              
              
            
          
          FROM openjdk:17
# 设置容器名称和重启策略
ARG CONTAINER_NAME={{projectName}}
ARG RESTART="always"
# 暴露端口
EXPOSE 8888
# 挂载本地文件系统到容器中
ADD ./core-{{version}}.jar app.jar
# 设置环境变量
ENV TZ=Asia/Shanghai
# 启动命令
CMD java -jar app.jar
        接着只需要通过 sed 命令进行正则匹配并替换文本即可:
            
            
              bash
              
              
            
          
          sed -e "s/{{projectName}}/${project_name}/" -e "s/{{version}}/${version}/" Dockerfile.template | cat > Dockerfile
        注意到这个 -e 选项可以组合多个命令,将多个命令作用于同一个文本。
接下来就是构建的操作了:
            
            
              bash
              
              
            
          
          docker rm -f "${project_name}"
docker build . -t "${project_name}"
docker run -d -p 8888:8888 --name "${project_name} ${project_name}"
        test命令
值得注意的是,为了防止多次重复生成 Dockerfile,脚本还对文本是否存在进行了检测,这里用到了 test 命令 -f 和 !:
            
            
              bash
              
              
            
          
          if [ ! -f Dockerfile ]; then
  echo "生成Dockerfile"
  sed -e "s/{{projectName}}/${project_name}/" -e "s/{{version}}/${version}/" Dockerfile.template | cat > Dockerfile
fi
        test 命令是用于测试文件或条件的工具,通常用于 shell 脚本中的条件判断。在 Unix/Linux 系统中,test 命令通常由 [ ] 表示,它们可以用于测试各种条件,并返回一个布尔值作为结果。
以下是常见的 test 命令及其功能:
- 
文件测试:
e FILE:检查文件是否存在。f FILE:检查文件是否存在且为普通文件。d FILE:检查文件是否存在且为目录。r FILE:检查文件是否存在且可读。w FILE:检查文件是否存在且可写。x FILE:检查文件是否存在且可执行。
 - 
字符串测试:
z STRING:检查字符串是否为空。n STRING:检查字符串是否非空。STRING1 = STRING2:检查两个字符串是否相等。STRING1 != STRING2:检查两个字符串是否不相等。
 - 
整数比较:
INTEGER1 -eq INTEGER2:检查两个整数是否相等。INTEGER1 -ne INTEGER2:检查两个整数是否不相等。INTEGER1 -gt INTEGER2:检查 INTEGER1 是否大于 INTEGER2。INTEGER1 -lt INTEGER2:检查 INTEGER1 是否小于 INTEGER2。INTEGER1 -ge INTEGER2:检查 INTEGER1 是否大于或等于 INTEGER2。INTEGER1 -le INTEGER2:检查 INTEGER1 是否小于或等于 INTEGER2。
 - 
逻辑测试:
! EXPRESSION:返回 EXPRESSION 的逻辑否定值。EXPRESSION1 -a EXPRESSION2:返回 AND 逻辑运算结果。EXPRESSION1 -o EXPRESSION2:返回 OR 逻辑运算结果
 
代码
            
            
              shell
              
              
            
          
          echo "解析pom.xml"
pom_xml_head10=$(head -n 10 pom.xml)
project_name=$(echo "$pom_xml_head10" | sed -n 's/.*<artifactId>(.*)</artifactId>.*$/\1/p')
version=$(echo "$pom_xml_head10" | sed -n 's/.*<version>(.*)</version>.*$/\1/p')
echo "项目名:$project_name"
echo "版本号:$version"
if [ ! -f Dockerfile ]; then
  echo "生成Dockerfile"
  sed -e "s/{{projectName}}/${project_name}/" -e "s/{{version}}/${version}/" Dockerfile.template | cat > Dockerfile
fi
echo "删除原有容器"
docker rm -f "${project_name}"
echo "构建镜像"
docker build . -t "${project_name}"
echo "启动容器"
docker run -d -p 8888:8888 --name "${project_name} ${project_name}"