cli_args.py 3.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. CLI 参数兼容工具。
  5. 背景:
  6. - data_modules 下的 CLI 普遍使用 argparse + subparsers。
  7. - argparse 的全局参数(例如 --project-root)要求出现在子命令之前:
  8. python -m data_modules.index_manager --project-root X get-core-entities
  9. 但实际写作流程里(skills/agents 文档、工具调用)经常把 --project-root 放在子命令之后:
  10. python -m data_modules.index_manager get-core-entities --project-root X
  11. 这会直接报 "unrecognized arguments"(见 issues7 日志)。
  12. 这里提供一个轻量的 argv 预处理:把 --project-root 从任意位置提取出来并前置,
  13. 让原有 argparse 定义无需大改即可兼容两种写法。
  14. """
  15. from __future__ import annotations
  16. import json
  17. import sys
  18. from pathlib import Path
  19. from typing import Any
  20. from typing import List, Optional, Tuple
  21. def _extract_flag_value(argv: List[str], flag: str) -> Tuple[Optional[str], List[str]]:
  22. """
  23. Extract a flag value from argv.
  24. Supports:
  25. - --flag VALUE
  26. - --flag=VALUE
  27. Returns:
  28. - (value, remaining_argv)
  29. - value uses the *last* occurrence when repeated.
  30. - if a dangling `--flag` has no value, it is kept in remaining_argv for argparse to raise.
  31. """
  32. value: Optional[str] = None
  33. rest: List[str] = []
  34. i = 0
  35. while i < len(argv):
  36. token = argv[i]
  37. if token == flag:
  38. if i + 1 < len(argv):
  39. value = argv[i + 1]
  40. i += 2
  41. continue
  42. # Dangling flag; keep it so argparse can error out properly.
  43. rest.append(token)
  44. i += 1
  45. continue
  46. if token.startswith(flag + "="):
  47. value = token.split("=", 1)[1]
  48. i += 1
  49. continue
  50. rest.append(token)
  51. i += 1
  52. return value, rest
  53. def normalize_global_project_root(argv: List[str], *, flag: str = "--project-root") -> List[str]:
  54. """
  55. Normalize argv so a global `--project-root` (when present) is moved before subcommands.
  56. This makes argparse+subparsers accept both:
  57. - `... --project-root X cmd ...`
  58. - `... cmd ... --project-root X`
  59. """
  60. value, rest = _extract_flag_value(argv, flag)
  61. if value is None:
  62. return argv
  63. return [flag, value] + rest
  64. def load_json_arg(raw: str) -> Any:
  65. """
  66. 解析 CLI 传入的 JSON 参数,支持两种形式:
  67. - 直接 JSON 字符串:'{"a":1}'
  68. - @ 文件路径:'@data.json'(从文件读取 JSON,避免 shell 引号地狱)
  69. - 特例:'@-' 表示从 stdin 读取
  70. """
  71. if raw is None:
  72. raise ValueError("missing json arg")
  73. text = str(raw).strip()
  74. if text.startswith("@"):
  75. target = text[1:].strip()
  76. if not target:
  77. raise ValueError("invalid json arg: '@' without path")
  78. if target == "-":
  79. content = sys.stdin.read()
  80. else:
  81. content = Path(target).read_text(encoding="utf-8")
  82. return json.loads(content)
  83. return json.loads(text)