通常在项目中会使用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
就可以解析使用了。
回到主题,既然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]
的配置依然存在不变。
分几种变更场景
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实现的核心是使用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的方式会更好。