Python在命令行修改Properties配置文件

Python在命令行修改Properties配置文件

背景

我在平常的工作中,经常需要修改配置文件。配置文件的格式多种多样,有一种配置文件最为常见,就是每一行形如 name=value 格式,例如 Java Properties。在编写这个脚本以前,我常常用 sed 命令来做增删改查,不过我觉得这并不足够方便

于是我专门写了一个 Python 命令行工具,来批量增删改查上述格式的配置文件,使用时注意要给用户对被处理的配置文件授予必要权限,比如 rw

代码实现

TIPS 代码的最新版本在 GitHub Gist 中维护
https://gist.github.com/ChenyangGao/41e33d761c1e532d5e0b89502bd21414

文件名称为 config_props.py,实现如下:

config_props.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
#!/usr/bin/env python3
# coding: utf-8

assert __name__ == "__main__", "不能被引入模块"

__author__ = "ChenyangGao <https://chenyanggao.github.io/>"
__version__ = (0, 1, 1)

from argparse import ArgumentParser, RawTextHelpFormatter
from sys import argv


parser = ArgumentParser(
description="增删改查类如 Java Properties 格式的配置文件",
epilog="😆 友情提示:\n每项 property 都是单行的,如果要换行,"
"请在上一行的末尾加反斜杠(续行符)\\",
formatter_class=RawTextHelpFormatter,
)
subparsers = parser.add_subparsers(dest="command", help="Available commands")

parser_get = subparsers.add_parser(
"get", formatter_class=RawTextHelpFormatter,
help="读取配置:config_props.py get path name ...",
)
parser_get.add_argument("path", help="配置文件路径")
parser_get.add_argument(
"patterns", metavar="pattern", nargs="+", help="属性名或模式")
parser_get.add_argument(
"-t", "--type", choices=("raw", "wild", "re"), default="raw",
help="""属性名作为什么模式处理:
raw(默认): 视为普通字符串;
wild: 视为 unix 通配符模式;
re: 视为正则表达式模式。""",
)
parser_get.add_argument(
"-s", "--sign", default="=", help="属性和值的分界,必须是单个字符,默认为 =",
)
parser_get.add_argument(
"-e", "--encoding", help="配置文件的编码",
)


parser_set = subparsers.add_parser(
"set", formatter_class=RawTextHelpFormatter,
help="更新配置:config_props.py set path name value ...",
)
parser_set.add_argument("path", help="配置文件路径")
parser_set.add_argument(
"pairs", metavar="name value", nargs="+",
help="属性名和属性值的对偶,此参数必须传偶数个",
)
parser_set.add_argument(
"-s", "--sign", default="=", help="属性和值的分界,必须是单个字符,默认为 =",
)
parser_set.add_argument(
"-e", "--encoding", help="配置文件的编码",
)

parser_del = subparsers.add_parser(
"del", formatter_class=RawTextHelpFormatter,
help="删除配置:config_props.py del path name ...",
)
parser_del.add_argument("path", help="配置文件路径")
parser_del.add_argument("patterns", metavar="pattern", nargs="+", help="属性名或模式")
parser_del.add_argument(
"-t", "--type", choices=("raw", "wild", "re"), default="raw",
help="""属性名作为什么模式处理:
raw(默认): 视为普通字符串;
wild: 视为 unix 通配符模式;
re: 视为正则表达式模式。""",
)
parser_del.add_argument(
"-s", "--sign", default="=", help="属性和值的分界,必须是单个字符,默认为 =",
)
parser_del.add_argument(
"-e", "--encoding", help="配置文件的编码",
)

parser_uplines = subparsers.add_parser(
"uplines", formatter_class=RawTextHelpFormatter,
help="更新配置:config_props.py uplines path name=value ...",
)
parser_uplines.add_argument("path", help="配置文件路径")
parser_uplines.add_argument(
"lines", metavar="line", nargs="+",
help="""形如:
1. name: 删除名称是name的属性;
2. name=value: 更新名称是name的属性。""",
)
parser_uplines.add_argument(
"-s", "--sign", default="=", help="属性和值的分界,必须是单个字符,默认为 =",
)
parser_uplines.add_argument(
"-e", "--encoding", help="配置文件的编码",
)

# TODO: 添加命令 comment,增删改属性上面的注释:config_props.py comment path name=value ...
# TODO: 添加命令 merge,合并多个配置文件:config_props.py merge path path2 ...

if len(argv) < 2 or argv[1] not in ("get", "set", "del", "uplines"):
args = parser.parse_args(["-h"])
args = parser.parse_args()

sign = args.sign
assert len(sign) == 1, "属性名和值的分隔符 sign 必须是单字符,比如 ="


import re


CRE_PROPLINE = re.compile(fr"^[\t ]*(?!#)(?P<name>[^{sign}\s]+)[\t ]*{sign}[\t ]*(?P<value>.*)", re.M)


class ConfigProps:
"""配置文件管理器
:param path: 配置文件路径
:param encoding: 配置文件的编码,默认为 None,即根据系统给定
"""
def __init__(self, path, encoding=None):
self._path = path
self.encoding = encoding
self.read()

def __contains__(self, name):
return name in self._map

def __len__(self):
return len(self._map)

def __getitem__(self, name):
return self._map[name]

@property
def path(self):
"配置文件路径"
return self._path

def read(self):
"从配置文件中读取配置"
try:
self._lines = lines = open(self._path, "r", encoding=self.encoding).readlines()
except FileNotFoundError:
self._lines = []
self._map = {}
return
if lines and not lines[-1].endswith("\n"):
lines[-1] += "\n"

self._map = map_ = {}
for lineno, line in enumerate(lines):
match = CRE_PROPLINE.search(line)
if match is not None:
map_[match["name"]] = [match["value"], lineno]

def write(self):
"把最新的配置写入配置文件"
open(self._path, "w", encoding=self.encoding).writelines(self._lines)

def search(self, *pats, type="raw"):
"""生成器,从配置文件中筛选属性名符合任一模式的属性行

:param pats: 属性名的模式
:param type: 以何种模式处理:
raw(默认): 视为普通字符串;
wild: 视为 unix 通配符模式;
re: 视为正则表达式模式。

:return: 迭代器
"""
lines, map_ = self._lines, self._map
if not pats:
return
pats = frozenset(pats)
if type == "raw":
predicate = pats.__contains__
elif type == "wild":
from fnmatch import translate
predicate = re.compile("|".join(map(translate, pats))).fullmatch
elif type == "re":
predicate = re.compile("|".join(f"(?:{pat})" for pat in pats)).fullmatch
else:
raise ValueError(f"Unacceptable type value: {type!r}")
for name, (value, lineno) in map_.items():
if predicate(name):
yield name, value, lineno

def select(self, *pats, type="raw"):
lines = self._lines
for _, _, lineno in self.search(*pats, type=type):
print(lines[lineno].rstrip("\n"))

def delete(self, *pats, type="raw"):
lines, map_ = self._lines, self._map
meets = tuple(self.search(*pats, type=type))
t = tuple(map_.values())
for n, (name, value, lineno) in enumerate(meets):
line = lines[lineno-n]
# 删除 1 行后,这行后面的每 1 行在列表中的索引会 -1
for e in t[lineno+1:]:
e[1] -= 1
del map_[name]
del lines[lineno-n]
print("Deleted:", name, "\n <=", line.rstrip("\n"))
return len(meets)

def insert(self, name, value, line=None):
lines, map_ = self._lines, self._map
if line is None:
line = f"{name}={value}\n"
lines.append(line)
map_[name] = (value, len(lines))
print("Inserted:", name, "\n =>", line.rstrip("\n"), "")

