shell实现config配置文件合并变更配置项

前言

通常在项目中会使用config文件作为项目的配置文件,config文件一般由[section]name=value组成。当然分隔符=或者:是可以根据自己来定的。
文件的格式通常如下:

[DEFAULT]
service_phone=18888888888
# 资源路径
resource_dir=/xxx/xxx/xxx/
# 服务端口
server_port=xxx

#WEB配置
[HTTP-SERVER]
host=0.0.0.0
http_port=xxx

对于该格式文件的解析,Python3有专门的库处理:configparser,通过导入import configparser就可以解析使用了。

为什么采用增量配置conf文件?

回到主题,既然config文件是用来保存项目配置的,那么什么时候会合并A配置文件到B配置文件呢?比如:发布项目的时候,项目release_1.0.0的版本已经发布,项目上的配置文件已经存在,在发布release_2.0.0的版本的时候,为了防止原配置文件的配置会被覆盖,导致原配置丢失,所以使用增量配置的方式来更改配置文件。
比如:
release_1.0.0版本的文件app.conf文件如下:

[SYS_CONFIG]
# 服务端口
server_port=8888
# 资源路径
resource_dir=/home/zhangsan/resource/

[USER_INFO]
# 姓名
name=张三
# 电话
phone=18888888888
# 住址
address=马栏山马栏坡马栏屯123号

然后该项目需要发布release_2.0.0版本,并且配置文件如下:

[SYS_CONFIG]
# 服务端口
server_port=8080
# 资源路径
resource_dir=/home/zhangsan/resource2/

由于发布版本时,开发人员可能只是想更改[SYS_CONFIG]部分的配置,但是不小心把[USER_INFO]部分的配置删除了,导致发布2.0版本之后线上配置文件被直接覆盖删除,导致出现问题。为了让每次发布只需要关心配置文件需要更改的部分,而不关心未更改的配置,解决配置文件轻易被覆盖删除的问题,那么采用增量配置的方式更加的稳妥。
比如接着上面的例子,发布2.0版本的配置文件只会关心发布更改的配置文件项,由于只列出了[SYS_CONFIG]的配置,所以只会更改线上原配置文件中的[SYS_CONFIG]中的配置,原配置文件的[USER_INFO]的配置依然存在不变。

增量变更配置的几种类型

分几种变更场景

新增[section]

A.conf

[SECTION_A]
opentionA_1=valueA
opentionA_2=valueA

B.conf

[SECTION_B]
opentionB_1=valueB
opentionB_2=valueB

合并B.conf到A.conf之后的内容:

[SECTION_A]
opentionA_1=valueA
opentionA_2=valueA
[SECTION_B]
opentionB_1=valueB
opentionB_2=valueB

修改配置项

A.conf

[SECTION_A]
opentionA_1=valueA
opentionA_2=valueA

B.conf

[SECTION_A]
opentionA_1=被修改了

合并B.conf到A.conf之后的内容:

[SECTION_A]
opentionA_1=被修改了
opentionA_2=valueA

删除配置项

删除配置项为了稳妥起见,采用将value值去除变为空值的方式,起到删除的作用。
A.conf

[SECTION_A]
opentionA_1=valueA
opentionA_2=valueA

B.conf

[SECTION_A]
opentionA_1=

合并B.conf到A.conf之后的内容:

[SECTION_A]
opentionA_1=
opentionA_2=valueA

新增配置项

A.conf

[SECTION_A]
opentionA_1=valueA
opentionA_2=valueA

B.conf

[SECTION_A]
opentionA_3=新增配置项3

合并B.conf到A.conf之后的内容:

[SECTION_A]
opentionA_1=valueA
opentionA_2=valueA
opentionA_3=新增配置项3

混合变更配置

A.conf

[SECTION_A]
opentionA_1=valueA
opentionA_2=valueA

B.conf

[SECTION_A]
opentionA_3=新增配置项3
opentionA_1=被修改了
opentionA_2=

[SECTION_B]
opentionB_1=valueB
opentionB_2=valueB

合并B.conf到A.conf之后的内容:

[SECTION_A]
opentionA_1=被修改了
opentionA_2=
opentionA_3=新增配置项3

[SECTION_B]
opentionB_1=valueB
opentionB_2=valueB

shell实现config配置文件的增量变更

shell实现的核心是使用awk命令读取分析A文件和B文件的内容,并确定B文件的每个配置项分别变更了A文件的哪一行配置或者在哪一行后面新增配置。然后将变更信息生成sed脚本,最后直接用sed命令替换配置内容。最后的sed命令采用流式编辑文件,因此非常的高效。

mconf.sh

#!/bin/bash

fcf_path=$1    # 配置文件路径
to_mcf_path=$2 # 合并目标文件
sed_script=$3  # sed脚本

