Skip to content

Instantly share code, notes, and snippets.

@ChenyangGao
Created August 27, 2022 13:12
Show Gist options
  • Save ChenyangGao/41e33d761c1e532d5e0b89502bd21414 to your computer and use it in GitHub Desktop.
Save ChenyangGao/41e33d761c1e532d5e0b89502bd21414 to your computer and use it in GitHub Desktop.
Python在命令行修改Properties配置文件
#!/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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment