Created
August 27, 2022 13:12
-
-
Save ChenyangGao/41e33d761c1e532d5e0b89502bd21414 to your computer and use it in GitHub Desktop.
Python在命令行修改Properties配置文件
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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