#awk 分别读取两个配置文件
#比较文件,找出每项配置合并到mcf的增删改并写入到sed文件
#使用sed将mcf文件变更

# 遇到section插入

if [ ! -f "$fcf_path" ]; then
  echo "配置文件不存在:${fcf_path}"
  exit 1
fi
if [ ! -f "$to_mcf_path" ]; then
  echo "目标配置文件不存在:${to_mcf_path}"
  exit 1
fi

# linux环境中去除Windows中的\r符号
sed -i 's/\r//g' "$fcf_path"
sed -i 's/\r//g' "$to_mcf_path"

cat /dev/null >"$sed_script" # 清空sed脚本文件

function cf_append() {
  # 附加插入  格式: 行号a\换行 附加内容
  local row_num=$1 #行号
  local content=$2 #附加内容
  {
    echo "${row_num}a\\"
    echo "${content}"
    echo "" #还需要一个空行,否则附加内容之后的内容会在一行
  } >>"$sed_script"
}

function cf_change() {
  # 更新配置   格式: 行号s/模式匹配/替换内容/g  模式匹配最好采用整行匹配xx=.*
  local row_num=$1 #行号
  local pattern=$2 #匹配字符串
  local content=$3 #替换内容
  if [[ $content == */* ]]; then
    {
      echo "${row_num}s!${pattern}!${content}!g" >>"$sed_script"
    }
  else
    {
      echo "${row_num}s/${pattern}/${content}/g" >>"$sed_script"
    }
  fi

}

function act_sed() {
  # 触发sed操作
  if [ ! -s "$to_mcf_path" ]; then
    echo -n "#head" >>"$to_mcf_path" # 如果文件为空,则需要添加一个头部内容才能用sed命令,否则sed命令无效
  fi
  sed -i -f "$sed_script" "$to_mcf_path"
}

function match_tconf() {
  # 匹配目标项的配置,如果有多个相同配置则去最后的配置
  local key=$1
  local IFS_OLD=$IFS
  match_rows=$(awk -F= '{if ($1 == key) print NR,$1,$2}' key="$key" "$to_mcf_path")
  if [ -z "$match_rows" ]; then #如果没有匹配项
    echo ""
    return
  fi
  IFS=$'\n'
  amatch_rows=($match_rows)
  local last_row=${amatch_rows[((${#amatch_rows[*]})) - 1]} #获取最后一行
  echo "$last_row"
  IFS=$IFS_OLD
}

function compare_conf() {
  #比较配置文件,并生成sed脚本
  f_rows=$(awk -F= '{print $0}' "$fcf_path")
  IFS_OLD=$IFS
  IFS=$'\n'

  last_section_num=0 #最近一次的section行号,$为最后一行。因为流式处理配置文件,所以所有项一定是按序处理
  for frow in $f_rows; do
    if [ -z "$frow" ]; then
      continue
    fi
    IFS=$'='
    fcolumns=($frow)
    local key=${fcolumns[0]}
    local fvalue=${fcolumns[1]}

    match_row=$(match_tconf "$key") #匹配目标配置文件中的内容
    IFS=$' '
    amatch_column=($match_row) #行号 key value
    if ((${#amatch_column[*]} == 2)); then
      amatch_column=($amatch_column "")
    fi
    if [[ $key == [* ]]; then #如果是section
      if [ -z "$match_row" ]; then #如果section未在目标配置中找到,则表示为新增section
        cf_append '$' ""
        cf_append '$' "$key"
        last_section_num='$'
      else
        local lnum=${amatch_column[0]} #section处在目标配置中的行号
        last_section_num=$lnum
      fi
      continue #继续循环
    fi

    if [ -z "$match_row" ]; then # 如果没有找到匹配项
      # 新增
      if [[ $frow != *=* ]]; then #如果非配置项
        if [[ $frow != [* ]]; then
          continue
        fi
        cf_append "$last_section_num" "$key"
      else
        cf_append "$last_section_num" "$key=$fvalue"
      fi
    elif [ "${amatch_column[((${#amatch_column[*]})) - 1]}" != "$fvalue" ]; then
      # 修改
      local lnum=${amatch_column[0]} #目标文件匹配的行号
      cf_change "$lnum" "$key=.*" "$key=$fvalue"
    fi
  done
  IFS=$IFS_OLD
}

compare_conf
act_sed

脚本执行:
./mconf.sh ./b.conf ./a.conf ./m.sed
最终会生成会将b.conf文件合并到a.conf文件,并且生成m.sed文件,m.sed文件会作为sed命令的-f参数传入并执行。

注意如果你是macOS,那么sed命令后面需要带上-i ‘’ ,因为mac系统强制需要你传入备份文件名。

脚本中读取配置文件采用的分隔符分隔的方式,可以使用while-read的方式会更好。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章