#!/usr/bin/env python3 # form-remove v1.10 — Remove form from 1C object # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse import os import re import shutil import sys from lxml import etree # Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet] # регистр не различают, в argparse совпадение точное. def ci_parse_args(parser, argv=None): """parse_args по правилам PS: имена параметров и значения choices регистронезависимы.""" argv = list(sys.argv[1:] if argv is None else argv) names = {s.lower(): s for a in parser._actions for s in a.option_strings} for i, tok in enumerate(argv): if tok.startswith('-') and tok.lower() in names: argv[i] = names[tok.lower()] # choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр choice_map = {} for a in parser._actions: if a.choices: for s in a.option_strings: choice_map[s] = {str(c).lower(): c for c in a.choices} for i in range(len(argv) - 1): m = choice_map.get(argv[i]) if m and argv[i + 1].lower() in m: argv[i + 1] = m[argv[i + 1].lower()] return parser.parse_args(argv) NSMAP = {"md": "http://v8.1c.ru/8.3/MDClasses"} def _detect_xml_style(path): """Стиль существующего файла для round-trip-сохранения: BOM / EOL / регистр encoding / финальный перенос. None → файл новый (сохранить текущее поведение).""" try: raw = open(path, "rb").read() except OSError: return None bom = raw.startswith(b"\xef\xbb\xbf") body = raw[3:] if bom else raw crlf = b"\r\n" in body m = re.search(rb'encoding="([^"]+)"', body[:200]) enc = m.group(1).decode("ascii") if m else "utf-8" final_nl = body.endswith(b"\n") return {"bom": bom, "crlf": crlf, "enc": enc, "final_nl": final_nl} def _finalize_xml_bytes(xml_bytes, style): """Привести байты к стилю оригинала; для НОВОГО файла (style is None) — к канону выгрузки Конфигуратора: encoding="UTF-8", CRLF в разделителях, без перевода в конце.""" enc_decl = style["enc"] if style else "UTF-8" xml_bytes = xml_bytes.replace( b"", b'') # Канонизировать переносы к LF (убирает от \r в tail'ах) xml_bytes = (xml_bytes.replace(b" \n", b"\n").replace(b" ", b"") .replace(b"\r\n", b"\n").replace(b"\r", b"\n")) # Финальный перенос — как в оригинале (новый файл → нет, канон #57) want_final_nl = style["final_nl"] if style else False xml_bytes = xml_bytes.rstrip(b"\n") if want_final_nl: xml_bytes += b"\n" # EOL — как в оригинале (новый файл → CRLF, канон #57) if (style["crlf"] if style else True): xml_bytes = xml_bytes.replace(b"\n", b"\r\n") return xml_bytes def save_xml_with_bom(tree, path): """Save XML tree preserving the existing file's BOM/EOL/encoding-case/final-newline.""" style = _detect_xml_style(path) xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8") xml_bytes = _finalize_xml_bytes(xml_bytes, style) with open(path, "wb") as f: if style is None or style["bom"]: f.write(b"\xef\xbb\xbf") f.write(xml_bytes) def long_path(path): """Полный путь в длинной форме. Зеркало Get-LongPath в PS: там Resolve-Path оставляет короткое имя 8.3 (NSHIRO~1), а перечисление отдаёт длинное — сравнение молча не совпадало.""" if not os.path.exists(path): return "" return os.path.realpath(path) def remove_node_with_indent(node): """Удалить элемент вместе с предшествующим whitespace; опустевший контейнер сделать самозакрывающимся. Зеркало Remove-NodeWithIndent в PS.""" parent = node.getparent() if parent is None: return # В DOM (PS) whitespace — отдельные узлы: удаляются предшествующий и сам элемент, а # whitespace ПОСЛЕ элемента остаётся. В lxml он лежит в node.tail и ушёл бы вместе с # узлом, поэтому его надо передать предшественнику — иначе ``. prev = node.getprevious() if prev is not None: prev.tail = node.tail else: parent.text = node.tail parent.remove(node) # Опустевший контейнер: text="" сериализуется парой , # а нужен — PS-порт через DOM даёт именно его. if len(parent) == 0 and not (parent.text or "").strip(): parent.text = None def clear_form_refs(tree, ref): """Очистить ссылки на форму. Каноничное «не задано» зависит от файла: в корневом XML объекта и в Configuration.xml пустой слот штатен (164 508 пустых на корпус), а внутри Ext/Form.xml пустых и нет ни одного — там свойство просто отсутствует. Зеркало Clear-FormRefs в PS.""" root = tree.getroot() is_form_file = etree.QName(root).localname == "Form" touched = [] ref_lc = ref.lower() for el in list(root.iter()): if not isinstance(el.tag, str): continue if len(el) > 0: # только листья continue # Сравнение регистронезависимое — как у платформы (в PS -eq регистр не различает). if (el.text or "").strip().lower() != ref_lc: continue ln = etree.QName(el).localname parent = el.getparent() if ln == "Form" and parent is not None and etree.QName(parent).localname == "Item": touched.append(f"{etree.QName(parent).localname}/{ln}") remove_node_with_indent(parent) elif is_form_file: touched.append(ln) remove_node_with_indent(el) else: # text=None, а не "": пустая строка сериализуется парой , а # Конфигуратор пустых пар не пишет (0 на 476 942 XML корпуса) — нужен . touched.append(ln) el.text = None return touched def main(): sys.stdout.reconfigure(encoding="utf-8") sys.stderr.reconfigure(encoding="utf-8") parser = argparse.ArgumentParser(description="Remove form from 1C object", allow_abbrev=False) parser.add_argument("-ObjectName", "-ProcessorName", required=True) parser.add_argument("-FormName", required=True) parser.add_argument("-SrcDir", default="src") parser.add_argument("-Force", action="store_true") args = ci_parse_args(parser) object_name = args.ObjectName form_name = args.FormName src_dir = args.SrcDir force = args.Force # --- Checks --- root_xml_path = os.path.join(src_dir, f"{object_name}.xml") if not os.path.exists(root_xml_path): print(f"Корневой файл обработки не найден: {root_xml_path}", file=sys.stderr) sys.exit(1) processor_dir = os.path.join(src_dir, object_name) forms_dir = os.path.join(processor_dir, "Forms") form_meta_path = os.path.join(forms_dir, f"{form_name}.xml") form_dir = os.path.join(forms_dir, form_name) if not os.path.exists(form_meta_path): print(f"Метаданные формы не найдены: {form_meta_path}", file=sys.stderr) sys.exit(1) # --- Load root XML: kind and object name --- root_xml_full = long_path(root_xml_path) or os.path.abspath(root_xml_path) parser_xml = etree.XMLParser(remove_blank_text=False) tree = etree.parse(root_xml_full, parser_xml) root = tree.getroot() type_node = None for c in root: if isinstance(c.tag, str): type_node = c break if type_node is None: print(f"Не удалось определить вид объекта в {root_xml_path}", file=sys.stderr) sys.exit(1) md_type = etree.QName(type_node).localname name_node = type_node.find("md:Properties/md:Name", NSMAP) obj_meta_name = (name_node.text or "").strip() if name_node is not None else "" if not obj_meta_name: obj_meta_name = os.path.splitext(os.path.basename(root_xml_path))[0] # Полная ссылка на форму. Матч по ней целиком, а не по хвосту "Form.<Имя>": иначе при # удалении своей ФормаСписка обнулялась бы и ссылка на DocumentJournal.Ж.Form.ФормаСписка. form_ref = f"{md_type}.{obj_meta_name}.Form.{form_name}" # --- Find references across the configuration --- config_dir = None probe = long_path(src_dir) or os.path.abspath(src_dir) for _ in range(4): if not probe: break if os.path.exists(os.path.join(probe, "Configuration.xml")): config_dir = probe break parent_probe = os.path.dirname(probe) if parent_probe == probe: break probe = parent_probe form_meta_full = long_path(form_meta_path) form_dir_full = long_path(form_dir) references = [] if config_dir: ref_pattern = re.compile(r"<([A-Za-z0-9_.]+)>" + re.escape(form_ref) + r" 1 else "" print(f" {rel} — <{tag}>{suffix}") print() if not force: print("[ERROR] Удаление остановлено: форма используется.") print(" Решает пользователь: убрать ссылки, отказаться от удаления или") print(" повторить с -Force — тогда ссылки будут очищены.") sys.exit(1) print("[WARN] -Force: ссылки будут очищены") print() elif not config_dir: print("[WARN] Корень конфигурации не найден — ссылки в других объектах не проверены") # --- Delete files --- if os.path.isdir(form_dir): shutil.rmtree(form_dir) print(f"[OK] Удалён каталог: {form_dir}") os.remove(form_meta_path) print(f"[OK] Удалён файл: {form_meta_path}") # --- Modify root XML --- # Remove
FormName
from ChildObjects for node in root.findall(".//md:ChildObjects/md:Form", NSMAP): if node.text and node.text.strip() == form_name: remove_node_with_indent(node) break # Очистить слоты своего объекта: Default*/Auxiliary*Form и ChoiceForm у реквизитов. clear_form_refs(tree, form_ref) # Save with BOM save_xml_with_bom(tree, root_xml_full) print(f"[OK] Форма {form_name} удалена из {root_xml_path}") # --- Clean references in other files (only with -Force) --- for fp in sorted({r["path"] for r in references}): other_tree = etree.parse(fp, parser_xml) touched = clear_form_refs(other_tree, form_ref) if not touched: continue save_xml_with_bom(other_tree, fp) rel = os.path.relpath(fp, config_dir) print(f"[OK] Очищена ссылка в {rel} — {', '.join(sorted(set(touched)))}") if __name__ == "__main__": main()