cli.py 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  1. # -*- coding:utf-8 -*-
  2. """
  3. Configuration loader for DDNS command-line interface.
  4. @author: NewFuture
  5. """
  6. from argparse import Action, ArgumentParser, RawTextHelpFormatter, SUPPRESS
  7. from logging import getLevelName
  8. from os import path as os_path
  9. import platform
  10. import sys
  11. from .file import save_config
  12. __all__ = ["load_config", "str_bool"]
  13. def str_bool(v):
  14. # type: (str | bool | None | int | float | list) -> bool | str
  15. """
  16. parse string to boolean
  17. """
  18. if isinstance(v, bool):
  19. return v
  20. if v is None:
  21. return False
  22. if not isinstance(v, str) and not type(v).__name__ == "unicode":
  23. # For non-string types, convert to string first
  24. return bool(v)
  25. if v.lower() in ("yes", "true", "t", "y", "1"):
  26. return True
  27. elif v.lower() in ("no", "false", "f", "n", "0"):
  28. return False
  29. else:
  30. return v
  31. def log_level(value):
  32. """
  33. parse string to log level
  34. or getattr(logging, value.upper())
  35. """
  36. return getLevelName(value if isinstance(value, int) else value.upper())
  37. def _get_system_info_str():
  38. system = platform.system()
  39. release = platform.release()
  40. machine = platform.machine()
  41. arch = platform.architecture()
  42. return "{}-{} {} {}".format(system, release, machine, arch)
  43. def _get_python_info_str():
  44. version = platform.python_version()
  45. branch, py_build_date = platform.python_build()
  46. return "Python-{} {} ({})".format(version, branch, py_build_date)
  47. class ExtendAction(Action):
  48. """
  49. 兼容 Python <3.8 的 extend action
  50. """
  51. def __call__(self, parser, namespace, values, option_string=None):
  52. items = getattr(namespace, self.dest, None)
  53. if items is None:
  54. items = []
  55. # values 可能是单个值或列表
  56. if isinstance(values, list):
  57. items.extend(values)
  58. else:
  59. items.append(values)
  60. setattr(namespace, self.dest, items)
  61. class NewConfigAction(Action):
  62. """生成配置文件并退出程序"""
  63. def __call__(self, parser, namespace, values, option_string=None):
  64. # 获取配置文件路径
  65. if values and values != "true":
  66. config_path = str(values) # type: str
  67. else:
  68. config_path = getattr(namespace, "config", "config.json") # type: str
  69. if os_path.exists(config_path):
  70. sys.stderr.write("The default %s already exists!\n" % config_path)
  71. sys.stdout.write("Please use `--new-config=%s` to specify a new config file.\n" % config_path)
  72. sys.exit(1)
  73. # 获取当前已解析的参数
  74. current_config = {k: v for k, v in vars(namespace).items() if v is not None}
  75. # 保存配置文件
  76. save_config(config_path, current_config)
  77. sys.stdout.write("%s is generated.\n" % config_path)
  78. sys.exit(0)
  79. def load_config(description, doc, version, date):
  80. # type: (str, str, str, str) -> dict
  81. """
  82. 解析命令行参数并返回配置字典。
  83. Args:
  84. description (str): 程序描述
  85. doc (str): 程序文档
  86. version (str): 程序版本
  87. date (str): 构建日期
  88. Returns:
  89. dict: 配置字典
  90. """
  91. parser = ArgumentParser(description=description, epilog=doc, formatter_class=RawTextHelpFormatter)
  92. sysinfo = _get_system_info_str()
  93. pyinfo = _get_python_info_str()
  94. version_str = "v{} ({})\n{}\n{}".format(version, date, pyinfo, sysinfo)
  95. log_levels = [
  96. "CRITICAL", # 50
  97. "ERROR", # 40
  98. "WARNING", # 30
  99. "INFO", # 20
  100. "DEBUG", # 10
  101. "NOTSET", # 0
  102. ]
  103. parser.add_argument("-v", "--version", action="version", version=version_str)
  104. parser.add_argument(
  105. "-c",
  106. "--config",
  107. nargs="*",
  108. action=ExtendAction,
  109. metavar="FILE",
  110. help="load config file [配置文件路径, 可多次指定]",
  111. )
  112. parser.add_argument("--debug", action="store_true", help="debug mode [开启调试模式]")
  113. parser.add_argument(
  114. "--new-config", metavar="FILE", action=NewConfigAction, nargs="?", help="generate new config [生成配置文件]"
  115. )
  116. # 参数定义
  117. parser.add_argument(
  118. "--dns",
  119. help="DNS provider [DNS服务提供商]",
  120. choices=[
  121. "51dns",
  122. "alidns",
  123. "aliesa",
  124. "callback",
  125. "cloudflare",
  126. "debug",
  127. "dnscom",
  128. "dnspod_com",
  129. "dnspod",
  130. "edgeone",
  131. "he",
  132. "huaweidns",
  133. "namesilo",
  134. "noip",
  135. "tencentcloud",
  136. ],
  137. )
  138. parser.add_argument("--id", help="API ID or email [对应账号ID或邮箱]")
  139. parser.add_argument("--token", help="API token or key [授权凭证或密钥]")
  140. parser.add_argument("--endpoint", help="API endpoint URL [API端点URL]")
  141. parser.add_argument(
  142. "--index4",
  143. nargs="*",
  144. action=ExtendAction,
  145. metavar="RULE",
  146. help="IPv4 rules [获取IPv4方式, 多次可配置多规则]",
  147. )
  148. parser.add_argument(
  149. "--index6",
  150. nargs="*",
  151. action=ExtendAction,
  152. metavar="RULE",
  153. help="IPv6 rules [获取IPv6方式, 多次可配置多规则]",
  154. )
  155. parser.add_argument(
  156. "--ipv4",
  157. nargs="*",
  158. action=ExtendAction,
  159. metavar="DOMAIN",
  160. help="IPv4 domains [IPv4域名列表, 可配置多个域名]",
  161. )
  162. parser.add_argument(
  163. "--ipv6",
  164. nargs="*",
  165. action=ExtendAction,
  166. metavar="DOMAIN",
  167. help="IPv6 domains [IPv6域名列表, 可配置多个域名]",
  168. )
  169. parser.add_argument("--ttl", type=int, help="DNS TTL(s) [设置域名解析过期时间]")
  170. parser.add_argument("--line", help="DNS line/route [DNS线路设置,如电信、联通、移动等]")
  171. parser.add_argument(
  172. "--proxy",
  173. nargs="*",
  174. action=ExtendAction,
  175. help="HTTP proxy [设置http代理,可配多个代理连接]",
  176. )
  177. parser.add_argument(
  178. "--cache",
  179. type=str_bool,
  180. nargs="?",
  181. const=True,
  182. help="set cache [启用缓存开关,或传入保存路径]",
  183. )
  184. parser.add_argument(
  185. "--no-cache",
  186. dest="cache",
  187. action="store_const",
  188. const=False,
  189. help="disable cache [关闭缓存等效 --cache=false]",
  190. )
  191. parser.add_argument(
  192. "--ssl",
  193. type=str_bool,
  194. nargs="?",
  195. const=True,
  196. help="SSL certificate verification [SSL证书验证方式]: "
  197. "true(强制验证), false(禁用验证), auto(自动降级), /path/to/cert.pem(自定义证书)",
  198. )
  199. parser.add_argument(
  200. "--no-ssl",
  201. dest="ssl",
  202. action="store_const",
  203. const=False,
  204. help="disable SSL verify [禁用验证, 等效 --ssl=false]",
  205. )
  206. parser.add_argument("--log_file", metavar="FILE", help="log file [日志文件,默认标准输出]")
  207. parser.add_argument("--log.file", "--log-file", dest="log_file", help=SUPPRESS) # 隐藏参数
  208. parser.add_argument("--log_level", type=log_level, metavar="|".join(log_levels), help=None)
  209. parser.add_argument("--log.level", "--log-level", dest="log_level", type=log_level, help=SUPPRESS) # 隐藏参数
  210. parser.add_argument("--log_format", metavar="FORMAT", help="set log format [日志格式]")
  211. parser.add_argument("--log.format", "--log-format", dest="log_format", help=SUPPRESS) # 隐藏参数
  212. parser.add_argument("--log_datefmt", metavar="FORMAT", help="set log date format [日志时间格式]")
  213. parser.add_argument("--log.datefmt", "--log-datefmt", dest="log_datefmt", help=SUPPRESS) # 隐藏参数
  214. args = parser.parse_args()
  215. is_debug = getattr(args, "debug", False)
  216. if is_debug:
  217. # 如果启用调试模式,则强制设置日志级别为 DEBUG
  218. args.log_level = log_level("DEBUG")
  219. if args.cache is None:
  220. args.cache = False # 禁用缓存
  221. # 将 Namespace 对象转换为字典并直接返回
  222. config = vars(args)
  223. return {k: v for k, v in config.items() if v is not None} # 过滤掉 None 值的配置项