def update(self, name, value, line=None):
lines, map_ = self._lines, self._map
if line is None:
line = f"{name}={value}\n"
lineno = map_[name][1]
map_[name] = value, lineno
line_old = lines[lineno]
lines[lineno] = line
print("Updated:", line_old.rstrip("\n"), "\n =>", line.rstrip("\n"))

def upsert(self, name, value, line=None):
map_ = self._map
if name in map_:
self.update(name, value, line)
else:
self.insert(name, value, line)

def update_lines(self, *lines):
n = 0
for line in lines:
match = CRE_PROPLINE.search(line)
if match is None:
name = line
if name in conf_props:
self.delete(name)
else:
print("😂 ignored:", name)
continue
else:
line += "\n"
name, value = match["name"], match["value"]
self.upsert(name, value, line)
n += 1
return n


command = args.command
path = args.path
encoding = args.encoding

conf_props = ConfigProps(path)

if command == "get":
patterns = args.patterns
type = args.type
conf_props.select(*patterns, type=type)
elif command == "set":
pairs = args.pairs
names = pairs[::2]
values = pairs[1::2]
for name, value in zip(names, values):
conf_props.upsert(name, value)
if names and values:
conf_props.write()
elif command == "del":
patterns = args.patterns
type = args.type
if conf_props.delete(*patterns, type=type):
conf_props.write()
elif command == "uplines":
lines = args.lines
if conf_props.update_lines(*lines):
conf_props.write()
else:
raise NotImplementedError

使用说明

命令帮助信息如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ python config_props.py -h
usage: config_props.py [-h] {get,set,del,uplines} ...

增删改查类如 Java Properties 格式的配置文件

positional arguments:
{get,set,del,uplines}
Available commands
get 读取配置:config_props.py get path name ...
set 更新配置:config_props.py set path name value ...
del 删除配置:config_props.py del path name ...
uplines 更新配置:config_props.py uplines path name=value ...

optional arguments:
-h, --help show this help message and exit

😆 友情提示:
每项 property 都是单行的,如果要换行,请在上一行的末尾加反斜杠(续行符)\

现在假设待处理的配置文件 example.properties 的内容如下:

example.properties
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
## 以下是数据库连接信息

# 数据库系统
db.dialet = mysql
# 主机
db.host = localhost
# 端口
db.port = 3306
# 用户名
db.username = root
# 密码
db.password =
# 数据库
db.database =

# 下面是 foo 业务要用到的表
foo.table = relation

目前实现了 4 个子命令,示例如下:

1. get 命令

命令帮助信息如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ python config_props.py get -h
usage: config_props.py get [-h] [-t {raw,wild,re}] [-s SIGN]
path pattern [pattern ...]

positional arguments:
path 配置文件路径
pattern 属性名或模式

optional arguments:
-h, --help show this help message and exit
-t {raw,wild,re}, --type {raw,wild,re}
属性名作为什么模式处理:
raw(默认): 视为普通字符串;
wild: 视为 unix 通配符模式;
re: 视为正则表达式模式。
-s SIGN, --sign SIGN 属性和值的分界,必须是单个字符,默认为 =
-e ENCODING, --encoding ENCODING
配置文件的编码

使用名称搜索:

1
2
3
$ python config_props.py get example.properties db.dialet foo.table non.exists.name
db.dialet = mysql
foo.table = relation

使用 unix 路径通配符模式搜索:

1
2
3
4
5
6
7
$ python config_props.py get example.properties 'db.*' -t wild
db.dialet = mysql
db.host = localhost
db.port = 3306
db.username = root
db.password =
db.database =

使用正则表达式模式搜索:

1
2
3
4
5
6
7
$ python config_props.py get example.properties 'db\..*' -t re
db.dialet = mysql
db.host = localhost
db.port = 3306
db.username = root
db.password =
db.database =

2. set 命令

命令帮助信息如下:

1
2
3
4
5
6
7
8
9
10
11
12
$ python config_props.py set -h
usage: config_props.py set [-h] [-s SIGN] path name value [name value ...]

positional arguments:
path 配置文件路径
name value 属性名和属性值的对偶,此参数必须传偶数个

optional arguments:
-h, --help show this help message and exit
-s SIGN, --sign SIGN 属性和值的分界,必须是单个字符,默认为 =
-e ENCODING, --encoding ENCODING
配置文件的编码

执行如下命令:

1
2
3
4
5
6
7
$ python config_props.py set example.properties db.password 123456 db.database test foo.bar 😂🤣😆😅
Updated: db.password =
=> db.password=123456
Updated: db.database =
=> db.database=test
Inserted: foo.bar
=> foo.bar=😂🤣😆😅

配置文件 example.properties 被更新为:

example.properties
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
## 以下是数据库连接信息

# 数据库系统
db.dialet = mysql
# 主机
db.host = localhost
# 端口
db.port = 3306
# 用户名
db.username = root
# 密码
db.password=123456
# 数据库
db.database=test

# 下面是 foo 业务要用到的表
foo.table = relation
foo.bar=😂🤣😆😅

3. del 命令

命令帮助信息如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ python config_props.py del -h
usage: config_props.py del [-h] [-t {raw,wild,re}] [-s SIGN]
path pattern [pattern ...]

positional arguments:
path 配置文件路径
pattern 属性名或模式

optional arguments:
-h, --help show this help message and exit
-t {raw,wild,re}, --type {raw,wild,re}
属性名作为什么模式处理:
raw(默认): 视为普通字符串;
wild: 视为 unix 通配符模式;
re: 视为正则表达式模式。
-s SIGN, --sign SIGN 属性和值的分界,必须是单个字符,默认为 =
-e ENCODING, --encoding ENCODING
配置文件的编码

执行如下命令:

1
2
3
$ python config_props.py del example.properties foo.bar foo.baz
Deleted: foo.bar
<= foo.bar=😂🤣😆😅

配置文件 example.properties 被更新为:

example.properties
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
## 以下是数据库连接信息

# 数据库系统
db.dialet = mysql
# 主机
db.host = localhost
# 端口
db.port = 3306
# 用户名
db.username = root
# 密码
db.password=123456
# 数据库
db.database=test

# 下面是 foo 业务要用到的表
foo.table = relation

4. uplines 命令

命令帮助信息如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ python config_props.py uplines -h
usage: config_props.py uplines [-h] [-s SIGN] path line [line ...]

positional arguments:
path 配置文件路径
line 形如:
1. name: 删除名称是name的属性;
2. name=value: 更新名称是name的属性。

optional arguments:
-h, --help show this help message and exit
-s SIGN, --sign SIGN 属性和值的分界,必须是单个字符,默认为 =
-e ENCODING, --encoding ENCODING
配置文件的编码

执行如下命令:

1
2
3
4
5
6
7
8
$ python config_props.py uplines example.properties db.username=test db.password=randomXYZ foo.table foo.bar
Updated: db.username = root
=> db.username=test
Updated: db.password=123456
=> db.password=randomXYZ
Deleted: foo.table
<= foo.table = relation
😂 ignored: foo.bar

配置文件 example.properties 被更新为:

example.properties
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
## 以下是数据库连接信息

# 数据库系统
db.dialet = mysql
# 主机
db.host = localhost
# 端口
db.port = 3306
# 用户名
db.username=test
# 密码
db.password=randomXYZ
# 数据库
db.database=test

# 下面是 foo 业务要用到的表

未来规划

其实我早就完成了,增删改查其他格式配置文件的命令行脚本,诸如:

但实现得都比较复杂,并且还用到了一些第三方库,而且在我看来还不够完善。我打算为它们设计并实现一个统一的操作方案,提供类似 XQuery
SQL
的增删改查,然后再分享出来,敬请期待后续文章。

评论

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×