mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-06-12 17:04:57 +03:00
526 lines
20 KiB
Python
526 lines
20 KiB
Python
#!/usr/bin/env python3
|
|
# subsystem-info v1.0 — Compact summary of 1C subsystem structure
|
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
|
|
import argparse
|
|
import os
|
|
import re
|
|
import sys
|
|
from collections import OrderedDict
|
|
from lxml import etree
|
|
|
|
sys.stdout.reconfigure(encoding="utf-8")
|
|
sys.stderr.reconfigure(encoding="utf-8")
|
|
|
|
# --- Argument parsing ---
|
|
parser = argparse.ArgumentParser(description="Analyze 1C subsystem structure", allow_abbrev=False)
|
|
parser.add_argument("-SubsystemPath", "-Path", required=True, help="Path to subsystem XML or Subsystems/ directory")
|
|
parser.add_argument("-Mode", choices=["overview", "content", "ci", "tree", "full"], default="overview", help="Output mode")
|
|
parser.add_argument("-Name", default="", help="Filter by name/type")
|
|
parser.add_argument("-Limit", type=int, default=150, help="Max lines to show")
|
|
parser.add_argument("-Offset", type=int, default=0, help="Lines to skip")
|
|
parser.add_argument("-OutFile", default="", help="Write output to file")
|
|
args = parser.parse_args()
|
|
|
|
# --- Output helper ---
|
|
lines_buf = []
|
|
|
|
def out(text=""):
|
|
lines_buf.append(text)
|
|
|
|
# --- Resolve path ---
|
|
subsystem_path = args.SubsystemPath
|
|
if not os.path.isabs(subsystem_path):
|
|
subsystem_path = os.path.join(os.getcwd(), subsystem_path)
|
|
|
|
NS = {
|
|
"md": "http://v8.1c.ru/8.3/MDClasses",
|
|
"v8": "http://v8.1c.ru/8.1/data/core",
|
|
"xr": "http://v8.1c.ru/8.3/xcf/readable",
|
|
"xsi": "http://www.w3.org/2001/XMLSchema-instance",
|
|
}
|
|
|
|
CI_NS = {
|
|
"ci": "http://v8.1c.ru/8.3/xcf/extrnprops",
|
|
"xr": "http://v8.1c.ru/8.3/xcf/readable",
|
|
}
|
|
|
|
# --- Helper: get LocalString text ---
|
|
def get_ml_text(node):
|
|
if node is None:
|
|
return ""
|
|
# Look for v8:item children
|
|
for item in node:
|
|
if not isinstance(item.tag, str):
|
|
continue
|
|
lang = ""
|
|
content = ""
|
|
for c in item:
|
|
if not isinstance(c.tag, str):
|
|
continue
|
|
local = etree.QName(c.tag).localname
|
|
if local == "lang":
|
|
lang = c.text or ""
|
|
if local == "content":
|
|
content = c.text or ""
|
|
if lang == "ru" and content:
|
|
return content
|
|
# fallback: first item
|
|
for item in node:
|
|
if not isinstance(item.tag, str):
|
|
continue
|
|
for c in item:
|
|
if not isinstance(c.tag, str):
|
|
continue
|
|
local = etree.QName(c.tag).localname
|
|
if local == "content" and c.text:
|
|
return c.text
|
|
return ""
|
|
|
|
# --- Helper: load subsystem XML ---
|
|
def load_subsystem_xml(xml_path):
|
|
tree = etree.parse(xml_path, etree.XMLParser(remove_blank_text=False))
|
|
doc_root = tree.getroot()
|
|
sub = doc_root.find("md:Subsystem", NS)
|
|
if sub is None:
|
|
print(f"[ERROR] Not a valid subsystem XML: {xml_path}", file=sys.stderr)
|
|
sys.exit(1)
|
|
return {"Doc": doc_root, "Sub": sub}
|
|
|
|
# --- Helper: get content items ---
|
|
def get_content_items(props):
|
|
items = []
|
|
content_node = props.find("md:Content", NS)
|
|
if content_node is None:
|
|
return items
|
|
for item in content_node.findall("xr:Item", NS):
|
|
if item.text:
|
|
items.append(item.text)
|
|
return items
|
|
|
|
# --- Helper: get child subsystem names ---
|
|
def get_child_names(sub):
|
|
names = []
|
|
co = sub.find("md:ChildObjects", NS)
|
|
if co is None:
|
|
return names
|
|
for child in co:
|
|
if not isinstance(child.tag, str):
|
|
continue
|
|
if etree.QName(child.tag).localname == "Subsystem":
|
|
names.append(child.text or "")
|
|
return names
|
|
|
|
# --- Helper: group content by type ---
|
|
def group_content_by_type(items):
|
|
groups = OrderedDict()
|
|
for item in items:
|
|
m = re.match(r'^([^.]+)\.(.+)$', item)
|
|
if m:
|
|
type_name = m.group(1)
|
|
name = m.group(2)
|
|
elif re.match(r'^[0-9a-fA-F]{8}-', item):
|
|
type_name = "[UUID]"
|
|
name = item
|
|
else:
|
|
type_name = "[Other]"
|
|
name = item
|
|
if type_name not in groups:
|
|
groups[type_name] = []
|
|
groups[type_name].append(name)
|
|
return groups
|
|
|
|
# --- Helper: find subsystem dir from XML path ---
|
|
def get_subsystem_dir(xml_path):
|
|
dir_name = os.path.dirname(xml_path)
|
|
base_name = os.path.splitext(os.path.basename(xml_path))[0]
|
|
return os.path.join(dir_name, base_name)
|
|
|
|
# --- Show functions ---
|
|
def show_overview(sub_name, synonym, comment_text, incl_ci, use_one_cmd,
|
|
explanation, pic_text, content_items, groups, child_names, has_ci):
|
|
out(f"Подсистема: {sub_name}")
|
|
if synonym and synonym != sub_name:
|
|
out(f"Синоним: {synonym}")
|
|
if comment_text:
|
|
out(f"Комментарий: {comment_text}")
|
|
out(f"ВключатьВКомандныйИнтерфейс: {incl_ci}")
|
|
out(f"ИспользоватьОднуКоманду: {use_one_cmd}")
|
|
if explanation:
|
|
out(f"Пояснение: {explanation}")
|
|
if pic_text:
|
|
out(f"Картинка: {pic_text}")
|
|
if len(content_items) > 0:
|
|
parts = []
|
|
for type_name in groups:
|
|
parts.append(f"{type_name}: {len(groups[type_name])}")
|
|
out(f"Состав: {len(content_items)} объектов ({', '.join(parts)})")
|
|
else:
|
|
out("Состав: пусто")
|
|
if len(child_names) > 0:
|
|
out(f"Дочерние подсистемы ({len(child_names)}): {', '.join(child_names)}")
|
|
if has_ci:
|
|
out("Командный интерфейс: есть")
|
|
|
|
|
|
def show_content(sub_name, content_items, groups, name_filter):
|
|
out(f"Состав подсистемы {sub_name} ({len(content_items)} объектов):")
|
|
out()
|
|
if name_filter:
|
|
if name_filter in groups:
|
|
filtered = groups[name_filter]
|
|
out(f"{name_filter} ({len(filtered)}):")
|
|
for n in filtered:
|
|
out(f" {n}")
|
|
else:
|
|
out(f"[INFO] Тип '{name_filter}' не найден в составе.")
|
|
out(f"Доступные типы: {', '.join(groups.keys())}")
|
|
else:
|
|
for type_name in groups:
|
|
out(f"{type_name} ({len(groups[type_name])}):")
|
|
for n in groups[type_name]:
|
|
out(f" {n}")
|
|
out()
|
|
|
|
|
|
def show_ci(sub_name, subsystem_path_local):
|
|
local_sub_dir = get_subsystem_dir(subsystem_path_local)
|
|
local_ci_path = os.path.join(local_sub_dir, "Ext", "CommandInterface.xml")
|
|
|
|
if not os.path.isfile(local_ci_path):
|
|
out(f"Командный интерфейс: {sub_name}")
|
|
out()
|
|
out("Файл CommandInterface.xml не найден.")
|
|
out(f"Путь: {local_ci_path}")
|
|
else:
|
|
ci_tree = etree.parse(local_ci_path, etree.XMLParser(remove_blank_text=False))
|
|
ci_root = ci_tree.getroot()
|
|
|
|
out(f"Командный интерфейс: {sub_name}")
|
|
out()
|
|
|
|
# --- CommandsVisibility ---
|
|
vis_section = ci_root.find("ci:CommandsVisibility", CI_NS)
|
|
if vis_section is not None:
|
|
hidden = []
|
|
shown = []
|
|
for cmd in vis_section.findall("ci:Command", CI_NS):
|
|
cmd_name = cmd.get("name", "")
|
|
vis = cmd.find("ci:Visibility/xr:Common", CI_NS)
|
|
if vis is not None and vis.text == "false":
|
|
hidden.append(cmd_name)
|
|
else:
|
|
shown.append(cmd_name)
|
|
total = len(hidden) + len(shown)
|
|
if not args.Name or args.Name == "visibility":
|
|
out(f"Видимость ({total}):")
|
|
if hidden:
|
|
out(f" СКРЫТО ({len(hidden)}):")
|
|
for h in hidden:
|
|
out(f" {h}")
|
|
if shown:
|
|
out(f" ПОКАЗАНО ({len(shown)}):")
|
|
for s in shown:
|
|
out(f" {s}")
|
|
out()
|
|
|
|
# --- CommandsPlacement ---
|
|
place_section = ci_root.find("ci:CommandsPlacement", CI_NS)
|
|
if place_section is not None:
|
|
placements = []
|
|
for cmd in place_section.findall("ci:Command", CI_NS):
|
|
cmd_name = cmd.get("name", "")
|
|
grp = cmd.find("ci:CommandGroup", CI_NS)
|
|
pl = cmd.find("ci:Placement", CI_NS)
|
|
grp_text = grp.text if grp is not None and grp.text else "?"
|
|
pl_text = pl.text if pl is not None and pl.text else "?"
|
|
placements.append({"Name": cmd_name, "Group": grp_text, "Placement": pl_text})
|
|
if (not args.Name or args.Name == "placement") and placements:
|
|
arrow = "\u2192"
|
|
out(f"Размещение ({len(placements)}):")
|
|
for p in placements:
|
|
out(f" {p['Name']} {arrow} {p['Group']} ({p['Placement']})")
|
|
out()
|
|
|
|
# --- CommandsOrder ---
|
|
order_section = ci_root.find("ci:CommandsOrder", CI_NS)
|
|
if order_section is not None:
|
|
order_groups = OrderedDict()
|
|
for cmd in order_section.findall("ci:Command", CI_NS):
|
|
cmd_name = cmd.get("name", "")
|
|
grp = cmd.find("ci:CommandGroup", CI_NS)
|
|
grp_text = grp.text if grp is not None and grp.text else "?"
|
|
if grp_text not in order_groups:
|
|
order_groups[grp_text] = []
|
|
order_groups[grp_text].append(cmd_name)
|
|
total_order = sum(len(v) for v in order_groups.values())
|
|
if (not args.Name or args.Name == "order") and total_order > 0:
|
|
out(f"Порядок команд ({total_order}):")
|
|
for grp_name, cmds in order_groups.items():
|
|
out(f" [{grp_name}]:")
|
|
for c in cmds:
|
|
out(f" {c}")
|
|
out()
|
|
|
|
# --- SubsystemsOrder ---
|
|
sub_order_section = ci_root.find("ci:SubsystemsOrder", CI_NS)
|
|
if sub_order_section is not None:
|
|
sub_order = []
|
|
for s in sub_order_section.findall("ci:Subsystem", CI_NS):
|
|
if s.text:
|
|
sub_order.append(s.text)
|
|
if (not args.Name or args.Name == "subsystems") and sub_order:
|
|
out(f"Порядок подсистем ({len(sub_order)}):")
|
|
for i, s in enumerate(sub_order):
|
|
out(f" {i + 1}. {s}")
|
|
out()
|
|
|
|
# --- GroupsOrder ---
|
|
grp_order_section = ci_root.find("ci:GroupsOrder", CI_NS)
|
|
if grp_order_section is not None:
|
|
grp_order = []
|
|
for g in grp_order_section.findall("ci:Group", CI_NS):
|
|
if g.text:
|
|
grp_order.append(g.text)
|
|
if (not args.Name or args.Name == "groups") and grp_order:
|
|
out(f"Порядок групп ({len(grp_order)}):")
|
|
for g in grp_order:
|
|
out(f" {g}")
|
|
|
|
|
|
# ============================================================
|
|
# Mode: tree
|
|
# ============================================================
|
|
if args.Mode == "tree":
|
|
is_dir = os.path.isdir(subsystem_path)
|
|
root_dir = None
|
|
root_xml = None
|
|
|
|
if is_dir:
|
|
root_dir = subsystem_path
|
|
else:
|
|
if not os.path.isfile(subsystem_path):
|
|
print(f"[ERROR] File not found: {subsystem_path}", file=sys.stderr)
|
|
sys.exit(1)
|
|
root_xml = subsystem_path
|
|
|
|
# Box-drawing chars
|
|
T_BRANCH = "\u251C\u2500\u2500 " # ├──
|
|
T_LAST = "\u2514\u2500\u2500 " # └──
|
|
T_PIPE = "\u2502 " # │
|
|
T_ARROW = "\u2192" # →
|
|
|
|
def get_tree_line(xml_path):
|
|
parsed = load_subsystem_xml(xml_path)
|
|
sub = parsed["Sub"]
|
|
props = sub.find("md:Properties", NS)
|
|
name_node = props.find("md:Name", NS)
|
|
name = name_node.text if name_node is not None else ""
|
|
|
|
markers = []
|
|
sub_dir = get_subsystem_dir(xml_path)
|
|
ci_path = os.path.join(sub_dir, "Ext", "CommandInterface.xml")
|
|
if os.path.isfile(ci_path):
|
|
markers.append("CI")
|
|
use_one = props.find("md:UseOneCommand", NS)
|
|
if use_one is not None and use_one.text == "true":
|
|
markers.append("OneCmd")
|
|
incl_ci_node = props.find("md:IncludeInCommandInterface", NS)
|
|
if incl_ci_node is not None and incl_ci_node.text == "false":
|
|
markers.append("Скрыт")
|
|
marker_str = f" [{', '.join(markers)}]" if markers else ""
|
|
|
|
content_items = get_content_items(props)
|
|
child_names = get_child_names(sub)
|
|
child_str = f", {len(child_names)} дочерних" if child_names else ""
|
|
|
|
return {
|
|
"Label": f"{name}{marker_str} ({len(content_items)} объектов{child_str})",
|
|
"SubDir": sub_dir,
|
|
"ChildNames": child_names,
|
|
}
|
|
|
|
def build_tree_entry(xml_path, prefix, is_last, is_root):
|
|
info = get_tree_line(xml_path)
|
|
|
|
if is_root:
|
|
connector = ""
|
|
elif is_last:
|
|
connector = T_LAST
|
|
else:
|
|
connector = T_BRANCH
|
|
out(f"{prefix}{connector}{info['Label']}")
|
|
|
|
if info["ChildNames"]:
|
|
if is_root:
|
|
child_prefix = ""
|
|
elif is_last:
|
|
child_prefix = prefix + " "
|
|
else:
|
|
child_prefix = prefix + T_PIPE
|
|
|
|
subs_dir = os.path.join(info["SubDir"], "Subsystems")
|
|
for i, child_name in enumerate(info["ChildNames"]):
|
|
child_xml = os.path.join(subs_dir, f"{child_name}.xml")
|
|
child_is_last = (i == len(info["ChildNames"]) - 1)
|
|
if os.path.isfile(child_xml):
|
|
build_tree_entry(child_xml, child_prefix, child_is_last, False)
|
|
else:
|
|
conn2 = T_LAST if child_is_last else T_BRANCH
|
|
out(f"{child_prefix}{conn2}{child_name} [NOT FOUND]")
|
|
|
|
if root_dir:
|
|
label = os.path.basename(root_dir)
|
|
out(f"Дерево подсистем от: {label}/")
|
|
out()
|
|
xml_files = sorted(
|
|
[f for f in os.listdir(root_dir) if f.lower().endswith(".xml") and os.path.isfile(os.path.join(root_dir, f))],
|
|
key=lambda x: x.lower()
|
|
)
|
|
if args.Name:
|
|
xml_files = [f for f in xml_files if os.path.splitext(f)[0] == args.Name]
|
|
if not xml_files:
|
|
print(f"[ERROR] Subsystem '{args.Name}' not found in {root_dir}", file=sys.stderr)
|
|
sys.exit(1)
|
|
for i, fname in enumerate(xml_files):
|
|
build_tree_entry(os.path.join(root_dir, fname), "", i == len(xml_files) - 1, True)
|
|
else:
|
|
build_tree_entry(root_xml, "", True, True)
|
|
|
|
elif args.Mode == "ci":
|
|
# ============================================================
|
|
# Mode: ci -- CommandInterface.xml
|
|
# ============================================================
|
|
if os.path.isdir(subsystem_path):
|
|
print("[ERROR] ci mode requires a subsystem .xml file, not a directory", file=sys.stderr)
|
|
sys.exit(1)
|
|
# File not found -- check Dir/Name/Name.xml -> Dir/Name.xml
|
|
if not os.path.isfile(subsystem_path):
|
|
fn = os.path.splitext(os.path.basename(subsystem_path))[0]
|
|
pd = os.path.dirname(subsystem_path)
|
|
if fn == os.path.basename(pd):
|
|
c = os.path.join(os.path.dirname(pd), f"{fn}.xml")
|
|
if os.path.isfile(c):
|
|
subsystem_path = c
|
|
if not os.path.isfile(subsystem_path):
|
|
print(f"[ERROR] File not found: {subsystem_path}", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
parsed = load_subsystem_xml(subsystem_path)
|
|
sub = parsed["Sub"]
|
|
props = sub.find("md:Properties", NS)
|
|
name_node = props.find("md:Name", NS)
|
|
sub_name = name_node.text if name_node is not None else ""
|
|
|
|
show_ci(sub_name, subsystem_path)
|
|
|
|
else:
|
|
# ============================================================
|
|
# Mode: overview / content / full -- requires a subsystem XML file
|
|
# ============================================================
|
|
if os.path.isdir(subsystem_path):
|
|
dir_name = os.path.basename(subsystem_path)
|
|
candidate = os.path.join(subsystem_path, f"{dir_name}.xml")
|
|
sibling = os.path.join(os.path.dirname(subsystem_path), f"{dir_name}.xml")
|
|
if os.path.isfile(candidate):
|
|
subsystem_path = candidate
|
|
elif os.path.isfile(sibling):
|
|
subsystem_path = sibling
|
|
else:
|
|
print(f"[ERROR] No {dir_name}.xml found in directory. Use -Mode tree for directory listing.", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
# File not found -- check Dir/Name/Name.xml -> Dir/Name.xml
|
|
if not os.path.isfile(subsystem_path):
|
|
fn = os.path.splitext(os.path.basename(subsystem_path))[0]
|
|
pd = os.path.dirname(subsystem_path)
|
|
if fn == os.path.basename(pd):
|
|
c = os.path.join(os.path.dirname(pd), f"{fn}.xml")
|
|
if os.path.isfile(c):
|
|
subsystem_path = c
|
|
if not os.path.isfile(subsystem_path):
|
|
print(f"[ERROR] File not found: {subsystem_path}", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
parsed = load_subsystem_xml(subsystem_path)
|
|
sub = parsed["Sub"]
|
|
props = sub.find("md:Properties", NS)
|
|
|
|
name_node = props.find("md:Name", NS)
|
|
sub_name = name_node.text if name_node is not None else ""
|
|
synonym = get_ml_text(props.find("md:Synonym", NS))
|
|
comment_node = props.find("md:Comment", NS)
|
|
comment_text = comment_node.text if comment_node is not None and comment_node.text else ""
|
|
incl_help_node = props.find("md:IncludeHelpInContents", NS)
|
|
incl_help = incl_help_node.text if incl_help_node is not None else ""
|
|
incl_ci_node = props.find("md:IncludeInCommandInterface", NS)
|
|
incl_ci = incl_ci_node.text if incl_ci_node is not None else ""
|
|
use_one_cmd_node = props.find("md:UseOneCommand", NS)
|
|
use_one_cmd = use_one_cmd_node.text if use_one_cmd_node is not None else ""
|
|
explanation = get_ml_text(props.find("md:Explanation", NS))
|
|
|
|
# Picture
|
|
pic_node = props.find("md:Picture", NS)
|
|
pic_text = ""
|
|
if pic_node is not None and len(pic_node) > 0:
|
|
pic_ref = pic_node.find("xr:Ref", NS)
|
|
if pic_ref is not None and pic_ref.text:
|
|
pic_text = pic_ref.text
|
|
|
|
# Content
|
|
content_items = get_content_items(props)
|
|
groups = group_content_by_type(content_items)
|
|
|
|
# Children
|
|
child_names = get_child_names(sub)
|
|
|
|
# CI presence
|
|
sub_dir = get_subsystem_dir(subsystem_path)
|
|
ci_path = os.path.join(sub_dir, "Ext", "CommandInterface.xml")
|
|
has_ci = os.path.isfile(ci_path)
|
|
|
|
if args.Mode == "overview":
|
|
show_overview(sub_name, synonym, comment_text, incl_ci, use_one_cmd,
|
|
explanation, pic_text, content_items, groups, child_names, has_ci)
|
|
elif args.Mode == "content":
|
|
show_content(sub_name, content_items, groups, args.Name)
|
|
elif args.Mode == "full":
|
|
show_overview(sub_name, synonym, comment_text, incl_ci, use_one_cmd,
|
|
explanation, pic_text, content_items, groups, child_names, has_ci)
|
|
out()
|
|
out("--- content ---")
|
|
out()
|
|
show_content(sub_name, content_items, groups, args.Name)
|
|
out()
|
|
out("--- ci ---")
|
|
out()
|
|
show_ci(sub_name, subsystem_path)
|
|
|
|
# --- Pagination and output ---
|
|
total_lines = len(lines_buf)
|
|
out_lines = lines_buf[:]
|
|
|
|
if args.Offset > 0:
|
|
if args.Offset >= total_lines:
|
|
print(f"[INFO] Offset {args.Offset} exceeds total lines ({total_lines}). Nothing to show.")
|
|
sys.exit(0)
|
|
out_lines = out_lines[args.Offset:]
|
|
|
|
if args.Limit > 0 and len(out_lines) > args.Limit:
|
|
shown = out_lines[:args.Limit]
|
|
remaining = total_lines - args.Offset - args.Limit
|
|
shown.append("")
|
|
shown.append(f"[ОБРЕЗАНО] Показано {args.Limit} из {total_lines} строк. Используйте -Offset {args.Offset + args.Limit} для продолжения.")
|
|
out_lines = shown
|
|
|
|
if args.OutFile:
|
|
out_file = args.OutFile
|
|
if not os.path.isabs(out_file):
|
|
out_file = os.path.join(os.getcwd(), out_file)
|
|
with open(out_file, "w", encoding="utf-8-sig") as f:
|
|
f.write("\n".join(out_lines))
|
|
print(f"Output written to {out_file}")
|
|
else:
|
|
for line in out_lines:
|
|
print(line)
|