mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-08-13 15:03:20 +03:00
Compare commits
166
Commits
w-2026-07-26
...
main
@@ -1,4 +1,4 @@
|
|||||||
# cf-edit v1.11 — Edit 1C configuration root (Configuration.xml)
|
# cf-edit v1.19 — Edit 1C configuration root (Configuration.xml)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)][Alias('Path')][string]$ConfigPath,
|
[Parameter(Mandatory)][Alias('Path')][string]$ConfigPath,
|
||||||
@@ -163,6 +163,8 @@ function Assert-EditAllowed([string]$targetPath, [string]$require) {
|
|||||||
Assert-EditAllowed $resolvedPath 'editable'
|
Assert-EditAllowed $resolvedPath 'editable'
|
||||||
|
|
||||||
# --- Load XML with PreserveWhitespace ---
|
# --- Load XML with PreserveWhitespace ---
|
||||||
|
# NB: парсер XML по спецификации схлопывает CRLF в LF, а вставки ниже собираются с
|
||||||
|
# явным CRLF — поэтому EOL приводится к целевому в точке записи (см. финализацию).
|
||||||
$script:xmlDoc = New-Object System.Xml.XmlDocument
|
$script:xmlDoc = New-Object System.Xml.XmlDocument
|
||||||
$script:xmlDoc.PreserveWhitespace = $true
|
$script:xmlDoc.PreserveWhitespace = $true
|
||||||
$script:xmlDoc.Load($resolvedPath)
|
$script:xmlDoc.Load($resolvedPath)
|
||||||
@@ -691,7 +693,9 @@ $bodyBlock$declarations
|
|||||||
if (-not (Test-Path $extDir)) { New-Item -ItemType Directory -Path $extDir -Force | Out-Null }
|
if (-not (Test-Path $extDir)) { New-Item -ItemType Directory -Path $extDir -Force | Out-Null }
|
||||||
$caiPath = Join-Path $extDir "ClientApplicationInterface.xml"
|
$caiPath = Join-Path $extDir "ClientApplicationInterface.xml"
|
||||||
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
|
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
|
||||||
[System.IO.File]::WriteAllText($caiPath, $caiXml, $utf8Bom)
|
# Файл создаём мы — канон: CRLF в разделителях, без перевода строки в конце.
|
||||||
|
$caiXml = ($caiXml -replace "`r`n", "`n") -replace "`n", "`r`n"
|
||||||
|
[System.IO.File]::WriteAllText($caiPath, $caiXml.TrimEnd("`r", "`n"), $utf8Bom)
|
||||||
$script:modifyCount++
|
$script:modifyCount++
|
||||||
Info "Wrote panel layout: $caiPath"
|
Info "Wrote panel layout: $caiPath"
|
||||||
}
|
}
|
||||||
@@ -880,7 +884,9 @@ $rightXml
|
|||||||
if (-not (Test-Path $extDir)) { New-Item -ItemType Directory -Path $extDir -Force | Out-Null }
|
if (-not (Test-Path $extDir)) { New-Item -ItemType Directory -Path $extDir -Force | Out-Null }
|
||||||
$hpPath = Join-Path $extDir "HomePageWorkArea.xml"
|
$hpPath = Join-Path $extDir "HomePageWorkArea.xml"
|
||||||
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
|
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
|
||||||
[System.IO.File]::WriteAllText($hpPath, $hpXml, $utf8Bom)
|
# Файл создаём мы — канон: CRLF в разделителях, без перевода строки в конце.
|
||||||
|
$hpXml = ($hpXml -replace "`r`n", "`n") -replace "`n", "`r`n"
|
||||||
|
[System.IO.File]::WriteAllText($hpPath, $hpXml.TrimEnd("`r", "`n"), $utf8Bom)
|
||||||
$script:modifyCount++
|
$script:modifyCount++
|
||||||
Info "Wrote home page layout: $hpPath"
|
Info "Wrote home page layout: $hpPath"
|
||||||
}
|
}
|
||||||
@@ -982,6 +988,14 @@ $memStream.Close()
|
|||||||
$text = [System.Text.Encoding]::UTF8.GetString($bytes)
|
$text = [System.Text.Encoding]::UTF8.GetString($bytes)
|
||||||
if ($text.Length -gt 0 -and $text[0] -eq [char]0xFEFF) { $text = $text.Substring(1) }
|
if ($text.Length -gt 0 -and $text[0] -eq [char]0xFEFF) { $text = $text.Substring(1) }
|
||||||
$text = $text.Replace('encoding="utf-8"', 'encoding="UTF-8"')
|
$text = $text.Replace('encoding="utf-8"', 'encoding="UTF-8"')
|
||||||
|
# Пустой элемент: XmlWriter отдаёт `<a />`, Конфигуратор пишет `<a/>`. Внутри
|
||||||
|
# CDATA/комментария ` />` может быть содержимым (там `>` не экранируется),
|
||||||
|
# поэтому они идут первыми ветками альтернации и возвращаются как есть.
|
||||||
|
$text = [regex]::Replace($text, '(?s)<!\[CDATA\[.*?\]\]>|<!--.*?-->|(?<=\S) />', { param($m) if ($m.Value -eq ' />') { '/>' } else { $m.Value } })
|
||||||
|
# Целевой перевод строки: стиль файла-назначения — правка наследует его (#44/#46/#47),
|
||||||
|
# новый файл получает канон выгрузки CRLF. Зеркало _detect_xml_style в py-порту.
|
||||||
|
$targetEol = if ((Test-Path -LiteralPath $resolvedPath) -and ([System.IO.File]::ReadAllText($resolvedPath) -notmatch "`r`n")) { "`n" } else { "`r`n" }
|
||||||
|
$text = ($text -replace "`r`n", "`n") -replace "`n", $targetEol
|
||||||
|
|
||||||
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
|
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
|
||||||
[System.IO.File]::WriteAllText($resolvedPath, $text, $utf8Bom)
|
[System.IO.File]::WriteAllText($resolvedPath, $text, $utf8Bom)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# cf-edit v1.11 — Edit 1C configuration root (Configuration.xml)
|
# cf-edit v1.19 — Edit 1C configuration root (Configuration.xml)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -12,6 +12,65 @@ import uuid as _uuid
|
|||||||
from html import escape as html_escape
|
from html import escape as html_escape
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
|
|
||||||
|
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
|
||||||
|
# регистр не различают, в argparse совпадение точное.
|
||||||
|
class CIDict(dict):
|
||||||
|
# Ключи храним КАК ЕСТЬ: часть из них — имена объектов (табличные части, стандартные
|
||||||
|
# реквизиты), они попадают в XML. Регистронезависим только поиск. Порядок вставки
|
||||||
|
# сохраняется — от него зависит порядок эмиссии.
|
||||||
|
def _actual(self, key):
|
||||||
|
if not isinstance(key, str) or dict.__contains__(self, key):
|
||||||
|
return key
|
||||||
|
ci = self.__dict__.get('_ci')
|
||||||
|
if ci is None or len(ci) != len(self):
|
||||||
|
ci = {k.lower(): k for k in self if isinstance(k, str)}
|
||||||
|
self.__dict__['_ci'] = ci
|
||||||
|
return ci.get(key.lower(), key)
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
return dict.__getitem__(self, self._actual(key))
|
||||||
|
|
||||||
|
def __contains__(self, key):
|
||||||
|
return dict.__contains__(self, self._actual(key))
|
||||||
|
|
||||||
|
def get(self, key, default=None):
|
||||||
|
return dict.get(self, self._actual(key), default)
|
||||||
|
|
||||||
|
def pop(self, key, *default):
|
||||||
|
return dict.pop(self, self._actual(key), *default)
|
||||||
|
|
||||||
|
def __setitem__(self, key, value):
|
||||||
|
# запись по ключу, отличающемуся регистром, обновляет существующий, а не плодит дубль
|
||||||
|
dict.__setitem__(self, self._actual(key), value)
|
||||||
|
|
||||||
|
def ci_json(obj):
|
||||||
|
"""Рекурсивно оборачивает разобранный JSON: словари → CIDict, списки обходятся."""
|
||||||
|
if isinstance(obj, dict):
|
||||||
|
return CIDict((k, ci_json(v)) for k, v in obj.items())
|
||||||
|
if isinstance(obj, list):
|
||||||
|
return [ci_json(v) for v in obj]
|
||||||
|
return obj
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# Support guard (Ext/ParentConfigurations.bin) — see docs/1c-support-state-spec.md
|
# Support guard (Ext/ParentConfigurations.bin) — see docs/1c-support-state-spec.md
|
||||||
@@ -341,21 +400,22 @@ def _detect_xml_style(path):
|
|||||||
|
|
||||||
|
|
||||||
def _finalize_xml_bytes(xml_bytes, style):
|
def _finalize_xml_bytes(xml_bytes, style):
|
||||||
"""Привести сериализованные байты к стилю оригинала (или к дефолту, если style is None)."""
|
"""Привести байты к стилю оригинала; для НОВОГО файла (style is None) — к канону
|
||||||
enc_decl = style["enc"] if style else "utf-8"
|
выгрузки Конфигуратора: encoding="UTF-8", CRLF в разделителях, без перевода в конце."""
|
||||||
|
enc_decl = style["enc"] if style else "UTF-8"
|
||||||
xml_bytes = xml_bytes.replace(
|
xml_bytes = xml_bytes.replace(
|
||||||
b"<?xml version='1.0' encoding='UTF-8'?>",
|
b"<?xml version='1.0' encoding='UTF-8'?>",
|
||||||
b'<?xml version="1.0" encoding="' + enc_decl.encode("ascii") + b'"?>')
|
b'<?xml version="1.0" encoding="' + enc_decl.encode("ascii") + b'"?>')
|
||||||
# Канонизировать переносы к LF (убирает от \r в tail'ах)
|
# Канонизировать переносы к LF (убирает от \r в tail'ах)
|
||||||
xml_bytes = (xml_bytes.replace(b" \n", b"\n").replace(b" ", b"")
|
xml_bytes = (xml_bytes.replace(b" \n", b"\n").replace(b" ", b"")
|
||||||
.replace(b"\r\n", b"\n").replace(b"\r", b"\n"))
|
.replace(b"\r\n", b"\n").replace(b"\r", b"\n"))
|
||||||
# Финальный перенос — как в оригинале (новый файл → есть)
|
# Финальный перенос — как в оригинале (новый файл → нет, канон #57)
|
||||||
want_final_nl = style["final_nl"] if style else True
|
want_final_nl = style["final_nl"] if style else False
|
||||||
xml_bytes = xml_bytes.rstrip(b"\n")
|
xml_bytes = xml_bytes.rstrip(b"\n")
|
||||||
if want_final_nl:
|
if want_final_nl:
|
||||||
xml_bytes += b"\n"
|
xml_bytes += b"\n"
|
||||||
# EOL — как в оригинале (новый файл → LF, текущее поведение)
|
# EOL — как в оригинале (новый файл → CRLF, канон #57)
|
||||||
if style and style["crlf"]:
|
if (style["crlf"] if style else True):
|
||||||
xml_bytes = xml_bytes.replace(b"\n", b"\r\n")
|
xml_bytes = xml_bytes.replace(b"\n", b"\r\n")
|
||||||
return xml_bytes
|
return xml_bytes
|
||||||
|
|
||||||
@@ -379,7 +439,7 @@ def main():
|
|||||||
parser.add_argument("-Operation", default=None, choices=["modify-property", "add-childObject", "remove-childObject", "add-defaultRole", "remove-defaultRole", "set-defaultRoles", "set-panels", "set-home-page"])
|
parser.add_argument("-Operation", default=None, choices=["modify-property", "add-childObject", "remove-childObject", "add-defaultRole", "remove-defaultRole", "set-defaultRoles", "set-panels", "set-home-page"])
|
||||||
parser.add_argument("-Value", default=None)
|
parser.add_argument("-Value", default=None)
|
||||||
parser.add_argument("-NoValidate", action="store_true")
|
parser.add_argument("-NoValidate", action="store_true")
|
||||||
args = parser.parse_args()
|
args = ci_parse_args(parser)
|
||||||
|
|
||||||
if args.DefinitionFile and args.Operation:
|
if args.DefinitionFile and args.Operation:
|
||||||
print("Cannot use both -DefinitionFile and -Operation", file=sys.stderr)
|
print("Cannot use both -DefinitionFile and -Operation", file=sys.stderr)
|
||||||
@@ -762,7 +822,7 @@ def main():
|
|||||||
layout = value
|
layout = value
|
||||||
if isinstance(layout, str):
|
if isinstance(layout, str):
|
||||||
try:
|
try:
|
||||||
layout = json.loads(layout)
|
layout = ci_json(json.loads(layout))
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
print(f"set-panels value must be valid JSON object", file=sys.stderr)
|
print(f"set-panels value must be valid JSON object", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
@@ -917,7 +977,7 @@ def main():
|
|||||||
layout = value
|
layout = value
|
||||||
if isinstance(layout, str):
|
if isinstance(layout, str):
|
||||||
try:
|
try:
|
||||||
layout = json.loads(layout)
|
layout = ci_json(json.loads(layout))
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
print("set-home-page value must be valid JSON object", file=sys.stderr)
|
print("set-home-page value must be valid JSON object", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
@@ -985,7 +1045,7 @@ def main():
|
|||||||
if not os.path.isabs(def_file):
|
if not os.path.isabs(def_file):
|
||||||
def_file = os.path.join(os.getcwd(), def_file)
|
def_file = os.path.join(os.getcwd(), def_file)
|
||||||
with open(def_file, "r", encoding="utf-8-sig") as fh:
|
with open(def_file, "r", encoding="utf-8-sig") as fh:
|
||||||
ops = json.loads(fh.read())
|
ops = ci_json(json.loads(fh.read()))
|
||||||
if isinstance(ops, list):
|
if isinstance(ops, list):
|
||||||
operations = ops
|
operations = ops
|
||||||
else:
|
else:
|
||||||
@@ -995,23 +1055,25 @@ def main():
|
|||||||
|
|
||||||
for op in operations:
|
for op in operations:
|
||||||
op_name = op.get("operation", args.Operation or "")
|
op_name = op.get("operation", args.Operation or "")
|
||||||
|
# PS сравнивает имя операции через switch, а он регистронезависим.
|
||||||
|
op_key = str(op_name).lower()
|
||||||
op_value = op.get("value", args.Value or "")
|
op_value = op.get("value", args.Value or "")
|
||||||
|
|
||||||
if op_name == "modify-property":
|
if op_key == "modify-property":
|
||||||
do_modify_property(op_value if isinstance(op_value, str) else str(op_value))
|
do_modify_property(op_value if isinstance(op_value, str) else str(op_value))
|
||||||
elif op_name == "add-childObject":
|
elif op_key == "add-childobject":
|
||||||
do_add_child_object(op_value if isinstance(op_value, str) else str(op_value))
|
do_add_child_object(op_value if isinstance(op_value, str) else str(op_value))
|
||||||
elif op_name == "remove-childObject":
|
elif op_key == "remove-childobject":
|
||||||
do_remove_child_object(op_value if isinstance(op_value, str) else str(op_value))
|
do_remove_child_object(op_value if isinstance(op_value, str) else str(op_value))
|
||||||
elif op_name == "add-defaultRole":
|
elif op_key == "add-defaultrole":
|
||||||
do_add_default_role(op_value if isinstance(op_value, str) else str(op_value))
|
do_add_default_role(op_value if isinstance(op_value, str) else str(op_value))
|
||||||
elif op_name == "remove-defaultRole":
|
elif op_key == "remove-defaultrole":
|
||||||
do_remove_default_role(op_value if isinstance(op_value, str) else str(op_value))
|
do_remove_default_role(op_value if isinstance(op_value, str) else str(op_value))
|
||||||
elif op_name == "set-defaultRoles":
|
elif op_key == "set-defaultroles":
|
||||||
do_set_default_roles(op_value if isinstance(op_value, str) else str(op_value))
|
do_set_default_roles(op_value if isinstance(op_value, str) else str(op_value))
|
||||||
elif op_name == "set-panels":
|
elif op_key == "set-panels":
|
||||||
do_set_panels(op_value)
|
do_set_panels(op_value)
|
||||||
elif op_name == "set-home-page":
|
elif op_key == "set-home-page":
|
||||||
do_set_home_page(op_value)
|
do_set_home_page(op_value)
|
||||||
else:
|
else:
|
||||||
print(f"Unknown operation: {op_name}", file=sys.stderr)
|
print(f"Unknown operation: {op_name}", file=sys.stderr)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# cf-info v1.4 — Compact summary of 1C configuration root
|
# cf-info v1.5 — Compact summary of 1C configuration root
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory=$true)][Alias('Path')][string]$ConfigPath,
|
[Parameter(Mandatory=$true)][Alias('Path')][string]$ConfigPath,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# cf-info v1.4 — Compact summary of 1C configuration root
|
# cf-info v1.5 — Compact summary of 1C configuration root
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -12,6 +12,28 @@ from lxml import etree
|
|||||||
sys.stdout.reconfigure(encoding="utf-8")
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
sys.stderr.reconfigure(encoding="utf-8")
|
||||||
|
|
||||||
|
# Регистронезависимый ввод — паритет с 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)
|
||||||
|
|
||||||
|
|
||||||
# --- Argument parsing ---
|
# --- Argument parsing ---
|
||||||
parser = argparse.ArgumentParser(description="Analyze 1C configuration structure", allow_abbrev=False)
|
parser = argparse.ArgumentParser(description="Analyze 1C configuration structure", allow_abbrev=False)
|
||||||
parser.add_argument("-ConfigPath", "-Path", required=True, help="Path to Configuration.xml or directory")
|
parser.add_argument("-ConfigPath", "-Path", required=True, help="Path to Configuration.xml or directory")
|
||||||
@@ -20,7 +42,7 @@ parser.add_argument("-Section", "-Name", choices=["home-page"], default=None, he
|
|||||||
parser.add_argument("-Limit", type=int, default=150, help="Max lines to show")
|
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("-Offset", type=int, default=0, help="Lines to skip")
|
||||||
parser.add_argument("-OutFile", default="", help="Write output to file")
|
parser.add_argument("-OutFile", default="", help="Write output to file")
|
||||||
args = parser.parse_args()
|
args = ci_parse_args(parser)
|
||||||
|
|
||||||
# --- Output helper (collect all, paginate at the end) ---
|
# --- Output helper (collect all, paginate at the end) ---
|
||||||
lines_buf = []
|
lines_buf = []
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# cf-init v1.3 — Create empty 1C configuration scaffold
|
# cf-init v1.11 — Create empty 1C configuration scaffold (+write_xml_file/write_utf8_bom: общий эталон записи)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory)]
|
||||||
@@ -9,13 +9,19 @@ param(
|
|||||||
[string]$Vendor,
|
[string]$Vendor,
|
||||||
[string]$CompatibilityMode = "Version8_3_24",
|
[string]$CompatibilityMode = "Version8_3_24",
|
||||||
# Версия формата выгрузки (MDClasses). Её задаёт ПЛАТФОРМА, которой выгружают, и от режима
|
# Версия формата выгрузки (MDClasses). Её задаёт ПЛАТФОРМА, которой выгружают, и от режима
|
||||||
# совместимости она не зависит: 8.3.24 пишет 2.17, 8.3.27 — 2.20. Дефолт консервативный —
|
# совместимости она не зависит: 8.3.20-8.3.24 пишут 2.17, 8.3.25 — 2.18, 8.3.26 — 2.19,
|
||||||
# 2.17 читается всеми поддерживаемыми платформами.
|
# 8.3.27 — 2.20. Дефолт консервативный: 2.17 читается всеми поддерживаемыми платформами.
|
||||||
[ValidateSet("2.17", "2.20", "2.21")]
|
[ValidateSet("2.17", "2.18", "2.19", "2.20", "2.21")]
|
||||||
[string]$FormatVersion = "2.17"
|
[string]$FormatVersion = "2.17"
|
||||||
)
|
)
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
function Esc-XmlText {
|
||||||
|
param([string]$s)
|
||||||
|
# Эскейп ТЕКСТА элемента: только & < > — кавычку и апостроф платформа держит сырыми.
|
||||||
|
return $s.Replace('&','&').Replace('<','<').Replace('>','>')
|
||||||
|
}
|
||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
|
||||||
# --- Resolve output dir ---
|
# --- Resolve output dir ---
|
||||||
@@ -43,6 +49,9 @@ $co6 = [guid]::NewGuid().ToString()
|
|||||||
$co7 = [guid]::NewGuid().ToString()
|
$co7 = [guid]::NewGuid().ToString()
|
||||||
|
|
||||||
# --- Mobile functionalities ---
|
# --- Mobile functionalities ---
|
||||||
|
# Версия формата как число — по ней ниже включаются вставки 2.21.
|
||||||
|
$is221 = (($FormatVersion -match '^(\d+)\.(\d+)$') -and ([int]$Matches[1] * 100 + [int]$Matches[2]) -ge 221)
|
||||||
|
|
||||||
$mobileFuncs = @(
|
$mobileFuncs = @(
|
||||||
@("Biometrics","true"), @("Location","false"), @("BackgroundLocation","false"),
|
@("Biometrics","true"), @("Location","false"), @("BackgroundLocation","false"),
|
||||||
@("BluetoothPrinters","false"), @("WiFiPrinters","false"), @("Contacts","false"),
|
@("BluetoothPrinters","false"), @("WiFiPrinters","false"), @("Contacts","false"),
|
||||||
@@ -59,6 +68,9 @@ $mobileFuncs = @(
|
|||||||
@("DocumentScanning","false"), @("SpeechToText","false"), @("Geofences","false"),
|
@("DocumentScanning","false"), @("SpeechToText","false"), @("Geofences","false"),
|
||||||
@("IncomingShareRequests","false"), @("AllIncomingShareRequestsTypesProcessing","false")
|
@("IncomingShareRequests","false"), @("AllIncomingShareRequestsTypesProcessing","false")
|
||||||
)
|
)
|
||||||
|
# TextToSpeech — возможность мобильного приложения, добавленная форматом 2.21 (8.5),
|
||||||
|
# последней в списке. На младших форматах платформа её не пишет.
|
||||||
|
if ($is221) { $mobileFuncs += ,@("TextToSpeech","false") }
|
||||||
|
|
||||||
$mobileXml = ""
|
$mobileXml = ""
|
||||||
foreach ($mf in $mobileFuncs) {
|
foreach ($mf in $mobileFuncs) {
|
||||||
@@ -68,17 +80,43 @@ foreach ($mf in $mobileFuncs) {
|
|||||||
# --- Synonym XML ---
|
# --- Synonym XML ---
|
||||||
$synonymXml = ""
|
$synonymXml = ""
|
||||||
if ($Synonym) {
|
if ($Synonym) {
|
||||||
$synonymXml = "`r`n`t`t`t`t<v8:item>`r`n`t`t`t`t`t<v8:lang>ru</v8:lang>`r`n`t`t`t`t`t<v8:content>$([System.Security.SecurityElement]::Escape($Synonym))</v8:content>`r`n`t`t`t`t</v8:item>`r`n`t`t`t"
|
$synonymXml = "`r`n`t`t`t`t<v8:item>`r`n`t`t`t`t`t<v8:lang>ru</v8:lang>`r`n`t`t`t`t`t<v8:content>$(Esc-XmlText ($Synonym))</v8:content>`r`n`t`t`t`t</v8:item>`r`n`t`t`t"
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- Optional properties ---
|
# --- Optional properties ---
|
||||||
$vendorXml = if ($Vendor) { [System.Security.SecurityElement]::Escape($Vendor) } else { "" }
|
# Элемент целиком, а не значение внутри пары: при пустом значении Конфигуратор
|
||||||
$versionXml = if ($Version) { [System.Security.SecurityElement]::Escape($Version) } else { "" }
|
# пишет <Vendor/>, а не <Vendor></Vendor>.
|
||||||
|
$vendorEl = if ($Vendor) { "<Vendor>$(Esc-XmlText ($Vendor))</Vendor>" } else { "<Vendor/>" }
|
||||||
|
$versionEl = if ($Version) { "<Version>$(Esc-XmlText ($Version))</Version>" } else { "<Version/>" }
|
||||||
|
|
||||||
|
# --- Свойства и пространство имён формата 2.21 (платформа 8.5) ---
|
||||||
|
# Значения и ПОЗИЦИИ сняты с выгрузки 8.5.1 (debug/fmt221/dump_v851): те же исходники,
|
||||||
|
# выгруженные с 8.3.27 и с 8.5.1, различаются ровно этим. Порядок важен — вставки идут
|
||||||
|
# на своё место, а не в конец.
|
||||||
|
$nl = "`r`n"
|
||||||
|
$f221AuxForms = ""; $f221WindowVariant = ""; $f221OpenVariant = ""; $f221Captions = ""; $f221Migration = ""
|
||||||
|
$palNs = ""
|
||||||
|
if ($is221) {
|
||||||
|
$palNs = ' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette"'
|
||||||
|
# Скобки вокруг -join обязательны: без них `$nl + (массив) -join $nl` разбирается как
|
||||||
|
# `($nl + массив) -join $nl`, массив склеивается пробелами и все теги уезжают в одну строку.
|
||||||
|
$f221AuxForms = $nl + ((@(
|
||||||
|
"<AuxiliaryReportForm/>", "<AuxiliaryReportVariantForm/>", "<AuxiliaryReportSettingsForm/>",
|
||||||
|
"<AuxiliaryDynamicListSettingsForm/>", "<AuxiliaryDataHistoryChangeHistoryForm/>",
|
||||||
|
"<AuxiliaryDataHistoryVersionDataForm/>", "<AuxiliaryDataHistoryVersionDifferencesForm/>",
|
||||||
|
"<AuxiliaryCollaborationSystemUsersChoiceForm/>"
|
||||||
|
) | ForEach-Object { "`t`t`t$_" }) -join $nl)
|
||||||
|
$f221WindowVariant = $nl + "`t`t`t<MainClientApplicationWindowInterfaceVariant>NavigationLeft</MainClientApplicationWindowInterfaceVariant>" +
|
||||||
|
$nl + "`t`t`t<ClientApplicationTheme>Auto</ClientApplicationTheme>"
|
||||||
|
$f221OpenVariant = $nl + "`t`t`t<ClientApplicationWindowsOpenVariant>OpenDataInDialogs</ClientApplicationWindowsOpenVariant>"
|
||||||
|
$f221Captions = $nl + "`t`t`t<Caption/>" + $nl + "`t`t`t<ShortCaption/>"
|
||||||
|
$f221Migration = $nl + "`t`t`t<Version85InterfaceMigrationMode>DontUse</Version85InterfaceMigrationMode>"
|
||||||
|
}
|
||||||
|
|
||||||
# --- Configuration.xml ---
|
# --- Configuration.xml ---
|
||||||
$cfgXml = @"
|
$cfgXml = @"
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="$FormatVersion">
|
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform"$palNs xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="$FormatVersion">
|
||||||
<Configuration uuid="$uuidCfg">
|
<Configuration uuid="$uuidCfg">
|
||||||
<InternalInfo>
|
<InternalInfo>
|
||||||
<xr:ContainedObject>
|
<xr:ContainedObject>
|
||||||
@@ -111,7 +149,7 @@ $cfgXml = @"
|
|||||||
</xr:ContainedObject>
|
</xr:ContainedObject>
|
||||||
</InternalInfo>
|
</InternalInfo>
|
||||||
<Properties>
|
<Properties>
|
||||||
<Name>$([System.Security.SecurityElement]::Escape($Name))</Name>
|
<Name>$(Esc-XmlText ($Name))</Name>
|
||||||
<Synonym>$synonymXml</Synonym>
|
<Synonym>$synonymXml</Synonym>
|
||||||
<Comment/>
|
<Comment/>
|
||||||
<NamePrefix/>
|
<NamePrefix/>
|
||||||
@@ -122,8 +160,8 @@ $cfgXml = @"
|
|||||||
</UsePurposes>
|
</UsePurposes>
|
||||||
<ScriptVariant>Russian</ScriptVariant>
|
<ScriptVariant>Russian</ScriptVariant>
|
||||||
<DefaultRoles/>
|
<DefaultRoles/>
|
||||||
<Vendor>$vendorXml</Vendor>
|
$vendorEl
|
||||||
<Version>$versionXml</Version>
|
$versionEl
|
||||||
<UpdateCatalogAddress/>
|
<UpdateCatalogAddress/>
|
||||||
<IncludeHelpInContents>false</IncludeHelpInContents>
|
<IncludeHelpInContents>false</IncludeHelpInContents>
|
||||||
<UseManagedFormInOrdinaryApplication>false</UseManagedFormInOrdinaryApplication>
|
<UseManagedFormInOrdinaryApplication>false</UseManagedFormInOrdinaryApplication>
|
||||||
@@ -145,15 +183,15 @@ $cfgXml = @"
|
|||||||
<DefaultDataHistoryChangeHistoryForm/>
|
<DefaultDataHistoryChangeHistoryForm/>
|
||||||
<DefaultDataHistoryVersionDataForm/>
|
<DefaultDataHistoryVersionDataForm/>
|
||||||
<DefaultDataHistoryVersionDifferencesForm/>
|
<DefaultDataHistoryVersionDifferencesForm/>
|
||||||
<DefaultCollaborationSystemUsersChoiceForm/>
|
<DefaultCollaborationSystemUsersChoiceForm/>$f221AuxForms
|
||||||
<RequiredMobileApplicationPermissions/>
|
<RequiredMobileApplicationPermissions/>
|
||||||
<UsedMobileApplicationFunctionalities>$mobileXml
|
<UsedMobileApplicationFunctionalities>$mobileXml
|
||||||
</UsedMobileApplicationFunctionalities>
|
</UsedMobileApplicationFunctionalities>
|
||||||
<StandaloneConfigurationRestrictionRoles/>
|
<StandaloneConfigurationRestrictionRoles/>
|
||||||
<MobileApplicationURLs/>
|
<MobileApplicationURLs/>
|
||||||
<AllowedIncomingShareRequestTypes/>
|
<AllowedIncomingShareRequestTypes/>$f221WindowVariant
|
||||||
<MainClientApplicationWindowMode>Normal</MainClientApplicationWindowMode>
|
<MainClientApplicationWindowMode>Normal</MainClientApplicationWindowMode>$f221OpenVariant
|
||||||
<DefaultInterface/>
|
<DefaultInterface/>$f221Captions
|
||||||
<DefaultStyle/>
|
<DefaultStyle/>
|
||||||
<DefaultLanguage>Language.Русский</DefaultLanguage>
|
<DefaultLanguage>Language.Русский</DefaultLanguage>
|
||||||
<BriefInformation/>
|
<BriefInformation/>
|
||||||
@@ -165,7 +203,7 @@ $cfgXml = @"
|
|||||||
<ObjectAutonumerationMode>NotAutoFree</ObjectAutonumerationMode>
|
<ObjectAutonumerationMode>NotAutoFree</ObjectAutonumerationMode>
|
||||||
<ModalityUseMode>DontUse</ModalityUseMode>
|
<ModalityUseMode>DontUse</ModalityUseMode>
|
||||||
<SynchronousPlatformExtensionAndAddInCallUseMode>DontUse</SynchronousPlatformExtensionAndAddInCallUseMode>
|
<SynchronousPlatformExtensionAndAddInCallUseMode>DontUse</SynchronousPlatformExtensionAndAddInCallUseMode>
|
||||||
<InterfaceCompatibilityMode>TaxiEnableVersion8_2</InterfaceCompatibilityMode>
|
<InterfaceCompatibilityMode>TaxiEnableVersion8_2</InterfaceCompatibilityMode>$f221Migration
|
||||||
<DatabaseTablespacesUseMode>DontUse</DatabaseTablespacesUseMode>
|
<DatabaseTablespacesUseMode>DontUse</DatabaseTablespacesUseMode>
|
||||||
<CompatibilityMode>$CompatibilityMode</CompatibilityMode>
|
<CompatibilityMode>$CompatibilityMode</CompatibilityMode>
|
||||||
<DefaultConstantsForm/>
|
<DefaultConstantsForm/>
|
||||||
@@ -180,7 +218,7 @@ $cfgXml = @"
|
|||||||
# --- Languages/Русский.xml ---
|
# --- Languages/Русский.xml ---
|
||||||
$langXml = @"
|
$langXml = @"
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="$FormatVersion">
|
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform"$palNs xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="$FormatVersion">
|
||||||
<Language uuid="$uuidLang">
|
<Language uuid="$uuidLang">
|
||||||
<Properties>
|
<Properties>
|
||||||
<Name>Русский</Name>
|
<Name>Русский</Name>
|
||||||
@@ -240,11 +278,20 @@ if (-not (Test-Path $extDir)) {
|
|||||||
# --- Write files with UTF-8 BOM ---
|
# --- Write files with UTF-8 BOM ---
|
||||||
$enc = New-Object System.Text.UTF8Encoding($true)
|
$enc = New-Object System.Text.UTF8Encoding($true)
|
||||||
|
|
||||||
[System.IO.File]::WriteAllText($cfgFile, $cfgXml, $enc)
|
# XML в каноне выгрузки Конфигуратора: CRLF в разделителях, без перевода в конце.
|
||||||
|
#
|
||||||
|
# Копия этой функции есть в каждом навыке-эмиттере (навыки автономны). Держать
|
||||||
|
# копии одинаковыми — сознательно: разошедшиеся копии сводят на нет весь смысл.
|
||||||
|
function Write-XmlFile([string]$path, [string]$text, $encoding) {
|
||||||
|
$t = ($text -replace "`r`n", "`n") -replace "`n", "`r`n"
|
||||||
|
[System.IO.File]::WriteAllText($path, $t.TrimEnd("`r", "`n"), $encoding)
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-XmlFile $cfgFile $cfgXml $enc
|
||||||
$langFile = Join-Path $langDir "Русский.xml"
|
$langFile = Join-Path $langDir "Русский.xml"
|
||||||
[System.IO.File]::WriteAllText($langFile, $langXml, $enc)
|
Write-XmlFile $langFile $langXml $enc
|
||||||
$caiFile = Join-Path $extDir "ClientApplicationInterface.xml"
|
$caiFile = Join-Path $extDir "ClientApplicationInterface.xml"
|
||||||
[System.IO.File]::WriteAllText($caiFile, $caiXml, $enc)
|
Write-XmlFile $caiFile $caiXml $enc
|
||||||
|
|
||||||
# --- Output ---
|
# --- Output ---
|
||||||
Write-Host "[OK] Создана конфигурация: $Name"
|
Write-Host "[OK] Создана конфигурация: $Name"
|
||||||
|
|||||||
@@ -1,19 +1,55 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# cf-init v1.3 — Create empty 1C configuration scaffold
|
# cf-init v1.11 — Create empty 1C configuration scaffold (+write_xml_file/write_utf8_bom: общий эталон записи)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
"""Generates minimal XML source files for a 1C configuration."""
|
"""Generates minimal XML source files for a 1C configuration."""
|
||||||
import sys, os, argparse, uuid
|
import sys, os, argparse, re, uuid
|
||||||
|
|
||||||
def esc_xml(s):
|
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
|
||||||
return s.replace('&','&').replace('<','<').replace('>','>').replace('"','"')
|
# регистр не различают, в 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)
|
||||||
|
|
||||||
|
|
||||||
|
def esc_xml_text(s):
|
||||||
|
# Эскейп ТЕКСТА элемента: только & < > — кавычку и апостроф платформа держит сырыми.
|
||||||
|
return s.replace('&', '&').replace('<', '<').replace('>', '>')
|
||||||
|
|
||||||
def new_uuid():
|
def new_uuid():
|
||||||
return str(uuid.uuid4())
|
return str(uuid.uuid4())
|
||||||
|
|
||||||
def write_utf8_bom(path, content):
|
def write_utf8_bom(path, content):
|
||||||
|
# newline='' — без трансляции: иначе текстовый режим Python дал бы CRLF на Windows
|
||||||
|
# и LF на macOS, то есть вывод навыка зависел бы от ОС.
|
||||||
with open(path, 'w', encoding='utf-8-sig', newline='') as f:
|
with open(path, 'w', encoding='utf-8-sig', newline='') as f:
|
||||||
f.write(content)
|
f.write(content)
|
||||||
|
|
||||||
|
|
||||||
|
def write_xml_file(path, content):
|
||||||
|
"""XML в каноне выгрузки Конфигуратора: CRLF в разделителях, без перевода в конце.
|
||||||
|
|
||||||
|
Копия этой функции есть в каждом навыке-эмиттере (навыки автономны). Держать
|
||||||
|
копии одинаковыми — сознательно: разошедшиеся копии сводят на нет весь смысл.
|
||||||
|
"""
|
||||||
|
text = content.replace('\r\n', '\n').replace('\n', '\r\n').rstrip('\r\n')
|
||||||
|
write_utf8_bom(path, text)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
sys.stdout.reconfigure(encoding="utf-8")
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
sys.stderr.reconfigure(encoding="utf-8")
|
||||||
@@ -25,9 +61,11 @@ def main():
|
|||||||
parser.add_argument('-Vendor', dest='Vendor', default='')
|
parser.add_argument('-Vendor', dest='Vendor', default='')
|
||||||
parser.add_argument('-CompatibilityMode', dest='CompatibilityMode', default='Version8_3_24')
|
parser.add_argument('-CompatibilityMode', dest='CompatibilityMode', default='Version8_3_24')
|
||||||
# Версия формата выгрузки (MDClasses) — её задаёт ПЛАТФОРМА, а не режим совместимости:
|
# Версия формата выгрузки (MDClasses) — её задаёт ПЛАТФОРМА, а не режим совместимости:
|
||||||
# 8.3.24 пишет 2.17, 8.3.27 — 2.20. Дефолт консервативный: 2.17 читается всеми платформами.
|
# 8.3.20-8.3.24 пишут 2.17, 8.3.25 — 2.18, 8.3.26 — 2.19, 8.3.27 — 2.20.
|
||||||
parser.add_argument('-FormatVersion', dest='FormatVersion', default='2.17', choices=['2.17', '2.20', '2.21'])
|
# Дефолт консервативный: 2.17 читается всеми платформами.
|
||||||
args = parser.parse_args()
|
parser.add_argument('-FormatVersion', dest='FormatVersion', default='2.17',
|
||||||
|
choices=['2.17', '2.18', '2.19', '2.20', '2.21'])
|
||||||
|
args = ci_parse_args(parser)
|
||||||
|
|
||||||
name = args.Name
|
name = args.Name
|
||||||
synonym = args.Synonym if args.Synonym else name
|
synonym = args.Synonym if args.Synonym else name
|
||||||
@@ -52,6 +90,10 @@ def main():
|
|||||||
co = [new_uuid() for _ in range(7)]
|
co = [new_uuid() for _ in range(7)]
|
||||||
|
|
||||||
# --- Mobile functionalities ---
|
# --- Mobile functionalities ---
|
||||||
|
# Версия формата как число — по ней ниже включаются вставки 2.21.
|
||||||
|
_fm = re.match(r'^(\d+)\.(\d+)$', args.FormatVersion)
|
||||||
|
is_221 = bool(_fm) and int(_fm.group(1)) * 100 + int(_fm.group(2)) >= 221
|
||||||
|
|
||||||
mobile_funcs = [
|
mobile_funcs = [
|
||||||
("Biometrics","true"), ("Location","false"), ("BackgroundLocation","false"),
|
("Biometrics","true"), ("Location","false"), ("BackgroundLocation","false"),
|
||||||
("BluetoothPrinters","false"), ("WiFiPrinters","false"), ("Contacts","false"),
|
("BluetoothPrinters","false"), ("WiFiPrinters","false"), ("Contacts","false"),
|
||||||
@@ -68,6 +110,10 @@ def main():
|
|||||||
("DocumentScanning","false"), ("SpeechToText","false"), ("Geofences","false"),
|
("DocumentScanning","false"), ("SpeechToText","false"), ("Geofences","false"),
|
||||||
("IncomingShareRequests","false"), ("AllIncomingShareRequestsTypesProcessing","false"),
|
("IncomingShareRequests","false"), ("AllIncomingShareRequestsTypesProcessing","false"),
|
||||||
]
|
]
|
||||||
|
# TextToSpeech — возможность мобильного приложения, добавленная форматом 2.21 (8.5),
|
||||||
|
# последней в списке. На младших форматах платформа её не пишет.
|
||||||
|
if is_221:
|
||||||
|
mobile_funcs.append(("TextToSpeech", "false"))
|
||||||
|
|
||||||
mobile_xml = ""
|
mobile_xml = ""
|
||||||
for func_name, func_use in mobile_funcs:
|
for func_name, func_use in mobile_funcs:
|
||||||
@@ -76,10 +122,12 @@ def main():
|
|||||||
# --- Synonym XML ---
|
# --- Synonym XML ---
|
||||||
synonym_xml = ""
|
synonym_xml = ""
|
||||||
if synonym:
|
if synonym:
|
||||||
synonym_xml = f"\r\n\t\t\t\t<v8:item>\r\n\t\t\t\t\t<v8:lang>ru</v8:lang>\r\n\t\t\t\t\t<v8:content>{esc_xml(synonym)}</v8:content>\r\n\t\t\t\t</v8:item>\r\n\t\t\t"
|
synonym_xml = f"\r\n\t\t\t\t<v8:item>\r\n\t\t\t\t\t<v8:lang>ru</v8:lang>\r\n\t\t\t\t\t<v8:content>{esc_xml_text(synonym)}</v8:content>\r\n\t\t\t\t</v8:item>\r\n\t\t\t"
|
||||||
|
|
||||||
vendor_xml = esc_xml(vendor) if vendor else ""
|
# Элемент целиком, а не значение внутри пары: при пустом значении Конфигуратор
|
||||||
version_xml = esc_xml(version) if version else ""
|
# пишет <Vendor/>, а не <Vendor></Vendor>.
|
||||||
|
vendor_el = f"<Vendor>{esc_xml_text(vendor)}</Vendor>" if vendor else "<Vendor/>"
|
||||||
|
version_el = f"<Version>{esc_xml_text(version)}</Version>" if version else "<Version/>"
|
||||||
|
|
||||||
class_ids = [
|
class_ids = [
|
||||||
"9cd510cd-abfc-11d4-9434-004095e12fc7",
|
"9cd510cd-abfc-11d4-9434-004095e12fc7",
|
||||||
@@ -91,6 +139,28 @@ def main():
|
|||||||
"fb282519-d103-4dd3-bc12-cb271d631dfc",
|
"fb282519-d103-4dd3-bc12-cb271d631dfc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Свойства и пространство имён формата 2.21 (платформа 8.5). Значения и ПОЗИЦИИ сняты
|
||||||
|
# с выгрузки 8.5.1 (debug/fmt221/dump_v851): те же исходники, выгруженные с 8.3.27 и
|
||||||
|
# с 8.5.1, различаются ровно этим. Порядок важен — вставки идут на своё место.
|
||||||
|
pal_ns = ""
|
||||||
|
f221_aux_forms = f221_window_variant = f221_open_variant = f221_captions = f221_migration = ""
|
||||||
|
if is_221:
|
||||||
|
pal_ns = ' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette"'
|
||||||
|
f221_aux_forms = "\r\n" + "\r\n".join(
|
||||||
|
f"\t\t\t{t}" for t in (
|
||||||
|
"<AuxiliaryReportForm/>", "<AuxiliaryReportVariantForm/>", "<AuxiliaryReportSettingsForm/>",
|
||||||
|
"<AuxiliaryDynamicListSettingsForm/>", "<AuxiliaryDataHistoryChangeHistoryForm/>",
|
||||||
|
"<AuxiliaryDataHistoryVersionDataForm/>", "<AuxiliaryDataHistoryVersionDifferencesForm/>",
|
||||||
|
"<AuxiliaryCollaborationSystemUsersChoiceForm/>"))
|
||||||
|
f221_window_variant = ("\r\n\t\t\t<MainClientApplicationWindowInterfaceVariant>NavigationLeft"
|
||||||
|
"</MainClientApplicationWindowInterfaceVariant>"
|
||||||
|
"\r\n\t\t\t<ClientApplicationTheme>Auto</ClientApplicationTheme>")
|
||||||
|
f221_open_variant = ("\r\n\t\t\t<ClientApplicationWindowsOpenVariant>OpenDataInDialogs"
|
||||||
|
"</ClientApplicationWindowsOpenVariant>")
|
||||||
|
f221_captions = "\r\n\t\t\t<Caption/>\r\n\t\t\t<ShortCaption/>"
|
||||||
|
f221_migration = ("\r\n\t\t\t<Version85InterfaceMigrationMode>DontUse"
|
||||||
|
"</Version85InterfaceMigrationMode>")
|
||||||
|
|
||||||
contained_objects = ""
|
contained_objects = ""
|
||||||
for i in range(7):
|
for i in range(7):
|
||||||
contained_objects += f"""\t\t\t<xr:ContainedObject>
|
contained_objects += f"""\t\t\t<xr:ContainedObject>
|
||||||
@@ -99,12 +169,12 @@ def main():
|
|||||||
\t\t\t</xr:ContainedObject>\n"""
|
\t\t\t</xr:ContainedObject>\n"""
|
||||||
|
|
||||||
cfg_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
|
cfg_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="{args.FormatVersion}">
|
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform"{pal_ns} xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="{args.FormatVersion}">
|
||||||
\t<Configuration uuid="{uuid_cfg}">
|
\t<Configuration uuid="{uuid_cfg}">
|
||||||
\t\t<InternalInfo>
|
\t\t<InternalInfo>
|
||||||
{contained_objects}\t\t</InternalInfo>
|
{contained_objects}\t\t</InternalInfo>
|
||||||
\t\t<Properties>
|
\t\t<Properties>
|
||||||
\t\t\t<Name>{esc_xml(name)}</Name>
|
\t\t\t<Name>{esc_xml_text(name)}</Name>
|
||||||
\t\t\t<Synonym>{synonym_xml}</Synonym>
|
\t\t\t<Synonym>{synonym_xml}</Synonym>
|
||||||
\t\t\t<Comment/>
|
\t\t\t<Comment/>
|
||||||
\t\t\t<NamePrefix/>
|
\t\t\t<NamePrefix/>
|
||||||
@@ -115,8 +185,8 @@ def main():
|
|||||||
\t\t\t</UsePurposes>
|
\t\t\t</UsePurposes>
|
||||||
\t\t\t<ScriptVariant>Russian</ScriptVariant>
|
\t\t\t<ScriptVariant>Russian</ScriptVariant>
|
||||||
\t\t\t<DefaultRoles/>
|
\t\t\t<DefaultRoles/>
|
||||||
\t\t\t<Vendor>{vendor_xml}</Vendor>
|
\t\t\t{vendor_el}
|
||||||
\t\t\t<Version>{version_xml}</Version>
|
\t\t\t{version_el}
|
||||||
\t\t\t<UpdateCatalogAddress/>
|
\t\t\t<UpdateCatalogAddress/>
|
||||||
\t\t\t<IncludeHelpInContents>false</IncludeHelpInContents>
|
\t\t\t<IncludeHelpInContents>false</IncludeHelpInContents>
|
||||||
\t\t\t<UseManagedFormInOrdinaryApplication>false</UseManagedFormInOrdinaryApplication>
|
\t\t\t<UseManagedFormInOrdinaryApplication>false</UseManagedFormInOrdinaryApplication>
|
||||||
@@ -138,15 +208,15 @@ def main():
|
|||||||
\t\t\t<DefaultDataHistoryChangeHistoryForm/>
|
\t\t\t<DefaultDataHistoryChangeHistoryForm/>
|
||||||
\t\t\t<DefaultDataHistoryVersionDataForm/>
|
\t\t\t<DefaultDataHistoryVersionDataForm/>
|
||||||
\t\t\t<DefaultDataHistoryVersionDifferencesForm/>
|
\t\t\t<DefaultDataHistoryVersionDifferencesForm/>
|
||||||
\t\t\t<DefaultCollaborationSystemUsersChoiceForm/>
|
\t\t\t<DefaultCollaborationSystemUsersChoiceForm/>{f221_aux_forms}
|
||||||
\t\t\t<RequiredMobileApplicationPermissions/>
|
\t\t\t<RequiredMobileApplicationPermissions/>
|
||||||
\t\t\t<UsedMobileApplicationFunctionalities>{mobile_xml}
|
\t\t\t<UsedMobileApplicationFunctionalities>{mobile_xml}
|
||||||
\t\t\t</UsedMobileApplicationFunctionalities>
|
\t\t\t</UsedMobileApplicationFunctionalities>
|
||||||
\t\t\t<StandaloneConfigurationRestrictionRoles/>
|
\t\t\t<StandaloneConfigurationRestrictionRoles/>
|
||||||
\t\t\t<MobileApplicationURLs/>
|
\t\t\t<MobileApplicationURLs/>
|
||||||
\t\t\t<AllowedIncomingShareRequestTypes/>
|
\t\t\t<AllowedIncomingShareRequestTypes/>{f221_window_variant}
|
||||||
\t\t\t<MainClientApplicationWindowMode>Normal</MainClientApplicationWindowMode>
|
\t\t\t<MainClientApplicationWindowMode>Normal</MainClientApplicationWindowMode>{f221_open_variant}
|
||||||
\t\t\t<DefaultInterface/>
|
\t\t\t<DefaultInterface/>{f221_captions}
|
||||||
\t\t\t<DefaultStyle/>
|
\t\t\t<DefaultStyle/>
|
||||||
\t\t\t<DefaultLanguage>Language.Русский</DefaultLanguage>
|
\t\t\t<DefaultLanguage>Language.Русский</DefaultLanguage>
|
||||||
\t\t\t<BriefInformation/>
|
\t\t\t<BriefInformation/>
|
||||||
@@ -158,7 +228,7 @@ def main():
|
|||||||
\t\t\t<ObjectAutonumerationMode>NotAutoFree</ObjectAutonumerationMode>
|
\t\t\t<ObjectAutonumerationMode>NotAutoFree</ObjectAutonumerationMode>
|
||||||
\t\t\t<ModalityUseMode>DontUse</ModalityUseMode>
|
\t\t\t<ModalityUseMode>DontUse</ModalityUseMode>
|
||||||
\t\t\t<SynchronousPlatformExtensionAndAddInCallUseMode>DontUse</SynchronousPlatformExtensionAndAddInCallUseMode>
|
\t\t\t<SynchronousPlatformExtensionAndAddInCallUseMode>DontUse</SynchronousPlatformExtensionAndAddInCallUseMode>
|
||||||
\t\t\t<InterfaceCompatibilityMode>TaxiEnableVersion8_2</InterfaceCompatibilityMode>
|
\t\t\t<InterfaceCompatibilityMode>TaxiEnableVersion8_2</InterfaceCompatibilityMode>{f221_migration}
|
||||||
\t\t\t<DatabaseTablespacesUseMode>DontUse</DatabaseTablespacesUseMode>
|
\t\t\t<DatabaseTablespacesUseMode>DontUse</DatabaseTablespacesUseMode>
|
||||||
\t\t\t<CompatibilityMode>{compat}</CompatibilityMode>
|
\t\t\t<CompatibilityMode>{compat}</CompatibilityMode>
|
||||||
\t\t\t<DefaultConstantsForm/>
|
\t\t\t<DefaultConstantsForm/>
|
||||||
@@ -171,7 +241,7 @@ def main():
|
|||||||
|
|
||||||
# --- Languages/Русский.xml ---
|
# --- Languages/Русский.xml ---
|
||||||
lang_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
|
lang_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="{args.FormatVersion}">
|
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform"{pal_ns} xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="{args.FormatVersion}">
|
||||||
\t<Language uuid="{uuid_lang}">
|
\t<Language uuid="{uuid_lang}">
|
||||||
\t\t<Properties>
|
\t\t<Properties>
|
||||||
\t\t\t<Name>Русский</Name>
|
\t\t\t<Name>Русский</Name>
|
||||||
@@ -220,11 +290,11 @@ def main():
|
|||||||
os.makedirs(ext_dir, exist_ok=True)
|
os.makedirs(ext_dir, exist_ok=True)
|
||||||
|
|
||||||
# --- Write files ---
|
# --- Write files ---
|
||||||
write_utf8_bom(cfg_file, cfg_xml)
|
write_xml_file(cfg_file, cfg_xml)
|
||||||
lang_file = os.path.join(lang_dir, "Русский.xml")
|
lang_file = os.path.join(lang_dir, "Русский.xml")
|
||||||
write_utf8_bom(lang_file, lang_xml)
|
write_xml_file(lang_file, lang_xml)
|
||||||
cai_file = os.path.join(ext_dir, "ClientApplicationInterface.xml")
|
cai_file = os.path.join(ext_dir, "ClientApplicationInterface.xml")
|
||||||
write_utf8_bom(cai_file, cai_xml)
|
write_xml_file(cai_file, cai_xml)
|
||||||
|
|
||||||
print(f"[OK] Создана конфигурация: {name}")
|
print(f"[OK] Создана конфигурация: {name}")
|
||||||
print(f" Каталог: {output_dir}")
|
print(f" Каталог: {output_dir}")
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# cf-validate v1.4 — Validate 1C configuration root structure
|
# cf-validate v1.6 — Validate 1C configuration root structure
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory)]
|
||||||
@@ -205,8 +205,9 @@ if ($root.NamespaceURI -ne $expectedNs) {
|
|||||||
$version = $root.GetAttribute("version")
|
$version = $root.GetAttribute("version")
|
||||||
if (-not $version) {
|
if (-not $version) {
|
||||||
Report-Warn "1. Missing version attribute on MetaDataObject"
|
Report-Warn "1. Missing version attribute on MetaDataObject"
|
||||||
} elseif ($version -ne "2.17" -and $version -ne "2.20" -and $version -ne "2.21") {
|
} elseif ($version -notin @("2.17", "2.18", "2.19", "2.20", "2.21")) {
|
||||||
Report-Warn "1. Unusual version '$version' (expected 2.17, 2.20 or 2.21)"
|
# Лестница версий формата: 2.17 (8.3.20-8.3.24), 2.18 (8.3.25), 2.19 (8.3.26), 2.20 (8.3.27).
|
||||||
|
Report-Warn "1. Unusual version '$version' (expected 2.17-2.20 or 2.21)"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Must have Configuration child
|
# Must have Configuration child
|
||||||
|
|||||||
@@ -1,10 +1,32 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# cf-validate v1.4 — Validate 1C configuration XML structure
|
# cf-validate v1.6 — Validate 1C configuration XML structure
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
"""Validates Configuration.xml: root structure, InternalInfo, properties, ChildObjects, languages."""
|
"""Validates Configuration.xml: root structure, InternalInfo, properties, ChildObjects, languages."""
|
||||||
import sys, os, argparse, re
|
import sys, os, argparse, re
|
||||||
from lxml import etree
|
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)
|
||||||
|
|
||||||
|
|
||||||
NS = {
|
NS = {
|
||||||
'md': 'http://v8.1c.ru/8.3/MDClasses',
|
'md': 'http://v8.1c.ru/8.3/MDClasses',
|
||||||
'v8': 'http://v8.1c.ru/8.1/data/core',
|
'v8': 'http://v8.1c.ru/8.1/data/core',
|
||||||
@@ -170,7 +192,7 @@ def main():
|
|||||||
parser.add_argument('-Detailed', action='store_true')
|
parser.add_argument('-Detailed', action='store_true')
|
||||||
parser.add_argument('-MaxErrors', dest='MaxErrors', type=int, default=30)
|
parser.add_argument('-MaxErrors', dest='MaxErrors', type=int, default=30)
|
||||||
parser.add_argument('-OutFile', dest='OutFile', default='')
|
parser.add_argument('-OutFile', dest='OutFile', default='')
|
||||||
args = parser.parse_args()
|
args = ci_parse_args(parser)
|
||||||
|
|
||||||
config_path = args.ConfigPath
|
config_path = args.ConfigPath
|
||||||
max_errors = args.MaxErrors
|
max_errors = args.MaxErrors
|
||||||
@@ -232,8 +254,9 @@ def main():
|
|||||||
version = root.get('version', '')
|
version = root.get('version', '')
|
||||||
if not version:
|
if not version:
|
||||||
r.warn('1. Missing version attribute on MetaDataObject')
|
r.warn('1. Missing version attribute on MetaDataObject')
|
||||||
elif version not in ('2.17', '2.20', '2.21'):
|
elif version not in ('2.17', '2.18', '2.19', '2.20', '2.21'):
|
||||||
r.warn(f"1. Unusual version '{version}' (expected 2.17, 2.20 or 2.21)")
|
# Лестница версий формата: 2.17 (8.3.20-8.3.24), 2.18 (8.3.25), 2.19 (8.3.26), 2.20 (8.3.27).
|
||||||
|
r.warn(f"1. Unusual version '{version}' (expected 2.17-2.20 or 2.21)")
|
||||||
|
|
||||||
# Must have Configuration child
|
# Must have Configuration child
|
||||||
cfg_node = None
|
cfg_node = None
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ allowed-tools:
|
|||||||
- `Enum.ВидыОплат` — перечисление
|
- `Enum.ВидыОплат` — перечисление
|
||||||
- `Catalog.Контрагенты.Form.ФормаЭлемента` — форма объекта (заимствование формы)
|
- `Catalog.Контрагенты.Form.ФормаЭлемента` — форма объекта (заимствование формы)
|
||||||
- `Catalog.X ;; CommonModule.Y ;; Enum.Z` — несколько объектов
|
- `Catalog.X ;; CommonModule.Y ;; Enum.Z` — несколько объектов
|
||||||
Поддерживаются все 44 типа объектов конфигурации.
|
|
||||||
|
|
||||||
### Заимствование форм
|
### Заимствование форм
|
||||||
|
|
||||||
@@ -71,31 +70,33 @@ allowed-tools:
|
|||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-borrow.ps1" -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты"
|
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-borrow.ps1" -ExtensionPath src\cfe\extname -ConfigPath src\cf -Object "Catalog.Контрагенты"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Примеры
|
## Примеры
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Заимствовать один объект
|
# Заимствовать один объект
|
||||||
... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты"
|
... -ExtensionPath src\cfe\extname -ConfigPath src\cf -Object "Catalog.Контрагенты"
|
||||||
|
|
||||||
# Заимствовать форму (автоматически заимствует родительский объект)
|
# Заимствовать форму (автоматически заимствует родительский объект)
|
||||||
... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты.Form.ФормаЭлемента"
|
... -ExtensionPath src\cfe\extname -ConfigPath src\cf -Object "Catalog.Контрагенты.Form.ФормаЭлемента"
|
||||||
|
|
||||||
# Несколько объектов за раз
|
# Несколько объектов за раз
|
||||||
... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты ;; CommonModule.ОбщийМодуль ;; Enum.ВидыОплат"
|
... -ExtensionPath src\cfe\extname -ConfigPath src\cf -Object "Catalog.Контрагенты ;; CommonModule.ОбщийМодуль ;; Enum.ВидыОплат"
|
||||||
|
|
||||||
# Заимствовать форму с основным реквизитом (реквизиты по DataPath формы)
|
# Заимствовать форму с основным реквизитом (реквизиты по DataPath формы)
|
||||||
... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Номенклатура.Form.ФормаЭлемента" -BorrowMainAttribute
|
... -ExtensionPath src\cfe\extname -ConfigPath src\cf -Object "Catalog.Номенклатура.Form.ФормаЭлемента" -BorrowMainAttribute
|
||||||
|
|
||||||
# Заимствовать форму с ВСЕМИ реквизитами объекта
|
# Заимствовать форму с ВСЕМИ реквизитами объекта
|
||||||
... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Номенклатура.Form.ФормаЭлемента" -BorrowMainAttribute All
|
... -ExtensionPath src\cfe\extname -ConfigPath src\cf -Object "Catalog.Номенклатура.Form.ФормаЭлемента" -BorrowMainAttribute All
|
||||||
```
|
```
|
||||||
|
|
||||||
## Верификация
|
## Верификация
|
||||||
|
|
||||||
```
|
```
|
||||||
/cfe-validate <ExtensionPath>
|
/cfe-validate <ExtensionPath> -ConfigPath <ConfigPath>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Конфигурацию-источник передавай и валидатору: заимствованные формы он проверяет по ней.
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# cfe-borrow v1.9 — Borrow objects from configuration into extension (CFE)
|
# cfe-borrow v1.25 — Borrow objects from configuration into extension (CFE) (не переносить FoldersOnTop — платформа его не хранит)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)][string]$ExtensionPath,
|
[Parameter(Mandatory)][string]$ExtensionPath,
|
||||||
@@ -38,6 +38,34 @@ function Strip-FormBindings {
|
|||||||
return $xml
|
return $xml
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Ссылки параметров выбора (<ChoiceParameterLinks>/<xr:Link>) — привязка особого рода: путь лежит
|
||||||
|
# в <xr:DataPath> и обычным стриппингом не снимается. Когда основной реквизит не заимствован,
|
||||||
|
# текстовый «Объект.X» в расширении не разрешается («Неверный путь к полю»), поэтому Конфигуратор
|
||||||
|
# переписывает его в непрозрачную форму «1/0:<uuid реквизита объекта>» — ссылка остаётся рабочей.
|
||||||
|
# Реквизит, которого в источнике нет, недоступен и по uuid: такую связь вырезаем целиком.
|
||||||
|
# Путь всегда односегментный (корпусная проверка: 285 из 285 у УТ), глубже — тоже вырезаем.
|
||||||
|
function Rewrite-ChoiceParameterLinks {
|
||||||
|
param([string]$xml, $attrUuids)
|
||||||
|
|
||||||
|
if ($xml -notmatch '<ChoiceParameterLinks>') { return $xml }
|
||||||
|
|
||||||
|
$xml = [regex]::Replace($xml, '(?s)\s*<xr:Link>.*?</xr:Link>', {
|
||||||
|
param($m)
|
||||||
|
$link = $m.Value
|
||||||
|
$dp = [regex]::Match($link, '<xr:DataPath[^>]*>Объект\.([^<]+)</xr:DataPath>')
|
||||||
|
if (-not $dp.Success) { return $link }
|
||||||
|
$attrName = $dp.Groups[1].Value
|
||||||
|
if ($attrUuids.ContainsKey($attrName)) {
|
||||||
|
return [regex]::Replace($link, '(<xr:DataPath[^>]*>)Объект\.[^<]+(</xr:DataPath>)', "`${1}1/0:$($attrUuids[$attrName])`${2}")
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
})
|
||||||
|
|
||||||
|
# Опустевший контейнер платформе не нужен
|
||||||
|
$xml = [regex]::Replace($xml, '(?s)\s*<ChoiceParameterLinks>\s*</ChoiceParameterLinks>', '')
|
||||||
|
return $xml
|
||||||
|
}
|
||||||
|
|
||||||
# --- 1. Resolve paths ---
|
# --- 1. Resolve paths ---
|
||||||
if (-not [System.IO.Path]::IsPathRooted($ExtensionPath)) {
|
if (-not [System.IO.Path]::IsPathRooted($ExtensionPath)) {
|
||||||
$ExtensionPath = Join-Path (Get-Location).Path $ExtensionPath
|
$ExtensionPath = Join-Path (Get-Location).Path $ExtensionPath
|
||||||
@@ -125,7 +153,7 @@ $childTypeDirMap = @{
|
|||||||
"Sequence"="Sequences"; "IntegrationService"="IntegrationServices"
|
"Sequence"="Sequences"; "IntegrationService"="IntegrationServices"
|
||||||
"XDTOPackage"="XDTOPackages"; "WebService"="WebServices"
|
"XDTOPackage"="XDTOPackages"; "WebService"="WebServices"
|
||||||
"HTTPService"="HTTPServices"; "WSReference"="WSReferences"
|
"HTTPService"="HTTPServices"; "WSReference"="WSReferences"
|
||||||
"CommonAttribute"="CommonAttributes"; "Style"="Styles"
|
"CommonAttribute"="CommonAttributes"; "Style"="Styles"; "Bot"="Bots"; "Language"="Languages"
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- 4b. Russian synonym → English type ---
|
# --- 4b. Russian synonym → English type ---
|
||||||
@@ -153,7 +181,7 @@ $synonymMap = @{
|
|||||||
$script:typeOrder = @(
|
$script:typeOrder = @(
|
||||||
"Language","Subsystem","StyleItem","Style",
|
"Language","Subsystem","StyleItem","Style",
|
||||||
"CommonPicture","SessionParameter","Role","CommonTemplate",
|
"CommonPicture","SessionParameter","Role","CommonTemplate",
|
||||||
"FilterCriterion","CommonModule","CommonAttribute","ExchangePlan",
|
"FilterCriterion","CommonModule","Bot","CommonAttribute","ExchangePlan",
|
||||||
"XDTOPackage","WebService","HTTPService","WSReference",
|
"XDTOPackage","WebService","HTTPService","WSReference",
|
||||||
"EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption",
|
"EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption",
|
||||||
"FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup",
|
"FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup",
|
||||||
@@ -209,7 +237,8 @@ $script:generatedTypes = @{
|
|||||||
@{ prefix = "AccumulationRegisterRecordKey"; category = "RecordKey" }
|
@{ prefix = "AccumulationRegisterRecordKey"; category = "RecordKey" }
|
||||||
)
|
)
|
||||||
"AccountingRegister" = @(
|
"AccountingRegister" = @(
|
||||||
@{ prefix = "AccountingRegisterRecord"; category = "Record" }
|
@{ prefix = "AccountingRegisterRecord"; category = "Record" }
|
||||||
|
@{ prefix = "AccountingRegisterExtDimensions"; category = "ExtDimensions" }
|
||||||
@{ prefix = "AccountingRegisterManager"; category = "Manager" }
|
@{ prefix = "AccountingRegisterManager"; category = "Manager" }
|
||||||
@{ prefix = "AccountingRegisterSelection"; category = "Selection" }
|
@{ prefix = "AccountingRegisterSelection"; category = "Selection" }
|
||||||
@{ prefix = "AccountingRegisterList"; category = "List" }
|
@{ prefix = "AccountingRegisterList"; category = "List" }
|
||||||
@@ -223,6 +252,7 @@ $script:generatedTypes = @{
|
|||||||
@{ prefix = "CalculationRegisterList"; category = "List" }
|
@{ prefix = "CalculationRegisterList"; category = "List" }
|
||||||
@{ prefix = "CalculationRegisterRecordSet"; category = "RecordSet" }
|
@{ prefix = "CalculationRegisterRecordSet"; category = "RecordSet" }
|
||||||
@{ prefix = "CalculationRegisterRecordKey"; category = "RecordKey" }
|
@{ prefix = "CalculationRegisterRecordKey"; category = "RecordKey" }
|
||||||
|
@{ prefix = "RecalculationsManager"; category = "Recalcs" }
|
||||||
)
|
)
|
||||||
"ChartOfAccounts" = @(
|
"ChartOfAccounts" = @(
|
||||||
@{ prefix = "ChartOfAccountsObject"; category = "Object" }
|
@{ prefix = "ChartOfAccountsObject"; category = "Object" }
|
||||||
@@ -230,12 +260,15 @@ $script:generatedTypes = @{
|
|||||||
@{ prefix = "ChartOfAccountsSelection"; category = "Selection" }
|
@{ prefix = "ChartOfAccountsSelection"; category = "Selection" }
|
||||||
@{ prefix = "ChartOfAccountsList"; category = "List" }
|
@{ prefix = "ChartOfAccountsList"; category = "List" }
|
||||||
@{ prefix = "ChartOfAccountsManager"; category = "Manager" }
|
@{ prefix = "ChartOfAccountsManager"; category = "Manager" }
|
||||||
|
@{ prefix = "ChartOfAccountsExtDimensionTypes"; category = "ExtDimensionTypes" }
|
||||||
|
@{ prefix = "ChartOfAccountsExtDimensionTypesRow"; category = "ExtDimensionTypesRow" }
|
||||||
)
|
)
|
||||||
"ChartOfCharacteristicTypes" = @(
|
"ChartOfCharacteristicTypes" = @(
|
||||||
@{ prefix = "ChartOfCharacteristicTypesObject"; category = "Object" }
|
@{ prefix = "ChartOfCharacteristicTypesObject"; category = "Object" }
|
||||||
@{ prefix = "ChartOfCharacteristicTypesRef"; category = "Ref" }
|
@{ prefix = "ChartOfCharacteristicTypesRef"; category = "Ref" }
|
||||||
@{ prefix = "ChartOfCharacteristicTypesSelection"; category = "Selection" }
|
@{ prefix = "ChartOfCharacteristicTypesSelection"; category = "Selection" }
|
||||||
@{ prefix = "ChartOfCharacteristicTypesList"; category = "List" }
|
@{ prefix = "ChartOfCharacteristicTypesList"; category = "List" }
|
||||||
|
@{ prefix = "Characteristic"; category = "Characteristic" }
|
||||||
@{ prefix = "ChartOfCharacteristicTypesManager"; category = "Manager" }
|
@{ prefix = "ChartOfCharacteristicTypesManager"; category = "Manager" }
|
||||||
)
|
)
|
||||||
"ChartOfCalculationTypes" = @(
|
"ChartOfCalculationTypes" = @(
|
||||||
@@ -245,8 +278,11 @@ $script:generatedTypes = @{
|
|||||||
@{ prefix = "ChartOfCalculationTypesList"; category = "List" }
|
@{ prefix = "ChartOfCalculationTypesList"; category = "List" }
|
||||||
@{ prefix = "ChartOfCalculationTypesManager"; category = "Manager" }
|
@{ prefix = "ChartOfCalculationTypesManager"; category = "Manager" }
|
||||||
@{ prefix = "DisplacingCalculationTypes"; category = "DisplacingCalculationTypes" }
|
@{ prefix = "DisplacingCalculationTypes"; category = "DisplacingCalculationTypes" }
|
||||||
|
@{ prefix = "DisplacingCalculationTypesRow"; category = "DisplacingCalculationTypesRow" }
|
||||||
@{ prefix = "BaseCalculationTypes"; category = "BaseCalculationTypes" }
|
@{ prefix = "BaseCalculationTypes"; category = "BaseCalculationTypes" }
|
||||||
|
@{ prefix = "BaseCalculationTypesRow"; category = "BaseCalculationTypesRow" }
|
||||||
@{ prefix = "LeadingCalculationTypes"; category = "LeadingCalculationTypes" }
|
@{ prefix = "LeadingCalculationTypes"; category = "LeadingCalculationTypes" }
|
||||||
|
@{ prefix = "LeadingCalculationTypesRow"; category = "LeadingCalculationTypesRow" }
|
||||||
)
|
)
|
||||||
"BusinessProcess" = @(
|
"BusinessProcess" = @(
|
||||||
@{ prefix = "BusinessProcessObject"; category = "Object" }
|
@{ prefix = "BusinessProcessObject"; category = "Object" }
|
||||||
@@ -254,6 +290,7 @@ $script:generatedTypes = @{
|
|||||||
@{ prefix = "BusinessProcessSelection"; category = "Selection" }
|
@{ prefix = "BusinessProcessSelection"; category = "Selection" }
|
||||||
@{ prefix = "BusinessProcessList"; category = "List" }
|
@{ prefix = "BusinessProcessList"; category = "List" }
|
||||||
@{ prefix = "BusinessProcessManager"; category = "Manager" }
|
@{ prefix = "BusinessProcessManager"; category = "Manager" }
|
||||||
|
@{ prefix = "BusinessProcessRoutePointRef"; category = "RoutePointRef" }
|
||||||
)
|
)
|
||||||
"Task" = @(
|
"Task" = @(
|
||||||
@{ prefix = "TaskObject"; category = "Object" }
|
@{ prefix = "TaskObject"; category = "Object" }
|
||||||
@@ -285,14 +322,36 @@ $script:generatedTypes = @{
|
|||||||
"DefinedType" = @(
|
"DefinedType" = @(
|
||||||
@{ prefix = "DefinedType"; category = "DefinedType" }
|
@{ prefix = "DefinedType"; category = "DefinedType" }
|
||||||
)
|
)
|
||||||
|
"Sequence" = @(
|
||||||
|
@{ prefix = "SequenceRecord"; category = "Record" }
|
||||||
|
@{ prefix = "SequenceManager"; category = "Manager" }
|
||||||
|
@{ prefix = "SequenceRecordSet"; category = "RecordSet" }
|
||||||
|
)
|
||||||
|
"FilterCriterion" = @(
|
||||||
|
@{ prefix = "FilterCriterionManager"; category = "Manager" }
|
||||||
|
@{ prefix = "FilterCriterionList"; category = "List" }
|
||||||
|
)
|
||||||
|
"SettingsStorage" = @(
|
||||||
|
@{ prefix = "SettingsStorageManager"; category = "Manager" }
|
||||||
|
)
|
||||||
|
"IntegrationService" = @(
|
||||||
|
@{ prefix = "IntegrationServiceManager"; category = "Manager" }
|
||||||
|
)
|
||||||
|
"WSReference" = @(
|
||||||
|
@{ prefix = "WSReferenceManager"; category = "Manager" }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
# Types that need ChildObjects element
|
# Types that need ChildObjects element — fallback when the source object cannot be probed.
|
||||||
|
# The platform emits <ChildObjects> for every container type even when empty, and rejects
|
||||||
|
# the file without it ("ожидаемое ChildObjects"); primary signal is the source object itself.
|
||||||
$typesWithChildObjects = @(
|
$typesWithChildObjects = @(
|
||||||
"Catalog","Document","ExchangePlan","ChartOfAccounts",
|
"Catalog","Document","ExchangePlan","ChartOfAccounts",
|
||||||
"ChartOfCharacteristicTypes","ChartOfCalculationTypes",
|
"ChartOfCharacteristicTypes","ChartOfCalculationTypes",
|
||||||
"BusinessProcess","Task","Enum",
|
"BusinessProcess","Task","Enum",
|
||||||
"InformationRegister","AccumulationRegister","AccountingRegister","CalculationRegister"
|
"InformationRegister","AccumulationRegister","AccountingRegister","CalculationRegister",
|
||||||
|
"DataProcessor","Report","DocumentJournal","FilterCriterion","SettingsStorage",
|
||||||
|
"Sequence","HTTPService","WebService","IntegrationService","Subsystem"
|
||||||
)
|
)
|
||||||
|
|
||||||
# CommonModule properties to copy from source
|
# CommonModule properties to copy from source
|
||||||
@@ -346,9 +405,20 @@ function Expand-SelfClosingElement($container, $parentIndent) {
|
|||||||
function Detect-FormatVersion([string]$dir) {
|
function Detect-FormatVersion([string]$dir) {
|
||||||
$d = $dir
|
$d = $dir
|
||||||
while ($d) {
|
while ($d) {
|
||||||
|
# Автономная внешняя обработка/отчёт: своего Configuration.xml у неё нет, версию несёт
|
||||||
|
# корень самой обработки. Без этого форма и макет внутри обработки 2.21 писались бы 2.17.
|
||||||
|
$extPath = "$d.xml"
|
||||||
|
if (Test-Path $extPath) {
|
||||||
|
$extText = [System.IO.File]::ReadAllText($extPath, [System.Text.Encoding]::UTF8)
|
||||||
|
$extHead = $extText.Substring(0, [Math]::Min(2000, $extText.Length))
|
||||||
|
if ($extHead -match '<(ExternalDataProcessor|ExternalReport)[ >]' -and $extHead -match '<MetaDataObject[^>]+version="(\d+\.\d+)"') { return $Matches[1] }
|
||||||
|
}
|
||||||
$cfgPath = Join-Path $d "Configuration.xml"
|
$cfgPath = Join-Path $d "Configuration.xml"
|
||||||
if (Test-Path $cfgPath) {
|
if (Test-Path $cfgPath) {
|
||||||
$head = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8).Substring(0, [Math]::Min(2000, (Get-Item $cfgPath).Length))
|
$cfgText = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8)
|
||||||
|
# Длину среза берём по СТРОКЕ, а не по размеру файла: размер в БАЙТАХ, Substring считает
|
||||||
|
# СИМВОЛЫ, и на кириллице байт больше — короткий Configuration.xml ронял навык исключением.
|
||||||
|
$head = $cfgText.Substring(0, [Math]::Min(2000, $cfgText.Length))
|
||||||
if ($head -match '<MetaDataObject[^>]+version="(\d+\.\d+)"') { return $Matches[1] }
|
if ($head -match '<MetaDataObject[^>]+version="(\d+\.\d+)"') { return $Matches[1] }
|
||||||
}
|
}
|
||||||
$parent = Split-Path $d -Parent
|
$parent = Split-Path $d -Parent
|
||||||
@@ -363,6 +433,20 @@ $script:formatVersion = Detect-FormatVersion $extDir
|
|||||||
# --- 8. Namespaces declaration for object XML ---
|
# --- 8. Namespaces declaration for object XML ---
|
||||||
$script:xmlnsDecl = 'xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
|
$script:xmlnsDecl = 'xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
|
||||||
|
|
||||||
|
# Версия формата как число для сравнений: "2.20" → 220, "2.9" → 209.
|
||||||
|
# Строковое сравнение здесь неверно ("2.9" > "2.17" лексикографически) — известная ловушка.
|
||||||
|
function Get-FormatRank([string]$ver) {
|
||||||
|
if ($ver -match '^(\d+)\.(\d+)$') { return [int]$Matches[1] * 100 + [int]$Matches[2] }
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# 2.21 (8.5) добавила в шапку пространство палитры — ради <Color> у значений перечисления.
|
||||||
|
# Вставляем НА МЕСТО (после lf, перед style): платформа держит объявления по алфавиту,
|
||||||
|
# дописать в конец нельзя.
|
||||||
|
if ((Get-FormatRank $script:formatVersion) -ge 221) {
|
||||||
|
$script:xmlnsDecl = $script:xmlnsDecl -replace ' xmlns:style=', ' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette" xmlns:style='
|
||||||
|
}
|
||||||
|
|
||||||
# --- 9. Parse -Object into items ---
|
# --- 9. Parse -Object into items ---
|
||||||
$items = @()
|
$items = @()
|
||||||
foreach ($part in $Object.Split(";;")) {
|
foreach ($part in $Object.Split(";;")) {
|
||||||
@@ -393,6 +477,37 @@ if ($BorrowMainAttribute) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# --- 10. Helper: read source object XML ---
|
# --- 10. Helper: read source object XML ---
|
||||||
|
# Имена реквизитов исходного объекта → uuid. Нужны для непрозрачной формы пути в ссылках
|
||||||
|
# параметров выбора (см. Rewrite-ChoiceParameterLinks).
|
||||||
|
function Get-SourceAttributeUuids {
|
||||||
|
param([string]$typeName, [string]$objName)
|
||||||
|
|
||||||
|
$result = @{}
|
||||||
|
$dirName = $childTypeDirMap[$typeName]
|
||||||
|
if (-not $dirName) { return $result }
|
||||||
|
$srcFile = Join-Path (Join-Path $cfgDir $dirName) "${objName}.xml"
|
||||||
|
if (-not (Test-Path $srcFile)) { return $result }
|
||||||
|
|
||||||
|
$doc = New-Object System.Xml.XmlDocument
|
||||||
|
$doc.PreserveWhitespace = $false
|
||||||
|
$doc.Load($srcFile)
|
||||||
|
$objEl = $null
|
||||||
|
foreach ($c in $doc.DocumentElement.ChildNodes) {
|
||||||
|
if ($c.NodeType -eq 'Element') { $objEl = $c; break }
|
||||||
|
}
|
||||||
|
if (-not $objEl) { return $result }
|
||||||
|
$childObjects = $objEl.SelectSingleNode("*[local-name()='ChildObjects']")
|
||||||
|
if (-not $childObjects) { return $result }
|
||||||
|
foreach ($child in $childObjects.ChildNodes) {
|
||||||
|
if ($child.NodeType -ne 'Element') { continue }
|
||||||
|
if ($child.LocalName -notin @('Attribute','TabularSection')) { continue }
|
||||||
|
$uuid = $child.GetAttribute("uuid")
|
||||||
|
$nameNode = $child.SelectSingleNode("*[local-name()='Properties']/*[local-name()='Name']")
|
||||||
|
if ($uuid -and $nameNode) { $result[$nameNode.InnerText.Trim()] = $uuid }
|
||||||
|
}
|
||||||
|
return $result
|
||||||
|
}
|
||||||
|
|
||||||
function Read-SourceObject {
|
function Read-SourceObject {
|
||||||
param([string]$typeName, [string]$objName)
|
param([string]$typeName, [string]$objName)
|
||||||
|
|
||||||
@@ -454,6 +569,9 @@ function Read-SourceObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Whether the platform emits <ChildObjects> for this type — the source object is the ground truth
|
||||||
|
$srcProps["__HasChildObjects"] = ($srcEl.SelectSingleNode("md:ChildObjects", $srcNs) -ne $null)
|
||||||
|
|
||||||
return @{
|
return @{
|
||||||
Uuid = $srcUuid
|
Uuid = $srcUuid
|
||||||
Properties = $srcProps
|
Properties = $srcProps
|
||||||
@@ -593,6 +711,11 @@ function Borrow-Form {
|
|||||||
# Get OuterXml and strip redundant namespace redeclarations (they're on root <Form>)
|
# Get OuterXml and strip redundant namespace redeclarations (they're on root <Form>)
|
||||||
$nsStripPattern = '\s+xmlns(?::\w+)?="[^"]*"'
|
$nsStripPattern = '\s+xmlns(?::\w+)?="[^"]*"'
|
||||||
|
|
||||||
|
# uuid реквизитов объекта — только для формы без заимствованного основного реквизита:
|
||||||
|
# там ссылки параметров выбора переводятся на непрозрачную форму пути
|
||||||
|
$srcAttrUuids = @{}
|
||||||
|
if (-not $BorrowMainAttr) { $srcAttrUuids = Get-SourceAttributeUuids $typeName $objName }
|
||||||
|
|
||||||
# AutoCommandBar: keep ChildItems (buttons with CommandName→0), Autofill→false
|
# AutoCommandBar: keep ChildItems (buttons with CommandName→0), Autofill→false
|
||||||
$autoCmdXml = ""
|
$autoCmdXml = ""
|
||||||
if ($srcAutoCmd) {
|
if ($srcAutoCmd) {
|
||||||
@@ -604,6 +727,7 @@ function Borrow-Form {
|
|||||||
$autoCmdXml = [regex]::Replace($autoCmdXml, '\s*<ExcludedCommand>[^<]*</ExcludedCommand>', '')
|
$autoCmdXml = [regex]::Replace($autoCmdXml, '\s*<ExcludedCommand>[^<]*</ExcludedCommand>', '')
|
||||||
# Strip data-binding tags whose root attribute isn't borrowed
|
# Strip data-binding tags whose root attribute isn't borrowed
|
||||||
$autoCmdXml = Strip-FormBindings $autoCmdXml ([bool]$BorrowMainAttr)
|
$autoCmdXml = Strip-FormBindings $autoCmdXml ([bool]$BorrowMainAttr)
|
||||||
|
if (-not $BorrowMainAttr) { $autoCmdXml = Rewrite-ChoiceParameterLinks $autoCmdXml $srcAttrUuids }
|
||||||
}
|
}
|
||||||
|
|
||||||
# ChildItems: copy full tree, clean up base-config references
|
# ChildItems: copy full tree, clean up base-config references
|
||||||
@@ -616,6 +740,7 @@ function Borrow-Form {
|
|||||||
# Strip data-binding tags whose root attribute isn't borrowed
|
# Strip data-binding tags whose root attribute isn't borrowed
|
||||||
# (DataPath/TitleDataPath/FooterDataPath/HeaderDataPath/MultipleValue*/RowPicture*)
|
# (DataPath/TitleDataPath/FooterDataPath/HeaderDataPath/MultipleValue*/RowPicture*)
|
||||||
$childItemsXml = Strip-FormBindings $childItemsXml ([bool]$BorrowMainAttr)
|
$childItemsXml = Strip-FormBindings $childItemsXml ([bool]$BorrowMainAttr)
|
||||||
|
if (-not $BorrowMainAttr) { $childItemsXml = Rewrite-ChoiceParameterLinks $childItemsXml $srcAttrUuids }
|
||||||
# Strip ExcludedCommand in nested AutoCommandBars (references to standard commands invalid in extension)
|
# Strip ExcludedCommand in nested AutoCommandBars (references to standard commands invalid in extension)
|
||||||
$childItemsXml = [regex]::Replace($childItemsXml, '\s*<ExcludedCommand>[^<]*</ExcludedCommand>', '')
|
$childItemsXml = [regex]::Replace($childItemsXml, '\s*<ExcludedCommand>[^<]*</ExcludedCommand>', '')
|
||||||
# Strip TypeLink blocks with human-readable DataPath (Items.XXX — can't convert to UUID)
|
# Strip TypeLink blocks with human-readable DataPath (Items.XXX — can't convert to UUID)
|
||||||
@@ -796,11 +921,22 @@ function Borrow-Form {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Extract the <Form ...> opening tag from source text (preserves namespace declarations)
|
# Открывающий тег <Form ...> берём из исходной формы — ради её объявлений пространств имён,
|
||||||
|
# но version подставляем СВОЮ: форма обязана нести версию расширения, иначе платформа
|
||||||
|
# отвергает импорт (форма 2.13 внутри расширения 2.17). Раньше тег копировался целиком,
|
||||||
|
# и версия источника молча побеждала.
|
||||||
$xmlDecl = '<?xml version="1.0" encoding="UTF-8"?>'
|
$xmlDecl = '<?xml version="1.0" encoding="UTF-8"?>'
|
||||||
$formTag = "<Form version=`"${formVersion}`">"
|
$formTag = "<Form version=`"${formVersion}`">"
|
||||||
if ($srcFormContent -match '(?s)^(<\?xml[^?]*\?>)') { $xmlDecl = $Matches[1] }
|
if ($srcFormContent -match '(?s)^(<\?xml[^?]*\?>)') { $xmlDecl = $Matches[1] }
|
||||||
if ($srcFormContent -match '(<Form[^>]*>)') { $formTag = $Matches[1] }
|
if ($srcFormContent -match '(<Form[^>]*>)') {
|
||||||
|
$srcTag = $Matches[1]
|
||||||
|
$srcNs = $srcTag -replace '^<Form\s*', '' -replace '\s*/?>$', '' -replace '\s*version="[^"]*"', ''
|
||||||
|
# 2.21 (8.5): пространство палитры. Место строгое — после lf, перед style.
|
||||||
|
if ((Get-FormatRank $formVersion) -ge 221 -and $srcNs -notmatch 'xmlns:pal=') {
|
||||||
|
$srcNs = $srcNs -replace ' xmlns:style=', ' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette" xmlns:style='
|
||||||
|
}
|
||||||
|
$formTag = if ($srcNs) { "<Form $srcNs version=`"${formVersion}`">" } else { "<Form version=`"${formVersion}`">" }
|
||||||
|
}
|
||||||
|
|
||||||
# Build output Form.xml
|
# Build output Form.xml
|
||||||
$formXmlSb = New-Object System.Text.StringBuilder
|
$formXmlSb = New-Object System.Text.StringBuilder
|
||||||
@@ -822,6 +958,12 @@ function Borrow-Form {
|
|||||||
$formXmlSb.Append("`t$childItemsXml") | Out-Null
|
$formXmlSb.Append("`t$childItemsXml") | Out-Null
|
||||||
$formXmlSb.Append("`r`n") | Out-Null
|
$formXmlSb.Append("`r`n") | Out-Null
|
||||||
}
|
}
|
||||||
|
# Секции основного реквизита исходной формы (<UseAlways>, <Columns>) — их нельзя терять
|
||||||
|
$mainAttrExtra = @()
|
||||||
|
if ($BorrowMainAttr) {
|
||||||
|
$mainAttrExtra = @(Get-MainAttributeExtraXml $srcFormEl $nsStripPattern)
|
||||||
|
}
|
||||||
|
|
||||||
# Attributes: empty or with MainAttribute when BorrowMainAttr
|
# Attributes: empty or with MainAttribute when BorrowMainAttr
|
||||||
if ($BorrowMainAttr) {
|
if ($BorrowMainAttr) {
|
||||||
$objTypePrefix = ""
|
$objTypePrefix = ""
|
||||||
@@ -833,6 +975,9 @@ function Borrow-Form {
|
|||||||
$formXmlSb.Append("`t`t`t<Type><v8:Type>${mainAttrType}</v8:Type></Type>`r`n") | Out-Null
|
$formXmlSb.Append("`t`t`t<Type><v8:Type>${mainAttrType}</v8:Type></Type>`r`n") | Out-Null
|
||||||
$formXmlSb.Append("`t`t`t<MainAttribute>true</MainAttribute>`r`n") | Out-Null
|
$formXmlSb.Append("`t`t`t<MainAttribute>true</MainAttribute>`r`n") | Out-Null
|
||||||
$formXmlSb.Append("`t`t`t<SavedData>true</SavedData>`r`n") | Out-Null
|
$formXmlSb.Append("`t`t`t<SavedData>true</SavedData>`r`n") | Out-Null
|
||||||
|
foreach ($extraXml in $mainAttrExtra) {
|
||||||
|
$formXmlSb.Append("`t`t`t$extraXml`r`n") | Out-Null
|
||||||
|
}
|
||||||
$formXmlSb.Append("`t`t</Attribute>`r`n") | Out-Null
|
$formXmlSb.Append("`t`t</Attribute>`r`n") | Out-Null
|
||||||
$formXmlSb.Append("`t</Attributes>") | Out-Null
|
$formXmlSb.Append("`t</Attributes>") | Out-Null
|
||||||
} else {
|
} else {
|
||||||
@@ -873,6 +1018,15 @@ function Borrow-Form {
|
|||||||
$formXmlSb.Append("`t`t`t`t<Type><v8:Type>${mainAttrType}</v8:Type></Type>`r`n") | Out-Null
|
$formXmlSb.Append("`t`t`t`t<Type><v8:Type>${mainAttrType}</v8:Type></Type>`r`n") | Out-Null
|
||||||
$formXmlSb.Append("`t`t`t`t<MainAttribute>true</MainAttribute>`r`n") | Out-Null
|
$formXmlSb.Append("`t`t`t`t<MainAttribute>true</MainAttribute>`r`n") | Out-Null
|
||||||
$formXmlSb.Append("`t`t`t`t<SavedData>true</SavedData>`r`n") | Out-Null
|
$formXmlSb.Append("`t`t`t`t<SavedData>true</SavedData>`r`n") | Out-Null
|
||||||
|
foreach ($extraXml in $mainAttrExtra) {
|
||||||
|
# В BaseForm та же секция на уровень глубже — приём переиндентации тот же, что у ChildItems
|
||||||
|
$exLines = $extraXml -split "`r?`n"
|
||||||
|
for ($li = 0; $li -lt $exLines.Count; $li++) {
|
||||||
|
if ($li -eq 0) { $formXmlSb.Append("`t`t`t`t$($exLines[$li])") | Out-Null }
|
||||||
|
else { $formXmlSb.Append("`t$($exLines[$li])") | Out-Null }
|
||||||
|
$formXmlSb.Append("`r`n") | Out-Null
|
||||||
|
}
|
||||||
|
}
|
||||||
$formXmlSb.Append("`t`t`t</Attribute>`r`n") | Out-Null
|
$formXmlSb.Append("`t`t`t</Attribute>`r`n") | Out-Null
|
||||||
$formXmlSb.Append("`t`t</Attributes>") | Out-Null
|
$formXmlSb.Append("`t`t</Attributes>") | Out-Null
|
||||||
} else {
|
} else {
|
||||||
@@ -889,7 +1043,15 @@ function Borrow-Form {
|
|||||||
New-Item -ItemType Directory -Path $formXmlDir -Force | Out-Null
|
New-Item -ItemType Directory -Path $formXmlDir -Force | Out-Null
|
||||||
}
|
}
|
||||||
$formXmlFile = Join-Path $formXmlDir "Form.xml"
|
$formXmlFile = Join-Path $formXmlDir "Form.xml"
|
||||||
[System.IO.File]::WriteAllText($formXmlFile, $formXmlSb.ToString(), $enc)
|
# Пустой элемент: XmlWriter отдаёт `<a />`, Конфигуратор пишет `<a/>`. Внутри
|
||||||
|
# CDATA/комментария ` />` может быть содержимым (там `>` не экранируется),
|
||||||
|
# поэтому они идут первыми ветками альтернации и возвращаются как есть.
|
||||||
|
# Здесь источник не XmlWriter, а OuterXml исходного документа — спацовывает так же.
|
||||||
|
$formXmlText = $formXmlSb.ToString()
|
||||||
|
$formXmlText = [regex]::Replace($formXmlText, '(?s)<!\[CDATA\[.*?\]\]>|<!--.*?-->|(?<=\S) />', { param($m) if ($m.Value -eq ' />') { '/>' } else { $m.Value } })
|
||||||
|
# Файл создаём мы — канон выгрузки: CRLF в разделителях строк.
|
||||||
|
$formXmlText = ($formXmlText -replace "`r`n", "`n") -replace "`n", "`r`n"
|
||||||
|
[System.IO.File]::WriteAllText($formXmlFile, $formXmlText, $enc)
|
||||||
Info " Created: $formXmlFile"
|
Info " Created: $formXmlFile"
|
||||||
|
|
||||||
# 6. Create empty Module.bsl — but NEVER overwrite an existing one (re-borrow must
|
# 6. Create empty Module.bsl — but NEVER overwrite an existing one (re-borrow must
|
||||||
@@ -995,8 +1157,16 @@ function Register-FormInObject {
|
|||||||
$text2 = [System.Text.Encoding]::UTF8.GetString($bytes2)
|
$text2 = [System.Text.Encoding]::UTF8.GetString($bytes2)
|
||||||
if ($text2.Length -gt 0 -and $text2[0] -eq [char]0xFEFF) { $text2 = $text2.Substring(1) }
|
if ($text2.Length -gt 0 -and $text2[0] -eq [char]0xFEFF) { $text2 = $text2.Substring(1) }
|
||||||
$text2 = $text2.Replace('encoding="utf-8"', 'encoding="UTF-8"')
|
$text2 = $text2.Replace('encoding="utf-8"', 'encoding="UTF-8"')
|
||||||
|
# Пустой элемент: XmlWriter отдаёт `<a />`, Конфигуратор пишет `<a/>`. Внутри
|
||||||
|
# CDATA/комментария ` />` может быть содержимым (там `>` не экранируется),
|
||||||
|
# поэтому они идут первыми ветками альтернации и возвращаются как есть.
|
||||||
|
$text2 = [regex]::Replace($text2, '(?s)<!\[CDATA\[.*?\]\]>|<!--.*?-->|(?<=\S) />', { param($m) if ($m.Value -eq ' />') { '/>' } else { $m.Value } })
|
||||||
|
|
||||||
$utf8Bom2 = New-Object System.Text.UTF8Encoding($true)
|
$utf8Bom2 = New-Object System.Text.UTF8Encoding($true)
|
||||||
|
# Целевой перевод строки: стиль файла-назначения — правка наследует его (#44/#46/#47),
|
||||||
|
# новый файл получает канон выгрузки CRLF. Зеркало _detect_xml_style в py-порту.
|
||||||
|
$targetEol = if ((Test-Path -LiteralPath $objFile) -and ([System.IO.File]::ReadAllText($objFile) -notmatch "`r`n")) { "`n" } else { "`r`n" }
|
||||||
|
$text2 = ($text2 -replace "`r`n", "`n") -replace "`n", $targetEol
|
||||||
[System.IO.File]::WriteAllText($objFile, $text2, $utf8Bom2)
|
[System.IO.File]::WriteAllText($objFile, $text2, $utf8Bom2)
|
||||||
Info " Registered form in: $objFile"
|
Info " Registered form in: $objFile"
|
||||||
}
|
}
|
||||||
@@ -1043,6 +1213,24 @@ function Build-InternalInfoXml {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# --- 11b. Collect DataPath references from source Form.xml ---
|
# --- 11b. Collect DataPath references from source Form.xml ---
|
||||||
|
# --- 11b1. Секции основного реквизита исходной формы, кроме Type/MainAttribute/SavedData ---
|
||||||
|
# Это <UseAlways> и <Columns> (доп. колонки табличных частей, объявленные прямо в форме). Их
|
||||||
|
# переносит Конфигуратор, и без них платформа отвергает форму: «Неверный путь к данным» на
|
||||||
|
# колонке, которой у объекта нет (Объект.Товары.Артикул). Возвращает список OuterXml без xmlns.
|
||||||
|
function Get-MainAttributeExtraXml {
|
||||||
|
param($formEl, [string]$nsStripPattern)
|
||||||
|
|
||||||
|
$result = @()
|
||||||
|
$mainAttr = $formEl.SelectSingleNode("*[local-name()='Attributes']/*[local-name()='Attribute'][*[local-name()='MainAttribute']='true']")
|
||||||
|
if (-not $mainAttr) { return $result }
|
||||||
|
foreach ($child in $mainAttr.ChildNodes) {
|
||||||
|
if ($child.NodeType -ne 'Element') { continue }
|
||||||
|
if ($child.LocalName -in @('Type','MainAttribute','SavedData')) { continue }
|
||||||
|
$result += [regex]::Replace($child.OuterXml, $nsStripPattern, '')
|
||||||
|
}
|
||||||
|
return $result
|
||||||
|
}
|
||||||
|
|
||||||
function Collect-FormDataPaths {
|
function Collect-FormDataPaths {
|
||||||
param([string]$formXmlPath)
|
param([string]$formXmlPath)
|
||||||
|
|
||||||
@@ -1087,6 +1275,16 @@ function Collect-FormDataPaths {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Also scan <AdditionalColumns table="Объект.X"> — доп. колонки табличной части, объявленные в
|
||||||
|
# самой форме (напр. Объект.Товары.Артикул). Такая ТЧ может больше нигде на форме не встречаться,
|
||||||
|
# и без её заимствования платформа отвергает форму: «Неверный путь к данным».
|
||||||
|
$acMatches = [regex]::Matches($content, '<AdditionalColumns table="Объект\.(\w+)"')
|
||||||
|
foreach ($m in $acMatches) {
|
||||||
|
$seg0 = $m.Groups[1].Value
|
||||||
|
if ($script:standardFields -contains $seg0) { continue }
|
||||||
|
$firstLevel[$seg0] = $true
|
||||||
|
}
|
||||||
|
|
||||||
# Deduplicate deep paths
|
# Deduplicate deep paths
|
||||||
$seen = @{}
|
$seen = @{}
|
||||||
$uniqueDeep = @()
|
$uniqueDeep = @()
|
||||||
@@ -1199,8 +1397,16 @@ function Resolve-SourceAttributes {
|
|||||||
$extraProps = [ordered]@{}
|
$extraProps = [ordered]@{}
|
||||||
$propsNode = $srcEl.SelectSingleNode("md:Properties", $srcNs)
|
$propsNode = $srcEl.SelectSingleNode("md:Properties", $srcNs)
|
||||||
if ($propsNode) {
|
if ($propsNode) {
|
||||||
$propsToExtract = @("Hierarchical","FoldersOnTop","CodeLength","DescriptionLength","CodeType","CodeAllowedLength",
|
# NumberPeriodicity сюда НЕ входит: платформа считает его модификацией настроек нумерации и
|
||||||
"NumberType","NumberLength","NumberAllowedLength","NumberPeriodicity")
|
# тогда требует объявить ещё и <Numerator/>, иначе /UpdateDBCfg падает — «отключать
|
||||||
|
# контролируемость свойства "Нумератор" недопустимо». Конфигуратор его не переносит
|
||||||
|
# (эталон заимствования документа: NumberType/NumberLength/NumberAllowedLength и всё).
|
||||||
|
# Загрузку это не ломает, ошибка вылезает только на обновлении конфигурации БД.
|
||||||
|
# FoldersOnTop сюда НЕ входит: платформа его у заимствованной оболочки не хранит — при
|
||||||
|
# загрузке молча выбрасывает (проверено раундтрипом: записали, выгрузили обратно, свойства
|
||||||
|
# нет). Конфигуратор его тоже не переносит. Остальные из списка сохраняются.
|
||||||
|
$propsToExtract = @("Hierarchical","CodeLength","DescriptionLength","CodeType","CodeAllowedLength",
|
||||||
|
"NumberType","NumberLength","NumberAllowedLength")
|
||||||
foreach ($pName in $propsToExtract) {
|
foreach ($pName in $propsToExtract) {
|
||||||
$pNode = $propsNode.SelectSingleNode("md:${pName}", $srcNs)
|
$pNode = $propsNode.SelectSingleNode("md:${pName}", $srcNs)
|
||||||
if ($pNode) { $extraProps[$pName] = $pNode.InnerText }
|
if ($pNode) { $extraProps[$pName] = $pNode.InnerText }
|
||||||
@@ -1385,7 +1591,17 @@ function Merge-AttributesIntoObject {
|
|||||||
# Insert attributes before </ChildObjects>
|
# Insert attributes before </ChildObjects>
|
||||||
$text3 = $text3 -replace '</ChildObjects>', "${allAttrXml}`r`n`t`t</ChildObjects>"
|
$text3 = $text3 -replace '</ChildObjects>', "${allAttrXml}`r`n`t`t</ChildObjects>"
|
||||||
|
|
||||||
|
# Пустой элемент: XmlWriter отдаёт `<a />`, Конфигуратор пишет `<a/>`. Внутри
|
||||||
|
# CDATA/комментария ` />` может быть содержимым (там `>` не экранируется),
|
||||||
|
# поэтому они идут первыми ветками альтернации и возвращаются как есть.
|
||||||
|
# Стоит ПОСЛЕ вставки реквизитов, чтобы накрыть и их.
|
||||||
|
$text3 = [regex]::Replace($text3, '(?s)<!\[CDATA\[.*?\]\]>|<!--.*?-->|(?<=\S) />', { param($m) if ($m.Value -eq ' />') { '/>' } else { $m.Value } })
|
||||||
|
|
||||||
$utf8Bom3 = New-Object System.Text.UTF8Encoding($true)
|
$utf8Bom3 = New-Object System.Text.UTF8Encoding($true)
|
||||||
|
# Целевой перевод строки: стиль файла-назначения — правка наследует его (#44/#46/#47),
|
||||||
|
# новый файл получает канон выгрузки CRLF. Зеркало _detect_xml_style в py-порту.
|
||||||
|
$targetEol = if ((Test-Path -LiteralPath $objFile) -and ([System.IO.File]::ReadAllText($objFile) -notmatch "`r`n")) { "`n" } else { "`r`n" }
|
||||||
|
$text3 = ($text3 -replace "`r`n", "`n") -replace "`n", $targetEol
|
||||||
[System.IO.File]::WriteAllText($objFile, $text3, $utf8Bom3)
|
[System.IO.File]::WriteAllText($objFile, $text3, $utf8Bom3)
|
||||||
Info " Merged $added attribute(s) into: $objFile"
|
Info " Merged $added attribute(s) into: $objFile"
|
||||||
}
|
}
|
||||||
@@ -1493,6 +1709,20 @@ function Borrow-MainAttribute {
|
|||||||
foreach ($ts in $srcTS) {
|
foreach ($ts in $srcTS) {
|
||||||
foreach ($tsa in $ts.Attributes) { $allTypeXmls += $tsa.TypeXml }
|
foreach ($tsa in $ts.Attributes) { $allTypeXmls += $tsa.TypeXml }
|
||||||
}
|
}
|
||||||
|
# Типы из <Columns> основного реквизита формы: колонку мы переносим (Borrow-Form), значит и её
|
||||||
|
# тип должен быть заимствован — иначе колонка ссылается на DefinedType/справочник, которого в
|
||||||
|
# расширении нет. Конфигуратор поступает так же (эталон: DefinedTypes/Артикул при заимствовании
|
||||||
|
# формы заказа поставщику).
|
||||||
|
$srcFormForCols = Join-Path (Join-Path (Join-Path (Join-Path (Join-Path $cfgDir $dirName) $objName) "Forms") $formName) "Ext/Form.xml"
|
||||||
|
if (Test-Path $srcFormForCols) {
|
||||||
|
$colsDoc = New-Object System.Xml.XmlDocument
|
||||||
|
$colsDoc.PreserveWhitespace = $true
|
||||||
|
$colsDoc.Load($srcFormForCols)
|
||||||
|
foreach ($extraXml in (Get-MainAttributeExtraXml $colsDoc.DocumentElement '\s+xmlns(?::\w+)?="[^"]*"')) {
|
||||||
|
if ($extraXml -like "<Columns*") { $allTypeXmls += $extraXml }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$refTypes = Collect-ReferenceTypes $allTypeXmls
|
$refTypes = Collect-ReferenceTypes $allTypeXmls
|
||||||
Info " Reference types to borrow: $($refTypes.Count)"
|
Info " Reference types to borrow: $($refTypes.Count)"
|
||||||
|
|
||||||
@@ -1669,7 +1899,7 @@ function Build-BorrowedObjectXml {
|
|||||||
$sb.AppendLine("`t`t</Properties>") | Out-Null
|
$sb.AppendLine("`t`t</Properties>") | Out-Null
|
||||||
|
|
||||||
# ChildObjects (for types that need it)
|
# ChildObjects (for types that need it)
|
||||||
if ($typesWithChildObjects -contains $typeName) {
|
if ($sourceProps["__HasChildObjects"] -or ($typesWithChildObjects -contains $typeName)) {
|
||||||
$sb.AppendLine("`t`t<ChildObjects/>") | Out-Null
|
$sb.AppendLine("`t`t<ChildObjects/>") | Out-Null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1849,8 +2079,16 @@ $memStream.Close()
|
|||||||
$text = [System.Text.Encoding]::UTF8.GetString($bytes)
|
$text = [System.Text.Encoding]::UTF8.GetString($bytes)
|
||||||
if ($text.Length -gt 0 -and $text[0] -eq [char]0xFEFF) { $text = $text.Substring(1) }
|
if ($text.Length -gt 0 -and $text[0] -eq [char]0xFEFF) { $text = $text.Substring(1) }
|
||||||
$text = $text.Replace('encoding="utf-8"', 'encoding="UTF-8"')
|
$text = $text.Replace('encoding="utf-8"', 'encoding="UTF-8"')
|
||||||
|
# Пустой элемент: XmlWriter отдаёт `<a />`, Конфигуратор пишет `<a/>`. Внутри
|
||||||
|
# CDATA/комментария ` />` может быть содержимым (там `>` не экранируется),
|
||||||
|
# поэтому они идут первыми ветками альтернации и возвращаются как есть.
|
||||||
|
$text = [regex]::Replace($text, '(?s)<!\[CDATA\[.*?\]\]>|<!--.*?-->|(?<=\S) />', { param($m) if ($m.Value -eq ' />') { '/>' } else { $m.Value } })
|
||||||
|
|
||||||
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
|
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
|
||||||
|
# Целевой перевод строки: стиль файла-назначения — правка наследует его (#44/#46/#47),
|
||||||
|
# новый файл получает канон выгрузки CRLF. Зеркало _detect_xml_style в py-порту.
|
||||||
|
$targetEol = if ((Test-Path -LiteralPath $extResolvedPath) -and ([System.IO.File]::ReadAllText($extResolvedPath) -notmatch "`r`n")) { "`n" } else { "`r`n" }
|
||||||
|
$text = ($text -replace "`r`n", "`n") -replace "`n", $targetEol
|
||||||
[System.IO.File]::WriteAllText($extResolvedPath, $text, $utf8Bom)
|
[System.IO.File]::WriteAllText($extResolvedPath, $text, $utf8Bom)
|
||||||
Info "Saved: $extResolvedPath"
|
Info "Saved: $extResolvedPath"
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# cfe-borrow v1.9 — Borrow objects from configuration into extension (CFE)
|
# cfe-borrow v1.25 — Borrow objects from configuration into extension (CFE) (не переносить FoldersOnTop — платформа его не хранит)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -9,6 +9,28 @@ import sys
|
|||||||
import uuid
|
import uuid
|
||||||
from lxml import etree
|
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)
|
||||||
|
|
||||||
|
|
||||||
MD_NS = "http://v8.1c.ru/8.3/MDClasses"
|
MD_NS = "http://v8.1c.ru/8.3/MDClasses"
|
||||||
XR_NS = "http://v8.1c.ru/8.3/xcf/readable"
|
XR_NS = "http://v8.1c.ru/8.3/xcf/readable"
|
||||||
XSI_NS = "http://www.w3.org/2001/XMLSchema-instance"
|
XSI_NS = "http://www.w3.org/2001/XMLSchema-instance"
|
||||||
@@ -36,6 +58,33 @@ def strip_form_bindings(xml, keep_objekt):
|
|||||||
return xml
|
return xml
|
||||||
|
|
||||||
|
|
||||||
|
def rewrite_choice_parameter_links(xml, attr_uuids):
|
||||||
|
"""Ссылки параметров выбора (<ChoiceParameterLinks>/<xr:Link>) — привязка особого рода: путь лежит
|
||||||
|
в <xr:DataPath> и обычным стриппингом не снимается. Когда основной реквизит не заимствован,
|
||||||
|
текстовый «Объект.X» в расширении не разрешается («Неверный путь к полю»), поэтому Конфигуратор
|
||||||
|
переписывает его в непрозрачную форму «1/0:<uuid реквизита объекта>» — ссылка остаётся рабочей.
|
||||||
|
Реквизит, которого в источнике нет, недоступен и по uuid: такую связь вырезаем целиком.
|
||||||
|
Путь всегда односегментный (корпусная проверка: 285 из 285 у УТ), глубже — тоже вырезаем."""
|
||||||
|
if '<ChoiceParameterLinks>' not in xml:
|
||||||
|
return xml
|
||||||
|
|
||||||
|
def repl(m):
|
||||||
|
link = m.group(0)
|
||||||
|
dp = re.search(r'<xr:DataPath[^>]*>Объект\.([^<]+)</xr:DataPath>', link)
|
||||||
|
if not dp:
|
||||||
|
return link
|
||||||
|
attr_name = dp.group(1)
|
||||||
|
if attr_name in attr_uuids:
|
||||||
|
return re.sub(r'(<xr:DataPath[^>]*>)Объект\.[^<]+(</xr:DataPath>)',
|
||||||
|
lambda mm: f"{mm.group(1)}1/0:{attr_uuids[attr_name]}{mm.group(2)}", link)
|
||||||
|
return ''
|
||||||
|
|
||||||
|
xml = re.sub(r'\s*<xr:Link>.*?</xr:Link>', repl, xml, flags=re.DOTALL)
|
||||||
|
# Опустевший контейнер платформе не нужен
|
||||||
|
xml = re.sub(r'\s*<ChoiceParameterLinks>\s*</ChoiceParameterLinks>', '', xml, flags=re.DOTALL)
|
||||||
|
return xml
|
||||||
|
|
||||||
|
|
||||||
def decode_numeric_entities(s):
|
def decode_numeric_entities(s):
|
||||||
"""lxml emits numeric character refs (&#xNNNN;) for non-ASCII in some self-closed
|
"""lxml emits numeric character refs (&#xNNNN;) for non-ASCII in some self-closed
|
||||||
elements where the PowerShell port writes literal characters. Normalize numeric refs
|
elements where the PowerShell port writes literal characters. Normalize numeric refs
|
||||||
@@ -80,6 +129,7 @@ CHILD_TYPE_DIR_MAP = {
|
|||||||
"XDTOPackage": "XDTOPackages", "WebService": "WebServices",
|
"XDTOPackage": "XDTOPackages", "WebService": "WebServices",
|
||||||
"HTTPService": "HTTPServices", "WSReference": "WSReferences",
|
"HTTPService": "HTTPServices", "WSReference": "WSReferences",
|
||||||
"CommonAttribute": "CommonAttributes", "Style": "Styles",
|
"CommonAttribute": "CommonAttributes", "Style": "Styles",
|
||||||
|
"Bot": "Bots", "Language": "Languages",
|
||||||
}
|
}
|
||||||
|
|
||||||
SYNONYM_MAP = {
|
SYNONYM_MAP = {
|
||||||
@@ -123,7 +173,7 @@ SYNONYM_MAP = {
|
|||||||
TYPE_ORDER = [
|
TYPE_ORDER = [
|
||||||
"Language", "Subsystem", "StyleItem", "Style",
|
"Language", "Subsystem", "StyleItem", "Style",
|
||||||
"CommonPicture", "SessionParameter", "Role", "CommonTemplate",
|
"CommonPicture", "SessionParameter", "Role", "CommonTemplate",
|
||||||
"FilterCriterion", "CommonModule", "CommonAttribute", "ExchangePlan",
|
"FilterCriterion", "CommonModule", "Bot", "CommonAttribute", "ExchangePlan",
|
||||||
"XDTOPackage", "WebService", "HTTPService", "WSReference",
|
"XDTOPackage", "WebService", "HTTPService", "WSReference",
|
||||||
"EventSubscription", "ScheduledJob", "SettingsStorage", "FunctionalOption",
|
"EventSubscription", "ScheduledJob", "SettingsStorage", "FunctionalOption",
|
||||||
"FunctionalOptionsParameter", "DefinedType", "CommonCommand", "CommandGroup",
|
"FunctionalOptionsParameter", "DefinedType", "CommonCommand", "CommandGroup",
|
||||||
@@ -179,6 +229,7 @@ GENERATED_TYPES = {
|
|||||||
],
|
],
|
||||||
"AccountingRegister": [
|
"AccountingRegister": [
|
||||||
{"prefix": "AccountingRegisterRecord", "category": "Record"},
|
{"prefix": "AccountingRegisterRecord", "category": "Record"},
|
||||||
|
{"prefix": "AccountingRegisterExtDimensions", "category": "ExtDimensions"},
|
||||||
{"prefix": "AccountingRegisterManager", "category": "Manager"},
|
{"prefix": "AccountingRegisterManager", "category": "Manager"},
|
||||||
{"prefix": "AccountingRegisterSelection", "category": "Selection"},
|
{"prefix": "AccountingRegisterSelection", "category": "Selection"},
|
||||||
{"prefix": "AccountingRegisterList", "category": "List"},
|
{"prefix": "AccountingRegisterList", "category": "List"},
|
||||||
@@ -192,6 +243,7 @@ GENERATED_TYPES = {
|
|||||||
{"prefix": "CalculationRegisterList", "category": "List"},
|
{"prefix": "CalculationRegisterList", "category": "List"},
|
||||||
{"prefix": "CalculationRegisterRecordSet", "category": "RecordSet"},
|
{"prefix": "CalculationRegisterRecordSet", "category": "RecordSet"},
|
||||||
{"prefix": "CalculationRegisterRecordKey", "category": "RecordKey"},
|
{"prefix": "CalculationRegisterRecordKey", "category": "RecordKey"},
|
||||||
|
{"prefix": "RecalculationsManager", "category": "Recalcs"},
|
||||||
],
|
],
|
||||||
"ChartOfAccounts": [
|
"ChartOfAccounts": [
|
||||||
{"prefix": "ChartOfAccountsObject", "category": "Object"},
|
{"prefix": "ChartOfAccountsObject", "category": "Object"},
|
||||||
@@ -199,12 +251,15 @@ GENERATED_TYPES = {
|
|||||||
{"prefix": "ChartOfAccountsSelection", "category": "Selection"},
|
{"prefix": "ChartOfAccountsSelection", "category": "Selection"},
|
||||||
{"prefix": "ChartOfAccountsList", "category": "List"},
|
{"prefix": "ChartOfAccountsList", "category": "List"},
|
||||||
{"prefix": "ChartOfAccountsManager", "category": "Manager"},
|
{"prefix": "ChartOfAccountsManager", "category": "Manager"},
|
||||||
|
{"prefix": "ChartOfAccountsExtDimensionTypes", "category": "ExtDimensionTypes"},
|
||||||
|
{"prefix": "ChartOfAccountsExtDimensionTypesRow", "category": "ExtDimensionTypesRow"},
|
||||||
],
|
],
|
||||||
"ChartOfCharacteristicTypes": [
|
"ChartOfCharacteristicTypes": [
|
||||||
{"prefix": "ChartOfCharacteristicTypesObject", "category": "Object"},
|
{"prefix": "ChartOfCharacteristicTypesObject", "category": "Object"},
|
||||||
{"prefix": "ChartOfCharacteristicTypesRef", "category": "Ref"},
|
{"prefix": "ChartOfCharacteristicTypesRef", "category": "Ref"},
|
||||||
{"prefix": "ChartOfCharacteristicTypesSelection", "category": "Selection"},
|
{"prefix": "ChartOfCharacteristicTypesSelection", "category": "Selection"},
|
||||||
{"prefix": "ChartOfCharacteristicTypesList", "category": "List"},
|
{"prefix": "ChartOfCharacteristicTypesList", "category": "List"},
|
||||||
|
{"prefix": "Characteristic", "category": "Characteristic"},
|
||||||
{"prefix": "ChartOfCharacteristicTypesManager", "category": "Manager"},
|
{"prefix": "ChartOfCharacteristicTypesManager", "category": "Manager"},
|
||||||
],
|
],
|
||||||
"ChartOfCalculationTypes": [
|
"ChartOfCalculationTypes": [
|
||||||
@@ -214,8 +269,11 @@ GENERATED_TYPES = {
|
|||||||
{"prefix": "ChartOfCalculationTypesList", "category": "List"},
|
{"prefix": "ChartOfCalculationTypesList", "category": "List"},
|
||||||
{"prefix": "ChartOfCalculationTypesManager", "category": "Manager"},
|
{"prefix": "ChartOfCalculationTypesManager", "category": "Manager"},
|
||||||
{"prefix": "DisplacingCalculationTypes", "category": "DisplacingCalculationTypes"},
|
{"prefix": "DisplacingCalculationTypes", "category": "DisplacingCalculationTypes"},
|
||||||
|
{"prefix": "DisplacingCalculationTypesRow", "category": "DisplacingCalculationTypesRow"},
|
||||||
{"prefix": "BaseCalculationTypes", "category": "BaseCalculationTypes"},
|
{"prefix": "BaseCalculationTypes", "category": "BaseCalculationTypes"},
|
||||||
|
{"prefix": "BaseCalculationTypesRow", "category": "BaseCalculationTypesRow"},
|
||||||
{"prefix": "LeadingCalculationTypes", "category": "LeadingCalculationTypes"},
|
{"prefix": "LeadingCalculationTypes", "category": "LeadingCalculationTypes"},
|
||||||
|
{"prefix": "LeadingCalculationTypesRow", "category": "LeadingCalculationTypesRow"},
|
||||||
],
|
],
|
||||||
"BusinessProcess": [
|
"BusinessProcess": [
|
||||||
{"prefix": "BusinessProcessObject", "category": "Object"},
|
{"prefix": "BusinessProcessObject", "category": "Object"},
|
||||||
@@ -223,6 +281,7 @@ GENERATED_TYPES = {
|
|||||||
{"prefix": "BusinessProcessSelection", "category": "Selection"},
|
{"prefix": "BusinessProcessSelection", "category": "Selection"},
|
||||||
{"prefix": "BusinessProcessList", "category": "List"},
|
{"prefix": "BusinessProcessList", "category": "List"},
|
||||||
{"prefix": "BusinessProcessManager", "category": "Manager"},
|
{"prefix": "BusinessProcessManager", "category": "Manager"},
|
||||||
|
{"prefix": "BusinessProcessRoutePointRef", "category": "RoutePointRef"},
|
||||||
],
|
],
|
||||||
"Task": [
|
"Task": [
|
||||||
{"prefix": "TaskObject", "category": "Object"},
|
{"prefix": "TaskObject", "category": "Object"},
|
||||||
@@ -254,13 +313,36 @@ GENERATED_TYPES = {
|
|||||||
"DefinedType": [
|
"DefinedType": [
|
||||||
{"prefix": "DefinedType", "category": "DefinedType"},
|
{"prefix": "DefinedType", "category": "DefinedType"},
|
||||||
],
|
],
|
||||||
|
"Sequence": [
|
||||||
|
{"prefix": "SequenceRecord", "category": "Record"},
|
||||||
|
{"prefix": "SequenceManager", "category": "Manager"},
|
||||||
|
{"prefix": "SequenceRecordSet", "category": "RecordSet"},
|
||||||
|
],
|
||||||
|
"FilterCriterion": [
|
||||||
|
{"prefix": "FilterCriterionManager", "category": "Manager"},
|
||||||
|
{"prefix": "FilterCriterionList", "category": "List"},
|
||||||
|
],
|
||||||
|
"SettingsStorage": [
|
||||||
|
{"prefix": "SettingsStorageManager", "category": "Manager"},
|
||||||
|
],
|
||||||
|
"IntegrationService": [
|
||||||
|
{"prefix": "IntegrationServiceManager", "category": "Manager"},
|
||||||
|
],
|
||||||
|
"WSReference": [
|
||||||
|
{"prefix": "WSReferenceManager", "category": "Manager"},
|
||||||
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Types that need ChildObjects element — fallback when the source object cannot be probed.
|
||||||
|
# The platform emits <ChildObjects> for every container type even when empty, and rejects
|
||||||
|
# the file without it ("expected ChildObjects"); primary signal is the source object itself.
|
||||||
TYPES_WITH_CHILD_OBJECTS = [
|
TYPES_WITH_CHILD_OBJECTS = [
|
||||||
"Catalog", "Document", "ExchangePlan", "ChartOfAccounts",
|
"Catalog", "Document", "ExchangePlan", "ChartOfAccounts",
|
||||||
"ChartOfCharacteristicTypes", "ChartOfCalculationTypes",
|
"ChartOfCharacteristicTypes", "ChartOfCalculationTypes",
|
||||||
"BusinessProcess", "Task", "Enum",
|
"BusinessProcess", "Task", "Enum",
|
||||||
"InformationRegister", "AccumulationRegister", "AccountingRegister", "CalculationRegister",
|
"InformationRegister", "AccumulationRegister", "AccountingRegister", "CalculationRegister",
|
||||||
|
"DataProcessor", "Report", "DocumentJournal", "FilterCriterion", "SettingsStorage",
|
||||||
|
"Sequence", "HTTPService", "WebService", "IntegrationService", "Subsystem",
|
||||||
]
|
]
|
||||||
|
|
||||||
COMMON_MODULE_PROPS = ["Global", "ClientManagedApplication", "Server", "ExternalConnection", "ClientOrdinaryApplication", "ServerCall"]
|
COMMON_MODULE_PROPS = ["Global", "ClientManagedApplication", "Server", "ExternalConnection", "ClientOrdinaryApplication", "ServerCall"]
|
||||||
@@ -286,6 +368,16 @@ XMLNS_DECL = (
|
|||||||
|
|
||||||
def detect_format_version(d):
|
def detect_format_version(d):
|
||||||
while d:
|
while d:
|
||||||
|
# Автономная внешняя обработка/отчёт: своего Configuration.xml у неё нет, версию несёт
|
||||||
|
# корень самой обработки. Без этого форма и макет внутри обработки 2.21 писались бы 2.17.
|
||||||
|
ext_path = d + ".xml"
|
||||||
|
if os.path.isfile(ext_path):
|
||||||
|
with open(ext_path, "r", encoding="utf-8-sig") as f:
|
||||||
|
ext_head = f.read(2000)
|
||||||
|
if re.search(r'<(ExternalDataProcessor|ExternalReport)[ >]', ext_head):
|
||||||
|
m = re.search(r'<MetaDataObject[^>]+version="(\d+\.\d+)"', ext_head)
|
||||||
|
if m:
|
||||||
|
return m.group(1)
|
||||||
cfg_path = os.path.join(d, "Configuration.xml")
|
cfg_path = os.path.join(d, "Configuration.xml")
|
||||||
if os.path.isfile(cfg_path):
|
if os.path.isfile(cfg_path):
|
||||||
with open(cfg_path, "r", encoding="utf-8-sig") as f:
|
with open(cfg_path, "r", encoding="utf-8-sig") as f:
|
||||||
@@ -300,6 +392,23 @@ def detect_format_version(d):
|
|||||||
return "2.17"
|
return "2.17"
|
||||||
|
|
||||||
|
|
||||||
|
def format_rank(ver):
|
||||||
|
""""2.20" → 220, "2.9" → 209. Строковое сравнение неверно ("2.9" > "2.17")."""
|
||||||
|
m = re.match(r'^(\d+)\.(\d+)$', ver or '')
|
||||||
|
return int(m.group(1)) * 100 + int(m.group(2)) if m else 0
|
||||||
|
|
||||||
|
|
||||||
|
def apply_pal_ns(format_version):
|
||||||
|
"""2.21 (8.5) добавила в шапку пространство палитры — ради <Color> у значений перечисления.
|
||||||
|
Вставляем НА МЕСТО (после lf, перед style): платформа держит объявления по алфавиту,
|
||||||
|
дописать в конец нельзя."""
|
||||||
|
global XMLNS_DECL
|
||||||
|
if format_rank(format_version) >= 221:
|
||||||
|
XMLNS_DECL = XMLNS_DECL.replace(
|
||||||
|
' xmlns:style=',
|
||||||
|
' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette" xmlns:style=')
|
||||||
|
|
||||||
|
|
||||||
def get_child_indent(container):
|
def get_child_indent(container):
|
||||||
if container.text and "\n" in container.text:
|
if container.text and "\n" in container.text:
|
||||||
after_nl = container.text.rsplit("\n", 1)[-1]
|
after_nl = container.text.rsplit("\n", 1)[-1]
|
||||||
@@ -366,21 +475,22 @@ def _detect_xml_style(path):
|
|||||||
|
|
||||||
|
|
||||||
def _finalize_xml_bytes(xml_bytes, style):
|
def _finalize_xml_bytes(xml_bytes, style):
|
||||||
"""Привести сериализованные байты к стилю оригинала (или к дефолту, если style is None)."""
|
"""Привести байты к стилю оригинала; для НОВОГО файла (style is None) — к канону
|
||||||
enc_decl = style["enc"] if style else "utf-8"
|
выгрузки Конфигуратора: encoding="UTF-8", CRLF в разделителях, без перевода в конце."""
|
||||||
|
enc_decl = style["enc"] if style else "UTF-8"
|
||||||
xml_bytes = xml_bytes.replace(
|
xml_bytes = xml_bytes.replace(
|
||||||
b"<?xml version='1.0' encoding='UTF-8'?>",
|
b"<?xml version='1.0' encoding='UTF-8'?>",
|
||||||
b'<?xml version="1.0" encoding="' + enc_decl.encode("ascii") + b'"?>')
|
b'<?xml version="1.0" encoding="' + enc_decl.encode("ascii") + b'"?>')
|
||||||
# Канонизировать переносы к LF (убирает от \r в tail'ах)
|
# Канонизировать переносы к LF (убирает от \r в tail'ах)
|
||||||
xml_bytes = (xml_bytes.replace(b" \n", b"\n").replace(b" ", b"")
|
xml_bytes = (xml_bytes.replace(b" \n", b"\n").replace(b" ", b"")
|
||||||
.replace(b"\r\n", b"\n").replace(b"\r", b"\n"))
|
.replace(b"\r\n", b"\n").replace(b"\r", b"\n"))
|
||||||
# Финальный перенос — как в оригинале (новый файл → есть)
|
# Финальный перенос — как в оригинале (новый файл → нет, канон #57)
|
||||||
want_final_nl = style["final_nl"] if style else True
|
want_final_nl = style["final_nl"] if style else False
|
||||||
xml_bytes = xml_bytes.rstrip(b"\n")
|
xml_bytes = xml_bytes.rstrip(b"\n")
|
||||||
if want_final_nl:
|
if want_final_nl:
|
||||||
xml_bytes += b"\n"
|
xml_bytes += b"\n"
|
||||||
# EOL — как в оригинале (новый файл → LF, текущее поведение)
|
# EOL — как в оригинале (новый файл → CRLF, канон #57)
|
||||||
if style and style["crlf"]:
|
if (style["crlf"] if style else True):
|
||||||
xml_bytes = xml_bytes.replace(b"\n", b"\r\n")
|
xml_bytes = xml_bytes.replace(b"\n", b"\r\n")
|
||||||
return xml_bytes
|
return xml_bytes
|
||||||
|
|
||||||
@@ -395,9 +505,24 @@ def save_xml_bom(tree, path):
|
|||||||
f.write(xml_bytes)
|
f.write(xml_bytes)
|
||||||
|
|
||||||
|
|
||||||
def save_text_bom(path, text):
|
def write_utf8_bom(path, content):
|
||||||
with open(path, "w", encoding="utf-8-sig") as fh:
|
# newline='' — без трансляции: иначе текстовый режим Python дал бы CRLF на Windows
|
||||||
fh.write(text)
|
# и LF на macOS, то есть вывод навыка зависел бы от ОС.
|
||||||
|
with open(path, 'w', encoding='utf-8-sig', newline='') as f:
|
||||||
|
f.write(content)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def write_xml_file(path, content):
|
||||||
|
"""XML в каноне выгрузки Конфигуратора: CRLF в разделителях, без перевода в конце.
|
||||||
|
|
||||||
|
Копия этой функции есть в каждом навыке-эмиттере (навыки автономны). Держать
|
||||||
|
копии одинаковыми — сознательно: разошедшиеся копии сводят на нет весь смысл.
|
||||||
|
|
||||||
|
Только для файлов, которые СОЗДАЁМ: правка существующего наследует его стиль.
|
||||||
|
"""
|
||||||
|
text = content.replace('\r\n', '\n').replace('\n', '\r\n').rstrip('\r\n')
|
||||||
|
write_utf8_bom(path, text)
|
||||||
|
|
||||||
|
|
||||||
def new_guid():
|
def new_guid():
|
||||||
@@ -412,7 +537,7 @@ def main():
|
|||||||
parser.add_argument("-ConfigPath", required=True)
|
parser.add_argument("-ConfigPath", required=True)
|
||||||
parser.add_argument("-Object", required=True)
|
parser.add_argument("-Object", required=True)
|
||||||
parser.add_argument("-BorrowMainAttribute", nargs="?", const="Form", default=None)
|
parser.add_argument("-BorrowMainAttribute", nargs="?", const="Form", default=None)
|
||||||
args = parser.parse_args()
|
args = ci_parse_args(parser)
|
||||||
|
|
||||||
# --- 1. Resolve paths ---
|
# --- 1. Resolve paths ---
|
||||||
ext_path = args.ExtensionPath
|
ext_path = args.ExtensionPath
|
||||||
@@ -448,6 +573,7 @@ def main():
|
|||||||
cfg_dir = os.path.dirname(cfg_resolved)
|
cfg_dir = os.path.dirname(cfg_resolved)
|
||||||
|
|
||||||
format_version = detect_format_version(ext_dir)
|
format_version = detect_format_version(ext_dir)
|
||||||
|
apply_pal_ns(format_version)
|
||||||
|
|
||||||
# --- 2. Load extension Configuration.xml ---
|
# --- 2. Load extension Configuration.xml ---
|
||||||
xml_parser = etree.XMLParser(remove_blank_text=False)
|
xml_parser = etree.XMLParser(remove_blank_text=False)
|
||||||
@@ -492,6 +618,45 @@ def main():
|
|||||||
borrowed_files = []
|
borrowed_files = []
|
||||||
|
|
||||||
# --- Helper functions ---
|
# --- Helper functions ---
|
||||||
|
def get_source_attribute_uuids(type_name, obj_name):
|
||||||
|
"""Имена реквизитов исходного объекта → uuid. Нужны для непрозрачной формы пути в ссылках
|
||||||
|
параметров выбора (см. rewrite_choice_parameter_links)."""
|
||||||
|
result = {}
|
||||||
|
dir_name = CHILD_TYPE_DIR_MAP.get(type_name)
|
||||||
|
if not dir_name:
|
||||||
|
return result
|
||||||
|
src_file = os.path.join(cfg_dir, dir_name, f"{obj_name}.xml")
|
||||||
|
if not os.path.isfile(src_file):
|
||||||
|
return result
|
||||||
|
|
||||||
|
tree = etree.parse(src_file, etree.XMLParser(remove_blank_text=True))
|
||||||
|
obj_el = None
|
||||||
|
for c in tree.getroot():
|
||||||
|
if isinstance(c.tag, str):
|
||||||
|
obj_el = c
|
||||||
|
break
|
||||||
|
if obj_el is None:
|
||||||
|
return result
|
||||||
|
for child in obj_el:
|
||||||
|
if not isinstance(child.tag, str) or localname(child) != "ChildObjects":
|
||||||
|
continue
|
||||||
|
for sub in child:
|
||||||
|
if not isinstance(sub.tag, str) or localname(sub) not in ("Attribute", "TabularSection"):
|
||||||
|
continue
|
||||||
|
uuid_val = sub.get("uuid")
|
||||||
|
name_val = None
|
||||||
|
for props in sub:
|
||||||
|
if isinstance(props.tag, str) and localname(props) == "Properties":
|
||||||
|
for prop in props:
|
||||||
|
if isinstance(prop.tag, str) and localname(prop) == "Name":
|
||||||
|
name_val = (prop.text or "").strip()
|
||||||
|
break
|
||||||
|
break
|
||||||
|
if uuid_val and name_val:
|
||||||
|
result[name_val] = uuid_val
|
||||||
|
break
|
||||||
|
return result
|
||||||
|
|
||||||
def read_source_object(type_name, obj_name):
|
def read_source_object(type_name, obj_name):
|
||||||
dir_name = CHILD_TYPE_DIR_MAP.get(type_name)
|
dir_name = CHILD_TYPE_DIR_MAP.get(type_name)
|
||||||
if not dir_name:
|
if not dir_name:
|
||||||
@@ -536,6 +701,9 @@ def main():
|
|||||||
type_xml = etree.tostring(type_node, encoding="unicode")
|
type_xml = etree.tostring(type_node, encoding="unicode")
|
||||||
src_props["__TypeXml"] = re.sub(r'\s+xmlns(?::\w+)?="[^"]*"', '', type_xml)
|
src_props["__TypeXml"] = re.sub(r'\s+xmlns(?::\w+)?="[^"]*"', '', type_xml)
|
||||||
|
|
||||||
|
# Whether the platform emits <ChildObjects> for this type — the source object is the ground truth
|
||||||
|
src_props["__HasChildObjects"] = src_el.find(f"{{{MD_NS}}}ChildObjects") is not None
|
||||||
|
|
||||||
return {"Uuid": src_uuid, "Properties": src_props, "Element": src_el}
|
return {"Uuid": src_uuid, "Properties": src_props, "Element": src_el}
|
||||||
|
|
||||||
def read_source_form_uuid(type_name, obj_name, form_name):
|
def read_source_form_uuid(type_name, obj_name, form_name):
|
||||||
@@ -612,7 +780,7 @@ def main():
|
|||||||
|
|
||||||
lines.append("\t\t</Properties>")
|
lines.append("\t\t</Properties>")
|
||||||
|
|
||||||
if type_name in TYPES_WITH_CHILD_OBJECTS:
|
if source_props.get("__HasChildObjects") or type_name in TYPES_WITH_CHILD_OBJECTS:
|
||||||
lines.append("\t\t<ChildObjects/>")
|
lines.append("\t\t<ChildObjects/>")
|
||||||
|
|
||||||
lines.append(f"\t</{type_name}>")
|
lines.append(f"\t</{type_name}>")
|
||||||
@@ -713,6 +881,39 @@ def main():
|
|||||||
save_xml_bom(obj_tree, obj_file)
|
save_xml_bom(obj_tree, obj_file)
|
||||||
info(f" Registered form in: {obj_file}")
|
info(f" Registered form in: {obj_file}")
|
||||||
|
|
||||||
|
# --- 11b1. Секции основного реквизита исходной формы, кроме Type/MainAttribute/SavedData ---
|
||||||
|
# Это <UseAlways> и <Columns> (доп. колонки табличных частей, объявленные прямо в форме). Их
|
||||||
|
# переносит Конфигуратор, и без них платформа отвергает форму: «Неверный путь к данным» на
|
||||||
|
# колонке, которой у объекта нет (Объект.Товары.Артикул). Возвращает список XML без xmlns.
|
||||||
|
def get_main_attribute_extra_xml(form_el, ns_strip_pattern):
|
||||||
|
result = []
|
||||||
|
main_attr = None
|
||||||
|
for child in form_el:
|
||||||
|
if not isinstance(child.tag, str) or localname(child) != "Attributes":
|
||||||
|
continue
|
||||||
|
for attr in child:
|
||||||
|
if not isinstance(attr.tag, str) or localname(attr) != "Attribute":
|
||||||
|
continue
|
||||||
|
for sub in attr:
|
||||||
|
if isinstance(sub.tag, str) and localname(sub) == "MainAttribute" and (sub.text or "").strip() == "true":
|
||||||
|
main_attr = attr
|
||||||
|
break
|
||||||
|
if main_attr is not None:
|
||||||
|
break
|
||||||
|
break
|
||||||
|
if main_attr is None:
|
||||||
|
return result
|
||||||
|
for sub in main_attr:
|
||||||
|
if not isinstance(sub.tag, str):
|
||||||
|
continue
|
||||||
|
if localname(sub) in ("Type", "MainAttribute", "SavedData"):
|
||||||
|
continue
|
||||||
|
# with_tail=False: хвостовой пробельный узел — часть родителя, а не секции; иначе в
|
||||||
|
# вывод попадают пустые строки, которых нет у PS (OuterXml хвост не включает).
|
||||||
|
xml = decode_numeric_entities(etree.tostring(sub, encoding="unicode", with_tail=False))
|
||||||
|
result.append(ns_strip_pattern.sub("", xml))
|
||||||
|
return result
|
||||||
|
|
||||||
# --- 11b. Collect DataPath references from source Form.xml ---
|
# --- 11b. Collect DataPath references from source Form.xml ---
|
||||||
def collect_form_data_paths(form_xml_path):
|
def collect_form_data_paths(form_xml_path):
|
||||||
with open(form_xml_path, "r", encoding="utf-8-sig") as fh:
|
with open(form_xml_path, "r", encoding="utf-8-sig") as fh:
|
||||||
@@ -754,6 +955,15 @@ def main():
|
|||||||
seg2 = segments[2] if len(segments) >= 3 else None
|
seg2 = segments[2] if len(segments) >= 3 else None
|
||||||
deep_paths.append({"ObjectAttr": seg0, "SubAttr": seg1, "SubSubAttr": seg2})
|
deep_paths.append({"ObjectAttr": seg0, "SubAttr": seg1, "SubSubAttr": seg2})
|
||||||
|
|
||||||
|
# Also scan <AdditionalColumns table="Объект.X"> — доп. колонки табличной части, объявленные в
|
||||||
|
# самой форме (напр. Объект.Товары.Артикул). Такая ТЧ может больше нигде на форме не встречаться,
|
||||||
|
# и без её заимствования платформа отвергает форму: «Неверный путь к данным».
|
||||||
|
for m in re.finditer(r'<AdditionalColumns table="Объект\.(\w+)"', content):
|
||||||
|
seg0 = m.group(1)
|
||||||
|
if seg0 in STANDARD_FIELDS:
|
||||||
|
continue
|
||||||
|
first_level[seg0] = True
|
||||||
|
|
||||||
# Deduplicate deep paths
|
# Deduplicate deep paths
|
||||||
seen = set()
|
seen = set()
|
||||||
unique_deep = []
|
unique_deep = []
|
||||||
@@ -876,10 +1086,18 @@ def main():
|
|||||||
extra_props = {}
|
extra_props = {}
|
||||||
props_node = src_el.find(f"{{{MD_NS}}}Properties")
|
props_node = src_el.find(f"{{{MD_NS}}}Properties")
|
||||||
if props_node is not None:
|
if props_node is not None:
|
||||||
|
# NumberPeriodicity сюда НЕ входит: платформа считает его модификацией настроек нумерации и
|
||||||
|
# тогда требует объявить ещё и <Numerator/>, иначе /UpdateDBCfg падает — «отключать
|
||||||
|
# контролируемость свойства "Нумератор" недопустимо». Конфигуратор его не переносит
|
||||||
|
# (эталон заимствования документа: NumberType/NumberLength/NumberAllowedLength и всё).
|
||||||
|
# Загрузку это не ломает, ошибка вылезает только на обновлении конфигурации БД.
|
||||||
|
# FoldersOnTop сюда НЕ входит: платформа его у заимствованной оболочки не хранит — при
|
||||||
|
# загрузке молча выбрасывает (проверено раундтрипом: записали, выгрузили обратно, свойства
|
||||||
|
# нет). Конфигуратор его тоже не переносит. Остальные из списка сохраняются.
|
||||||
props_to_extract = [
|
props_to_extract = [
|
||||||
"Hierarchical", "FoldersOnTop", "CodeLength", "DescriptionLength",
|
"Hierarchical", "CodeLength", "DescriptionLength",
|
||||||
"CodeType", "CodeAllowedLength", "NumberType", "NumberLength",
|
"CodeType", "CodeAllowedLength", "NumberType", "NumberLength",
|
||||||
"NumberAllowedLength", "NumberPeriodicity",
|
"NumberAllowedLength",
|
||||||
]
|
]
|
||||||
for p_name in props_to_extract:
|
for p_name in props_to_extract:
|
||||||
p_node = props_node.find(f"{{{MD_NS}}}{p_name}")
|
p_node = props_node.find(f"{{{MD_NS}}}{p_name}")
|
||||||
@@ -971,7 +1189,9 @@ def main():
|
|||||||
warn(f"Cannot merge attributes: {obj_file} not found")
|
warn(f"Cannot merge attributes: {obj_file} not found")
|
||||||
return
|
return
|
||||||
|
|
||||||
with open(obj_file, "r", encoding="utf-8-sig") as fh:
|
# newline="" => без трансляции: иначе CRLF молча схлопнется в LF при чтении
|
||||||
|
# и файл будет переписан в LF.
|
||||||
|
with open(obj_file, "r", encoding="utf-8-sig", newline="") as fh:
|
||||||
obj_content = fh.read()
|
obj_content = fh.read()
|
||||||
|
|
||||||
# Collect existing attribute names for dedup (text-based)
|
# Collect existing attribute names for dedup (text-based)
|
||||||
@@ -993,7 +1213,7 @@ def main():
|
|||||||
obj_content = re.sub(r'<ChildObjects\s*/>', f"<ChildObjects>{all_attr_xml}\r\n\t\t</ChildObjects>", obj_content)
|
obj_content = re.sub(r'<ChildObjects\s*/>', f"<ChildObjects>{all_attr_xml}\r\n\t\t</ChildObjects>", obj_content)
|
||||||
else:
|
else:
|
||||||
obj_content = obj_content.replace("</ChildObjects>", f"{all_attr_xml}\r\n\t\t</ChildObjects>")
|
obj_content = obj_content.replace("</ChildObjects>", f"{all_attr_xml}\r\n\t\t</ChildObjects>")
|
||||||
save_text_bom(obj_file, obj_content)
|
write_utf8_bom(obj_file, obj_content)
|
||||||
info(f" Merged {added} attribute(s) into: {obj_file}")
|
info(f" Merged {added} attribute(s) into: {obj_file}")
|
||||||
|
|
||||||
# --- 11h. Borrow main attribute orchestrator ---
|
# --- 11h. Borrow main attribute orchestrator ---
|
||||||
@@ -1030,7 +1250,9 @@ def main():
|
|||||||
obj_file = os.path.join(ext_dir, dir_name, f"{obj_name}.xml")
|
obj_file = os.path.join(ext_dir, dir_name, f"{obj_name}.xml")
|
||||||
|
|
||||||
# Read existing object XML (needed for dedup + enrichment)
|
# Read existing object XML (needed for dedup + enrichment)
|
||||||
with open(obj_file, "r", encoding="utf-8-sig") as fh:
|
# newline="" => без трансляции: иначе CRLF молча схлопнется в LF при чтении
|
||||||
|
# и файл будет переписан в LF.
|
||||||
|
with open(obj_file, "r", encoding="utf-8-sig", newline="") as fh:
|
||||||
obj_content = fh.read()
|
obj_content = fh.read()
|
||||||
|
|
||||||
# Dedup: skip attributes/TS already present in object's ChildObjects (idempotent re-borrow)
|
# Dedup: skip attributes/TS already present in object's ChildObjects (idempotent re-borrow)
|
||||||
@@ -1078,7 +1300,7 @@ def main():
|
|||||||
f"<ChildObjects>{existing_inner}\r\n{adopted_content}\r\n\t\t</ChildObjects>"
|
f"<ChildObjects>{existing_inner}\r\n{adopted_content}\r\n\t\t</ChildObjects>"
|
||||||
)
|
)
|
||||||
|
|
||||||
save_text_bom(obj_file, obj_content)
|
write_utf8_bom(obj_file, obj_content)
|
||||||
info(f" Enriched object: {obj_file}")
|
info(f" Enriched object: {obj_file}")
|
||||||
|
|
||||||
# Step 4: Collect all reference types and borrow as shells
|
# Step 4: Collect all reference types and borrow as shells
|
||||||
@@ -1088,6 +1310,18 @@ def main():
|
|||||||
for ts in src_ts:
|
for ts in src_ts:
|
||||||
for tsa in ts["Attributes"]:
|
for tsa in ts["Attributes"]:
|
||||||
all_type_xmls.append(tsa["TypeXml"])
|
all_type_xmls.append(tsa["TypeXml"])
|
||||||
|
# Типы из <Columns> основного реквизита формы: колонку мы переносим (borrow_form), значит и её
|
||||||
|
# тип должен быть заимствован — иначе колонка ссылается на DefinedType/справочник, которого в
|
||||||
|
# расширении нет. Конфигуратор поступает так же (эталон: DefinedTypes/Артикул при заимствовании
|
||||||
|
# формы заказа поставщику).
|
||||||
|
src_form_for_cols = os.path.join(cfg_dir, dir_name, obj_name, "Forms", form_name, "Ext", "Form.xml")
|
||||||
|
if os.path.isfile(src_form_for_cols):
|
||||||
|
cols_tree = etree.parse(src_form_for_cols)
|
||||||
|
cols_ns_strip = re.compile(r'\s+xmlns(?::\w+)?="[^"]*"')
|
||||||
|
for extra_xml in get_main_attribute_extra_xml(cols_tree.getroot(), cols_ns_strip):
|
||||||
|
if extra_xml.startswith("<Columns"):
|
||||||
|
all_type_xmls.append(extra_xml)
|
||||||
|
|
||||||
ref_types = collect_reference_types(all_type_xmls)
|
ref_types = collect_reference_types(all_type_xmls)
|
||||||
info(f" Reference types to borrow: {len(ref_types)}")
|
info(f" Reference types to borrow: {len(ref_types)}")
|
||||||
|
|
||||||
@@ -1107,7 +1341,7 @@ def main():
|
|||||||
target_dir = os.path.join(ext_dir, CHILD_TYPE_DIR_MAP[rt["TypeName"]])
|
target_dir = os.path.join(ext_dir, CHILD_TYPE_DIR_MAP[rt["TypeName"]])
|
||||||
os.makedirs(target_dir, exist_ok=True)
|
os.makedirs(target_dir, exist_ok=True)
|
||||||
target_file = os.path.join(target_dir, f"{rt['ObjName']}.xml")
|
target_file = os.path.join(target_dir, f"{rt['ObjName']}.xml")
|
||||||
save_text_bom(target_file, borrowed_xml)
|
write_xml_file(target_file, borrowed_xml)
|
||||||
add_to_child_objects(rt["TypeName"], rt["ObjName"])
|
add_to_child_objects(rt["TypeName"], rt["ObjName"])
|
||||||
borrowed_files.append(target_file)
|
borrowed_files.append(target_file)
|
||||||
info(f" Auto-borrowed: {rt['TypeName']}.{rt['ObjName']}")
|
info(f" Auto-borrowed: {rt['TypeName']}.{rt['ObjName']}")
|
||||||
@@ -1172,7 +1406,7 @@ def main():
|
|||||||
t_target_dir = os.path.join(ext_dir, CHILD_TYPE_DIR_MAP[target_type_name])
|
t_target_dir = os.path.join(ext_dir, CHILD_TYPE_DIR_MAP[target_type_name])
|
||||||
os.makedirs(t_target_dir, exist_ok=True)
|
os.makedirs(t_target_dir, exist_ok=True)
|
||||||
t_target_file = os.path.join(t_target_dir, f"{target_obj_name}.xml")
|
t_target_file = os.path.join(t_target_dir, f"{target_obj_name}.xml")
|
||||||
save_text_bom(t_target_file, t_borrowed_xml)
|
write_xml_file(t_target_file, t_borrowed_xml)
|
||||||
add_to_child_objects(target_type_name, target_obj_name)
|
add_to_child_objects(target_type_name, target_obj_name)
|
||||||
borrowed_files.append(t_target_file)
|
borrowed_files.append(t_target_file)
|
||||||
info(f" Auto-borrowed for deep path: {target_type_name}.{target_obj_name}")
|
info(f" Auto-borrowed for deep path: {target_type_name}.{target_obj_name}")
|
||||||
@@ -1196,7 +1430,7 @@ def main():
|
|||||||
s_target_dir = os.path.join(ext_dir, CHILD_TYPE_DIR_MAP[srt["TypeName"]])
|
s_target_dir = os.path.join(ext_dir, CHILD_TYPE_DIR_MAP[srt["TypeName"]])
|
||||||
os.makedirs(s_target_dir, exist_ok=True)
|
os.makedirs(s_target_dir, exist_ok=True)
|
||||||
s_target_file = os.path.join(s_target_dir, f"{srt['ObjName']}.xml")
|
s_target_file = os.path.join(s_target_dir, f"{srt['ObjName']}.xml")
|
||||||
save_text_bom(s_target_file, s_borrowed_xml)
|
write_xml_file(s_target_file, s_borrowed_xml)
|
||||||
add_to_child_objects(srt["TypeName"], srt["ObjName"])
|
add_to_child_objects(srt["TypeName"], srt["ObjName"])
|
||||||
borrowed_files.append(s_target_file)
|
borrowed_files.append(s_target_file)
|
||||||
info(f" Auto-borrowed (deep): {srt['TypeName']}.{srt['ObjName']}")
|
info(f" Auto-borrowed (deep): {srt['TypeName']}.{srt['ObjName']}")
|
||||||
@@ -1253,7 +1487,7 @@ def main():
|
|||||||
os.makedirs(form_meta_dir, exist_ok=True)
|
os.makedirs(form_meta_dir, exist_ok=True)
|
||||||
|
|
||||||
form_meta_file = os.path.join(form_meta_dir, f"{form_name}.xml")
|
form_meta_file = os.path.join(form_meta_dir, f"{form_name}.xml")
|
||||||
save_text_bom(form_meta_file, "\n".join(form_meta_lines))
|
write_xml_file(form_meta_file, "\n".join(form_meta_lines))
|
||||||
info(f" Created: {form_meta_file}")
|
info(f" Created: {form_meta_file}")
|
||||||
|
|
||||||
# 5. Generate Form.xml with BaseForm
|
# 5. Generate Form.xml with BaseForm
|
||||||
@@ -1282,14 +1516,20 @@ def main():
|
|||||||
continue
|
continue
|
||||||
if not reached_visual:
|
if not reached_visual:
|
||||||
# Form-level properties before AutoCommandBar (WindowOpeningMode, AutoFillCheck, etc.)
|
# Form-level properties before AutoCommandBar (WindowOpeningMode, AutoFillCheck, etc.)
|
||||||
form_props.append(decode_numeric_entities(etree.tostring(fc, encoding="unicode")))
|
# with_tail=False — хвостовой пробел принадлежит родителю; с ним в вывод попадали
|
||||||
|
# пустые строки, которых нет у PS-порта (OuterXml хвост не включает).
|
||||||
|
form_props.append(decode_numeric_entities(etree.tostring(fc, encoding="unicode", with_tail=False)))
|
||||||
|
|
||||||
ns_strip_pattern = re.compile(r'\s+xmlns(?::\w+)?="[^"]*"')
|
ns_strip_pattern = re.compile(r'\s+xmlns(?::\w+)?="[^"]*"')
|
||||||
|
|
||||||
|
# uuid реквизитов объекта — только для формы без заимствованного основного реквизита:
|
||||||
|
# там ссылки параметров выбора переводятся на непрозрачную форму пути
|
||||||
|
src_attr_uuids = {} if borrow_main_attr else get_source_attribute_uuids(type_name, obj_name)
|
||||||
|
|
||||||
# AutoCommandBar: keep ChildItems (buttons with CommandName->0), Autofill->false
|
# AutoCommandBar: keep ChildItems (buttons with CommandName->0), Autofill->false
|
||||||
auto_cmd_xml = ""
|
auto_cmd_xml = ""
|
||||||
if src_auto_cmd is not None:
|
if src_auto_cmd is not None:
|
||||||
auto_cmd_xml = decode_numeric_entities(etree.tostring(src_auto_cmd, encoding="unicode"))
|
auto_cmd_xml = decode_numeric_entities(etree.tostring(src_auto_cmd, encoding="unicode", with_tail=False))
|
||||||
auto_cmd_xml = ns_strip_pattern.sub("", auto_cmd_xml)
|
auto_cmd_xml = ns_strip_pattern.sub("", auto_cmd_xml)
|
||||||
auto_cmd_xml = re.sub(r'<CommandName>[^<]*</CommandName>', '<CommandName>0</CommandName>', auto_cmd_xml)
|
auto_cmd_xml = re.sub(r'<CommandName>[^<]*</CommandName>', '<CommandName>0</CommandName>', auto_cmd_xml)
|
||||||
auto_cmd_xml = auto_cmd_xml.replace('<Autofill>true</Autofill>', '<Autofill>false</Autofill>')
|
auto_cmd_xml = auto_cmd_xml.replace('<Autofill>true</Autofill>', '<Autofill>false</Autofill>')
|
||||||
@@ -1297,6 +1537,8 @@ def main():
|
|||||||
auto_cmd_xml = re.sub(r'\s*<ExcludedCommand>[^<]*</ExcludedCommand>', '', auto_cmd_xml)
|
auto_cmd_xml = re.sub(r'\s*<ExcludedCommand>[^<]*</ExcludedCommand>', '', auto_cmd_xml)
|
||||||
# Strip data-binding tags whose root attribute isn't borrowed
|
# Strip data-binding tags whose root attribute isn't borrowed
|
||||||
auto_cmd_xml = strip_form_bindings(auto_cmd_xml, borrow_main_attr)
|
auto_cmd_xml = strip_form_bindings(auto_cmd_xml, borrow_main_attr)
|
||||||
|
if not borrow_main_attr:
|
||||||
|
auto_cmd_xml = rewrite_choice_parameter_links(auto_cmd_xml, src_attr_uuids)
|
||||||
|
|
||||||
# ChildItems: copy full tree, clean up base-config references
|
# ChildItems: copy full tree, clean up base-config references
|
||||||
child_items_xml = ""
|
child_items_xml = ""
|
||||||
@@ -1307,12 +1549,14 @@ def main():
|
|||||||
break
|
break
|
||||||
|
|
||||||
if src_child_items is not None:
|
if src_child_items is not None:
|
||||||
child_items_xml = decode_numeric_entities(etree.tostring(src_child_items, encoding="unicode"))
|
child_items_xml = decode_numeric_entities(etree.tostring(src_child_items, encoding="unicode", with_tail=False))
|
||||||
child_items_xml = ns_strip_pattern.sub("", child_items_xml)
|
child_items_xml = ns_strip_pattern.sub("", child_items_xml)
|
||||||
# Replace all CommandName values with 0
|
# Replace all CommandName values with 0
|
||||||
child_items_xml = re.sub(r'<CommandName>[^<]*</CommandName>', '<CommandName>0</CommandName>', child_items_xml)
|
child_items_xml = re.sub(r'<CommandName>[^<]*</CommandName>', '<CommandName>0</CommandName>', child_items_xml)
|
||||||
# Strip data-binding tags whose root attribute isn't borrowed
|
# Strip data-binding tags whose root attribute isn't borrowed
|
||||||
child_items_xml = strip_form_bindings(child_items_xml, borrow_main_attr)
|
child_items_xml = strip_form_bindings(child_items_xml, borrow_main_attr)
|
||||||
|
if not borrow_main_attr:
|
||||||
|
child_items_xml = rewrite_choice_parameter_links(child_items_xml, src_attr_uuids)
|
||||||
# Strip ExcludedCommand in nested AutoCommandBars (references to standard commands invalid in extension)
|
# Strip ExcludedCommand in nested AutoCommandBars (references to standard commands invalid in extension)
|
||||||
child_items_xml = re.sub(r'\s*<ExcludedCommand>[^<]*</ExcludedCommand>', '', child_items_xml)
|
child_items_xml = re.sub(r'\s*<ExcludedCommand>[^<]*</ExcludedCommand>', '', child_items_xml)
|
||||||
# Strip TypeLink blocks with human-readable DataPath (Items.XXX)
|
# Strip TypeLink blocks with human-readable DataPath (Items.XXX)
|
||||||
@@ -1339,7 +1583,7 @@ def main():
|
|||||||
target_dir = os.path.join(ext_dir, "CommonPictures")
|
target_dir = os.path.join(ext_dir, "CommonPictures")
|
||||||
os.makedirs(target_dir, exist_ok=True)
|
os.makedirs(target_dir, exist_ok=True)
|
||||||
target_file = os.path.join(target_dir, f"{pic_name}.xml")
|
target_file = os.path.join(target_dir, f"{pic_name}.xml")
|
||||||
save_text_bom(target_file, borrowed_xml)
|
write_xml_file(target_file, borrowed_xml)
|
||||||
add_to_child_objects("CommonPicture", pic_name)
|
add_to_child_objects("CommonPicture", pic_name)
|
||||||
auto_borrowed_pics.append(pic_name)
|
auto_borrowed_pics.append(pic_name)
|
||||||
borrowed_files.append(target_file)
|
borrowed_files.append(target_file)
|
||||||
@@ -1388,7 +1632,7 @@ def main():
|
|||||||
target_dir = os.path.join(ext_dir, "StyleItems")
|
target_dir = os.path.join(ext_dir, "StyleItems")
|
||||||
os.makedirs(target_dir, exist_ok=True)
|
os.makedirs(target_dir, exist_ok=True)
|
||||||
target_file = os.path.join(target_dir, f"{style_name}.xml")
|
target_file = os.path.join(target_dir, f"{style_name}.xml")
|
||||||
save_text_bom(target_file, borrowed_xml)
|
write_xml_file(target_file, borrowed_xml)
|
||||||
add_to_child_objects("StyleItem", style_name)
|
add_to_child_objects("StyleItem", style_name)
|
||||||
borrowed_files.append(target_file)
|
borrowed_files.append(target_file)
|
||||||
info(f" Auto-borrowed: StyleItem.{style_name}")
|
info(f" Auto-borrowed: StyleItem.{style_name}")
|
||||||
@@ -1452,14 +1696,17 @@ def main():
|
|||||||
target_dir = os.path.join(ext_dir, "Enums")
|
target_dir = os.path.join(ext_dir, "Enums")
|
||||||
os.makedirs(target_dir, exist_ok=True)
|
os.makedirs(target_dir, exist_ok=True)
|
||||||
target_file = os.path.join(target_dir, f"{enum_name}.xml")
|
target_file = os.path.join(target_dir, f"{enum_name}.xml")
|
||||||
save_text_bom(target_file, borrowed_xml)
|
write_xml_file(target_file, borrowed_xml)
|
||||||
add_to_child_objects("Enum", enum_name)
|
add_to_child_objects("Enum", enum_name)
|
||||||
borrowed_files.append(target_file)
|
borrowed_files.append(target_file)
|
||||||
info(f" Auto-borrowed: Enum.{enum_name} (with {len(ev_xmls)} EnumValue(s))")
|
info(f" Auto-borrowed: Enum.{enum_name} (with {len(ev_xmls)} EnumValue(s))")
|
||||||
else:
|
else:
|
||||||
warn(f" Enum.{enum_name} not found in source config")
|
warn(f" Enum.{enum_name} not found in source config")
|
||||||
|
|
||||||
# Extract the <Form ...> opening tag from source text
|
# Открывающий тег <Form ...> берём из исходной формы — ради её объявлений пространств
|
||||||
|
# имён, но version подставляем СВОЮ: форма обязана нести версию расширения, иначе
|
||||||
|
# платформа отвергает импорт (форма 2.13 внутри расширения 2.17). Раньше тег
|
||||||
|
# копировался целиком, и версия источника молча побеждала.
|
||||||
xml_decl = '<?xml version="1.0" encoding="UTF-8"?>'
|
xml_decl = '<?xml version="1.0" encoding="UTF-8"?>'
|
||||||
form_tag = f'<Form version="{form_version}">'
|
form_tag = f'<Form version="{form_version}">'
|
||||||
m_decl = re.search(r'^(<\?xml[^?]*\?>)', src_form_content)
|
m_decl = re.search(r'^(<\?xml[^?]*\?>)', src_form_content)
|
||||||
@@ -1467,7 +1714,15 @@ def main():
|
|||||||
xml_decl = m_decl.group(1)
|
xml_decl = m_decl.group(1)
|
||||||
m_tag = re.search(r'(<Form[^>]*>)', src_form_content)
|
m_tag = re.search(r'(<Form[^>]*>)', src_form_content)
|
||||||
if m_tag:
|
if m_tag:
|
||||||
form_tag = m_tag.group(1)
|
src_ns = re.sub(r'^<Form\s*', '', m_tag.group(1))
|
||||||
|
src_ns = re.sub(r'\s*/?>$', '', src_ns)
|
||||||
|
src_ns = re.sub(r'\s*version="[^"]*"', '', src_ns)
|
||||||
|
# 2.21 (8.5): пространство палитры. Место строгое — после lf, перед style.
|
||||||
|
if format_rank(form_version) >= 221 and 'xmlns:pal=' not in src_ns:
|
||||||
|
src_ns = src_ns.replace(
|
||||||
|
' xmlns:style=',
|
||||||
|
' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette" xmlns:style=')
|
||||||
|
form_tag = f'<Form {src_ns} version="{form_version}">' if src_ns else f'<Form version="{form_version}">'
|
||||||
|
|
||||||
# Build output
|
# Build output
|
||||||
parts = []
|
parts = []
|
||||||
@@ -1485,6 +1740,11 @@ def main():
|
|||||||
if child_items_xml:
|
if child_items_xml:
|
||||||
parts.append(f"\t{child_items_xml}\r\n")
|
parts.append(f"\t{child_items_xml}\r\n")
|
||||||
|
|
||||||
|
# Секции основного реквизита исходной формы (<UseAlways>, <Columns>) — их нельзя терять
|
||||||
|
main_attr_extra = []
|
||||||
|
if borrow_main_attr:
|
||||||
|
main_attr_extra = get_main_attribute_extra_xml(src_form_el, ns_strip_pattern)
|
||||||
|
|
||||||
# Attributes: empty or with MainAttribute when borrow_main_attr
|
# Attributes: empty or with MainAttribute when borrow_main_attr
|
||||||
if borrow_main_attr:
|
if borrow_main_attr:
|
||||||
obj_type_prefix = ""
|
obj_type_prefix = ""
|
||||||
@@ -1499,6 +1759,8 @@ def main():
|
|||||||
parts.append(f"\t\t\t<Type><v8:Type>{main_attr_type}</v8:Type></Type>\r\n")
|
parts.append(f"\t\t\t<Type><v8:Type>{main_attr_type}</v8:Type></Type>\r\n")
|
||||||
parts.append("\t\t\t<MainAttribute>true</MainAttribute>\r\n")
|
parts.append("\t\t\t<MainAttribute>true</MainAttribute>\r\n")
|
||||||
parts.append("\t\t\t<SavedData>true</SavedData>\r\n")
|
parts.append("\t\t\t<SavedData>true</SavedData>\r\n")
|
||||||
|
for extra_xml in main_attr_extra:
|
||||||
|
parts.append(f"\t\t\t{extra_xml}\r\n")
|
||||||
parts.append("\t\t</Attribute>\r\n")
|
parts.append("\t\t</Attribute>\r\n")
|
||||||
parts.append("\t</Attributes>")
|
parts.append("\t</Attributes>")
|
||||||
else:
|
else:
|
||||||
@@ -1535,6 +1797,11 @@ def main():
|
|||||||
parts.append(f"\t\t\t\t<Type><v8:Type>{main_attr_type}</v8:Type></Type>\r\n")
|
parts.append(f"\t\t\t\t<Type><v8:Type>{main_attr_type}</v8:Type></Type>\r\n")
|
||||||
parts.append("\t\t\t\t<MainAttribute>true</MainAttribute>\r\n")
|
parts.append("\t\t\t\t<MainAttribute>true</MainAttribute>\r\n")
|
||||||
parts.append("\t\t\t\t<SavedData>true</SavedData>\r\n")
|
parts.append("\t\t\t\t<SavedData>true</SavedData>\r\n")
|
||||||
|
for extra_xml in main_attr_extra:
|
||||||
|
# В BaseForm та же секция на уровень глубже — приём переиндентации тот же, что у ChildItems
|
||||||
|
for li, line in enumerate(extra_xml.split("\n")):
|
||||||
|
parts.append(f"\t\t\t\t{line}" if li == 0 else f"\t{line}")
|
||||||
|
parts.append("\r\n")
|
||||||
parts.append("\t\t\t</Attribute>\r\n")
|
parts.append("\t\t\t</Attribute>\r\n")
|
||||||
parts.append("\t\t</Attributes>")
|
parts.append("\t\t</Attributes>")
|
||||||
else:
|
else:
|
||||||
@@ -1546,7 +1813,7 @@ def main():
|
|||||||
form_xml_dir = os.path.join(form_meta_dir, form_name, "Ext")
|
form_xml_dir = os.path.join(form_meta_dir, form_name, "Ext")
|
||||||
os.makedirs(form_xml_dir, exist_ok=True)
|
os.makedirs(form_xml_dir, exist_ok=True)
|
||||||
form_xml_file = os.path.join(form_xml_dir, "Form.xml")
|
form_xml_file = os.path.join(form_xml_dir, "Form.xml")
|
||||||
save_text_bom(form_xml_file, "".join(parts))
|
write_xml_file(form_xml_file, "".join(parts))
|
||||||
info(f" Created: {form_xml_file}")
|
info(f" Created: {form_xml_file}")
|
||||||
|
|
||||||
# 6. Create empty Module.bsl — but NEVER overwrite an existing one (re-borrow must
|
# 6. Create empty Module.bsl — but NEVER overwrite an existing one (re-borrow must
|
||||||
@@ -1557,7 +1824,7 @@ def main():
|
|||||||
if os.path.isfile(module_bsl_file):
|
if os.path.isfile(module_bsl_file):
|
||||||
info(" Preserved existing Module.bsl")
|
info(" Preserved existing Module.bsl")
|
||||||
else:
|
else:
|
||||||
save_text_bom(module_bsl_file, "")
|
write_utf8_bom(module_bsl_file, "")
|
||||||
info(f" Created: {module_bsl_file}")
|
info(f" Created: {module_bsl_file}")
|
||||||
|
|
||||||
# 7. Register form in parent object ChildObjects
|
# 7. Register form in parent object ChildObjects
|
||||||
@@ -1630,7 +1897,7 @@ def main():
|
|||||||
target_dir = os.path.join(ext_dir, dir_name)
|
target_dir = os.path.join(ext_dir, dir_name)
|
||||||
os.makedirs(target_dir, exist_ok=True)
|
os.makedirs(target_dir, exist_ok=True)
|
||||||
target_file = os.path.join(target_dir, f"{obj_name}.xml")
|
target_file = os.path.join(target_dir, f"{obj_name}.xml")
|
||||||
save_text_bom(target_file, borrowed_xml)
|
write_xml_file(target_file, borrowed_xml)
|
||||||
info(f" Created: {target_file}")
|
info(f" Created: {target_file}")
|
||||||
|
|
||||||
add_to_child_objects(type_name, obj_name)
|
add_to_child_objects(type_name, obj_name)
|
||||||
@@ -1657,7 +1924,7 @@ def main():
|
|||||||
os.makedirs(target_dir, exist_ok=True)
|
os.makedirs(target_dir, exist_ok=True)
|
||||||
|
|
||||||
target_file = os.path.join(target_dir, f"{obj_name}.xml")
|
target_file = os.path.join(target_dir, f"{obj_name}.xml")
|
||||||
save_text_bom(target_file, borrowed_xml)
|
write_xml_file(target_file, borrowed_xml)
|
||||||
info(f" Created: {target_file}")
|
info(f" Created: {target_file}")
|
||||||
|
|
||||||
add_to_child_objects(type_name, obj_name)
|
add_to_child_objects(type_name, obj_name)
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ allowed-tools:
|
|||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-diff.ps1" -ExtensionPath src -ConfigPath C:\cfsrc\erp -Mode A
|
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-diff.ps1" -ExtensionPath src\cfe\extname -ConfigPath src\cf -Mode A
|
||||||
```
|
```
|
||||||
|
|
||||||
## Mode A — обзор расширения
|
## Mode A — обзор расширения
|
||||||
@@ -50,8 +50,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-diff.ps1" -Exte
|
|||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Обзор — что изменено в расширении
|
# Обзор — что изменено в расширении
|
||||||
... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Mode A
|
... -ExtensionPath src\cfe\extname -ConfigPath src\cf -Mode A
|
||||||
|
|
||||||
# Проверка переноса — все ли #Вставка перенесены
|
# Проверка переноса — все ли #Вставка перенесены
|
||||||
... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Mode B
|
... -ExtensionPath src\cfe\extname -ConfigPath src\cf -Mode B
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# cfe-diff v1.0 — Analyze and compare 1C configuration extension (CFE)
|
# cfe-diff v1.2 — Analyze and compare 1C configuration extension (CFE) (+тип Bot; cfe-diff/cfe-borrow: недостающие типы)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory)]
|
||||||
@@ -49,7 +49,9 @@ $childTypeDirMap = @{
|
|||||||
"SettingsStorage"="SettingsStorages"; "FilterCriterion"="FilterCriteria"
|
"SettingsStorage"="SettingsStorages"; "FilterCriterion"="FilterCriteria"
|
||||||
"CommandGroup"="CommandGroups"; "DocumentNumerator"="DocumentNumerators"
|
"CommandGroup"="CommandGroups"; "DocumentNumerator"="DocumentNumerators"
|
||||||
"Sequence"="Sequences"; "IntegrationService"="IntegrationServices"
|
"Sequence"="Sequences"; "IntegrationService"="IntegrationServices"
|
||||||
"CommonAttribute"="CommonAttributes"
|
"CommonAttribute"="CommonAttributes"; "Style"="Styles"; "XDTOPackage"="XDTOPackages"
|
||||||
|
"WebService"="WebServices"; "HTTPService"="HTTPServices"; "WSReference"="WSReferences"
|
||||||
|
"Bot"="Bots"
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- Parse extension Configuration.xml ---
|
# --- Parse extension Configuration.xml ---
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# cfe-diff v1.0 — Analyze and compare 1C configuration extension (CFE)
|
# cfe-diff v1.2 — Analyze and compare 1C configuration extension (CFE) (+тип Bot; cfe-diff/cfe-borrow: недостающие типы)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -8,6 +8,28 @@ import re
|
|||||||
import sys
|
import sys
|
||||||
from lxml import etree
|
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)
|
||||||
|
|
||||||
|
|
||||||
# --- Namespace maps ---
|
# --- Namespace maps ---
|
||||||
|
|
||||||
MD_NSMAP = {
|
MD_NSMAP = {
|
||||||
@@ -60,6 +82,12 @@ CHILD_TYPE_DIR_MAP = {
|
|||||||
"Sequence": "Sequences",
|
"Sequence": "Sequences",
|
||||||
"IntegrationService": "IntegrationServices",
|
"IntegrationService": "IntegrationServices",
|
||||||
"CommonAttribute": "CommonAttributes",
|
"CommonAttribute": "CommonAttributes",
|
||||||
|
"Style": "Styles",
|
||||||
|
"XDTOPackage": "XDTOPackages",
|
||||||
|
"WebService": "WebServices",
|
||||||
|
"HTTPService": "HTTPServices",
|
||||||
|
"WSReference": "WSReferences",
|
||||||
|
"Bot": "Bots",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -468,7 +496,7 @@ def main():
|
|||||||
parser.add_argument("-ExtensionPath", required=True, help="Path to extension dump root")
|
parser.add_argument("-ExtensionPath", required=True, help="Path to extension dump root")
|
||||||
parser.add_argument("-ConfigPath", required=True, help="Path to base config dump root")
|
parser.add_argument("-ConfigPath", required=True, help="Path to base config dump root")
|
||||||
parser.add_argument("-Mode", choices=["A", "B"], default="A", help="A=overview, B=transfer check")
|
parser.add_argument("-Mode", choices=["A", "B"], default="A", help="A=overview, B=transfer check")
|
||||||
args = parser.parse_args()
|
args = ci_parse_args(parser)
|
||||||
|
|
||||||
extension_path = args.ExtensionPath
|
extension_path = args.ExtensionPath
|
||||||
config_path = args.ConfigPath
|
config_path = args.ConfigPath
|
||||||
|
|||||||
@@ -33,39 +33,39 @@ allowed-tools:
|
|||||||
| `Name` | Имя расширения (обязат.) | — |
|
| `Name` | Имя расширения (обязат.) | — |
|
||||||
| `Synonym` | Синоним | = Name |
|
| `Synonym` | Синоним | = Name |
|
||||||
| `NamePrefix` | Префикс собственных объектов | = Name + "_" |
|
| `NamePrefix` | Префикс собственных объектов | = Name + "_" |
|
||||||
| `OutputDir` | Каталог для создания | `src` |
|
| `OutputDir` | Каталог для создания; клади расширение в свой подкаталог, названный по имени: `src\cfe\<Name>` | `src` |
|
||||||
| `Purpose` | `Patch` (исправление) / `Customization` (доработка) / `AddOn` (дополнение) | `Customization` |
|
| `Purpose` | `Patch` (исправление) / `Customization` (доработка) / `AddOn` (дополнение) | `Customization` |
|
||||||
| `Version` | Версия расширения | — |
|
| `Version` | Версия расширения | — |
|
||||||
| `Vendor` | Поставщик | — |
|
| `Vendor` | Поставщик | — |
|
||||||
| `CompatibilityMode` | Режим совместимости | `Version8_3_24` |
|
| `CompatibilityMode` | Режим совместимости; при заданном `ConfigPath` определяется по базовой конфигурации и этот параметр не нужен | `Version8_3_24` |
|
||||||
| `ConfigPath` | Путь к выгрузке базовой конфигурации (авто-определяет CompatibilityMode и Language UUID) | — |
|
| `ConfigPath` | Путь к выгрузке базовой конфигурации (авто-определяет CompatibilityMode и Language UUID) | — |
|
||||||
| `NoRole` | Без основной роли | false |
|
| `NoRole` | Без основной роли | false |
|
||||||
|
|
||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-init.ps1" -Name "МоёРасширение"
|
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-init.ps1" -Name "МоёРасширение" -OutputDir "src\cfe\МоёРасширение" -ConfigPath "src\cf"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Примеры
|
## Примеры
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Расширение для ERP с авто-определением совместимости из базовой конфигурации
|
# Расширение для ERP с авто-определением совместимости из базовой конфигурации
|
||||||
... -Name Расш1 -ConfigPath C:\WS\tasks\cfsrc\erp_8.3.24 -OutputDir src
|
... -Name Расш1 -ConfigPath src\cf -OutputDir src\cfe\Расш1
|
||||||
|
|
||||||
# Расширение-исправление с явным режимом совместимости
|
# Расширение-исправление с явным режимом совместимости
|
||||||
... -Name Расш1 -Purpose Patch -CompatibilityMode Version8_3_17 -OutputDir src
|
... -Name Расш1 -Purpose Patch -CompatibilityMode Version8_3_17 -OutputDir src\cfe\Расш1
|
||||||
|
|
||||||
# Расширение-доработка с версией
|
# Расширение-доработка с версией
|
||||||
... -Name МоёРасширение -Version "1.0.0.1" -Vendor "Компания" -OutputDir src
|
... -Name МоёРасширение -Version "1.0.0.1" -Vendor "Компания" -OutputDir src\cfe\МоёРасширение
|
||||||
|
|
||||||
# Без роли, с явным префиксом
|
# Без роли, с явным префиксом
|
||||||
... -Name ИсправлениеБага -NamePrefix "ИБ_" -Purpose Patch -NoRole -OutputDir src
|
... -Name ИсправлениеБага -NamePrefix "ИБ_" -Purpose Patch -NoRole -OutputDir src\cfe\ИсправлениеБага
|
||||||
```
|
```
|
||||||
|
|
||||||
## Верификация
|
## Верификация
|
||||||
|
|
||||||
```
|
```
|
||||||
/cfe-validate <OutputDir>
|
/cfe-validate <OutputDir> -ConfigPath <ConfigPath>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# cfe-init v1.2 — Create 1C configuration extension scaffold (CFE)
|
# cfe-init v1.10 — Create 1C configuration extension scaffold (CFE) (+write_xml_file/write_utf8_bom: общий эталон записи)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory)]
|
||||||
@@ -16,6 +16,12 @@ param(
|
|||||||
)
|
)
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
function Esc-XmlText {
|
||||||
|
param([string]$s)
|
||||||
|
# Эскейп ТЕКСТА элемента: только & < > — кавычку и апостроф платформа держит сырыми.
|
||||||
|
return $s.Replace('&','&').Replace('<','<').Replace('>','>')
|
||||||
|
}
|
||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
|
||||||
# --- Default NamePrefix ---
|
# --- Default NamePrefix ---
|
||||||
@@ -121,20 +127,23 @@ $co7 = [guid]::NewGuid().ToString()
|
|||||||
# --- Synonym XML ---
|
# --- Synonym XML ---
|
||||||
$synonymXml = ""
|
$synonymXml = ""
|
||||||
if ($Synonym) {
|
if ($Synonym) {
|
||||||
$synonymXml = "`r`n`t`t`t`t<v8:item>`r`n`t`t`t`t`t<v8:lang>ru</v8:lang>`r`n`t`t`t`t`t<v8:content>$([System.Security.SecurityElement]::Escape($Synonym))</v8:content>`r`n`t`t`t`t</v8:item>`r`n`t`t`t"
|
$synonymXml = "`r`n`t`t`t`t<v8:item>`r`n`t`t`t`t`t<v8:lang>ru</v8:lang>`r`n`t`t`t`t`t<v8:content>$(Esc-XmlText ($Synonym))</v8:content>`r`n`t`t`t`t</v8:item>`r`n`t`t`t"
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- Optional properties ---
|
# --- Optional properties ---
|
||||||
$vendorXml = if ($Vendor) { [System.Security.SecurityElement]::Escape($Vendor) } else { "" }
|
# Элемент целиком, а не значение внутри пары: при пустом значении Конфигуратор
|
||||||
$versionXml = if ($Version) { [System.Security.SecurityElement]::Escape($Version) } else { "" }
|
# пишет <Vendor/>, а не <Vendor></Vendor>.
|
||||||
|
$vendorEl = if ($Vendor) { "<Vendor>$(Esc-XmlText ($Vendor))</Vendor>" } else { "<Vendor/>" }
|
||||||
|
$versionEl = if ($Version) { "<Version>$(Esc-XmlText ($Version))</Version>" } else { "<Version/>" }
|
||||||
|
|
||||||
# --- Role name ---
|
# --- Role name ---
|
||||||
$roleName = "${NamePrefix}ОсновнаяРоль"
|
$roleName = "${NamePrefix}ОсновнаяРоль"
|
||||||
|
|
||||||
# --- DefaultRoles XML ---
|
# --- DefaultRoles XML ---
|
||||||
$defaultRolesXml = ""
|
# Элемент целиком: без роли Конфигуратор пишет <DefaultRoles/>, а не пустую пару.
|
||||||
|
$defaultRolesEl = "<DefaultRoles/>"
|
||||||
if (-not $NoRole) {
|
if (-not $NoRole) {
|
||||||
$defaultRolesXml = "`r`n`t`t`t`t<xr:Item xsi:type=`"xr:MDObjectRef`">Role.$roleName</xr:Item>`r`n`t`t`t"
|
$defaultRolesEl = "<DefaultRoles>`r`n`t`t`t`t<xr:Item xsi:type=`"xr:MDObjectRef`">Role.$roleName</xr:Item>`r`n`t`t`t</DefaultRoles>"
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- ChildObjects ---
|
# --- ChildObjects ---
|
||||||
@@ -144,10 +153,32 @@ if (-not $NoRole) {
|
|||||||
}
|
}
|
||||||
$childObjectsXml += "`r`n`t`t"
|
$childObjectsXml += "`r`n`t`t"
|
||||||
|
|
||||||
|
# Объявления пространств имён — одной переменной: места эмиссии её только интерполируют.
|
||||||
|
# Правки шапки (как xmlns:pal в формате 2.21) делаются здесь, в одном месте.
|
||||||
|
$xmlnsDecl = 'xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
|
||||||
|
|
||||||
|
# Версия формата как число для сравнений: "2.20" → 220, "2.9" → 209.
|
||||||
|
# Строковое сравнение здесь неверно ("2.9" > "2.17" лексикографически) — известная ловушка.
|
||||||
|
function Get-FormatRank([string]$ver) {
|
||||||
|
if ($ver -match '^(\d+)\.(\d+)$') { return [int]$Matches[1] * 100 + [int]$Matches[2] }
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# 2.21 (8.5) добавила в шапку пространство палитры — ради <Color> у значений перечисления.
|
||||||
|
# Вставляем НА МЕСТО (после lf, перед style): платформа держит объявления по алфавиту,
|
||||||
|
# дописать в конец нельзя.
|
||||||
|
# Caption/ShortCaption — свойства корня из того же формата 2.21, между Version и
|
||||||
|
# DefaultLanguage (позиция снята с выгрузки расширения из базы 8.5).
|
||||||
|
$f221Captions = ""
|
||||||
|
if ((Get-FormatRank $formatVersion) -ge 221) {
|
||||||
|
$xmlnsDecl = $xmlnsDecl -replace ' xmlns:style=', ' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette" xmlns:style='
|
||||||
|
$f221Captions = "`r`n`t`t`t<Caption/>`r`n`t`t`t<ShortCaption/>"
|
||||||
|
}
|
||||||
|
|
||||||
# --- Configuration.xml ---
|
# --- Configuration.xml ---
|
||||||
$cfgXml = @"
|
$cfgXml = @"
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="$formatVersion">
|
<MetaDataObject $xmlnsDecl version="$formatVersion">
|
||||||
<Configuration uuid="$uuidCfg">
|
<Configuration uuid="$uuidCfg">
|
||||||
<InternalInfo>
|
<InternalInfo>
|
||||||
<xr:ContainedObject>
|
<xr:ContainedObject>
|
||||||
@@ -181,21 +212,21 @@ $cfgXml = @"
|
|||||||
</InternalInfo>
|
</InternalInfo>
|
||||||
<Properties>
|
<Properties>
|
||||||
<ObjectBelonging>Adopted</ObjectBelonging>
|
<ObjectBelonging>Adopted</ObjectBelonging>
|
||||||
<Name>$([System.Security.SecurityElement]::Escape($Name))</Name>
|
<Name>$(Esc-XmlText ($Name))</Name>
|
||||||
<Synonym>$synonymXml</Synonym>
|
<Synonym>$synonymXml</Synonym>
|
||||||
<Comment/>
|
<Comment/>
|
||||||
<ConfigurationExtensionPurpose>$Purpose</ConfigurationExtensionPurpose>
|
<ConfigurationExtensionPurpose>$Purpose</ConfigurationExtensionPurpose>
|
||||||
<KeepMappingToExtendedConfigurationObjectsByIDs>true</KeepMappingToExtendedConfigurationObjectsByIDs>
|
<KeepMappingToExtendedConfigurationObjectsByIDs>true</KeepMappingToExtendedConfigurationObjectsByIDs>
|
||||||
<NamePrefix>$([System.Security.SecurityElement]::Escape($NamePrefix))</NamePrefix>
|
<NamePrefix>$(Esc-XmlText ($NamePrefix))</NamePrefix>
|
||||||
<ConfigurationExtensionCompatibilityMode>$CompatibilityMode</ConfigurationExtensionCompatibilityMode>
|
<ConfigurationExtensionCompatibilityMode>$CompatibilityMode</ConfigurationExtensionCompatibilityMode>
|
||||||
<DefaultRunMode>ManagedApplication</DefaultRunMode>
|
<DefaultRunMode>ManagedApplication</DefaultRunMode>
|
||||||
<UsePurposes>
|
<UsePurposes>
|
||||||
<v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value>
|
<v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value>
|
||||||
</UsePurposes>
|
</UsePurposes>
|
||||||
<ScriptVariant>Russian</ScriptVariant>
|
<ScriptVariant>Russian</ScriptVariant>
|
||||||
<DefaultRoles>$defaultRolesXml</DefaultRoles>
|
$defaultRolesEl
|
||||||
<Vendor>$vendorXml</Vendor>
|
$vendorEl
|
||||||
<Version>$versionXml</Version>
|
$versionEl$f221Captions
|
||||||
<DefaultLanguage>Language.Русский</DefaultLanguage>
|
<DefaultLanguage>Language.Русский</DefaultLanguage>
|
||||||
<BriefInformation/>
|
<BriefInformation/>
|
||||||
<DetailedInformation/>
|
<DetailedInformation/>
|
||||||
@@ -212,7 +243,7 @@ $cfgXml = @"
|
|||||||
# --- Languages/Русский.xml (adopted format) ---
|
# --- Languages/Русский.xml (adopted format) ---
|
||||||
$langXml = @"
|
$langXml = @"
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="$formatVersion">
|
<MetaDataObject $xmlnsDecl version="$formatVersion">
|
||||||
<Language uuid="$uuidLang">
|
<Language uuid="$uuidLang">
|
||||||
<InternalInfo/>
|
<InternalInfo/>
|
||||||
<Properties>
|
<Properties>
|
||||||
@@ -229,10 +260,10 @@ $langXml = @"
|
|||||||
# --- Role XML ---
|
# --- Role XML ---
|
||||||
$roleXml = @"
|
$roleXml = @"
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="$formatVersion">
|
<MetaDataObject $xmlnsDecl version="$formatVersion">
|
||||||
<Role uuid="$uuidRole">
|
<Role uuid="$uuidRole">
|
||||||
<Properties>
|
<Properties>
|
||||||
<Name>$([System.Security.SecurityElement]::Escape($roleName))</Name>
|
<Name>$(Esc-XmlText ($roleName))</Name>
|
||||||
<Synonym/>
|
<Synonym/>
|
||||||
<Comment/>
|
<Comment/>
|
||||||
</Properties>
|
</Properties>
|
||||||
@@ -252,9 +283,18 @@ if (-not (Test-Path $langDir)) {
|
|||||||
# --- Write files with UTF-8 BOM ---
|
# --- Write files with UTF-8 BOM ---
|
||||||
$enc = New-Object System.Text.UTF8Encoding($true)
|
$enc = New-Object System.Text.UTF8Encoding($true)
|
||||||
|
|
||||||
[System.IO.File]::WriteAllText($cfgFile, $cfgXml, $enc)
|
# XML в каноне выгрузки Конфигуратора: CRLF в разделителях, без перевода в конце.
|
||||||
|
#
|
||||||
|
# Копия этой функции есть в каждом навыке-эмиттере (навыки автономны). Держать
|
||||||
|
# копии одинаковыми — сознательно: разошедшиеся копии сводят на нет весь смысл.
|
||||||
|
function Write-XmlFile([string]$path, [string]$text, $encoding) {
|
||||||
|
$t = ($text -replace "`r`n", "`n") -replace "`n", "`r`n"
|
||||||
|
[System.IO.File]::WriteAllText($path, $t.TrimEnd("`r", "`n"), $encoding)
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-XmlFile $cfgFile $cfgXml $enc
|
||||||
$langFile = Join-Path $langDir "Русский.xml"
|
$langFile = Join-Path $langDir "Русский.xml"
|
||||||
[System.IO.File]::WriteAllText($langFile, $langXml, $enc)
|
Write-XmlFile $langFile $langXml $enc
|
||||||
|
|
||||||
# --- Role ---
|
# --- Role ---
|
||||||
if (-not $NoRole) {
|
if (-not $NoRole) {
|
||||||
@@ -263,7 +303,7 @@ if (-not $NoRole) {
|
|||||||
New-Item -ItemType Directory -Path $roleDir -Force | Out-Null
|
New-Item -ItemType Directory -Path $roleDir -Force | Out-Null
|
||||||
}
|
}
|
||||||
$roleFile = Join-Path $roleDir "$roleName.xml"
|
$roleFile = Join-Path $roleDir "$roleName.xml"
|
||||||
[System.IO.File]::WriteAllText($roleFile, $roleXml, $enc)
|
Write-XmlFile $roleFile $roleXml $enc
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- Output ---
|
# --- Output ---
|
||||||
|
|||||||
@@ -1,20 +1,62 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# cfe-init v1.2 — Create 1C configuration extension scaffold (CFE)
|
# cfe-init v1.10 — Create 1C configuration extension scaffold (CFE) (+write_xml_file/write_utf8_bom: общий эталон записи)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
"""Generates minimal XML source files for a 1C configuration extension."""
|
"""Generates minimal XML source files for a 1C configuration extension."""
|
||||||
import sys, os, argparse, uuid
|
import sys, os, re, argparse, uuid
|
||||||
from xml.etree import ElementTree as ET
|
from xml.etree import ElementTree as ET
|
||||||
|
|
||||||
def esc_xml(s):
|
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
|
||||||
return s.replace('&','&').replace('<','<').replace('>','>').replace('"','"')
|
# регистр не различают, в 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)
|
||||||
|
|
||||||
|
|
||||||
|
def esc_xml_text(s):
|
||||||
|
# Эскейп ТЕКСТА элемента: только & < > — кавычку и апостроф платформа держит сырыми.
|
||||||
|
return s.replace('&', '&').replace('<', '<').replace('>', '>')
|
||||||
|
|
||||||
def new_uuid():
|
def new_uuid():
|
||||||
return str(uuid.uuid4())
|
return str(uuid.uuid4())
|
||||||
|
|
||||||
def write_utf8_bom(path, content):
|
def write_utf8_bom(path, content):
|
||||||
|
# newline='' — без трансляции: иначе текстовый режим Python дал бы CRLF на Windows
|
||||||
|
# и LF на macOS, то есть вывод навыка зависел бы от ОС.
|
||||||
with open(path, 'w', encoding='utf-8-sig', newline='') as f:
|
with open(path, 'w', encoding='utf-8-sig', newline='') as f:
|
||||||
f.write(content)
|
f.write(content)
|
||||||
|
|
||||||
|
|
||||||
|
def write_xml_file(path, content):
|
||||||
|
"""XML в каноне выгрузки Конфигуратора: CRLF в разделителях, без перевода в конце.
|
||||||
|
|
||||||
|
Копия этой функции есть в каждом навыке-эмиттере (навыки автономны). Держать
|
||||||
|
копии одинаковыми — сознательно: разошедшиеся копии сводят на нет весь смысл.
|
||||||
|
"""
|
||||||
|
text = content.replace('\r\n', '\n').replace('\n', '\r\n').rstrip('\r\n')
|
||||||
|
write_utf8_bom(path, text)
|
||||||
|
|
||||||
|
|
||||||
|
def format_rank(ver):
|
||||||
|
""""2.20" → 220, "2.9" → 209. Строковое сравнение неверно ("2.9" > "2.17")."""
|
||||||
|
m = re.match(r'^(\d+)\.(\d+)$', ver or '')
|
||||||
|
return int(m.group(1)) * 100 + int(m.group(2)) if m else 0
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
sys.stdout.reconfigure(encoding="utf-8")
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
sys.stderr.reconfigure(encoding="utf-8")
|
||||||
@@ -29,7 +71,7 @@ def main():
|
|||||||
parser.add_argument('-CompatibilityMode', dest='CompatibilityMode', default='Version8_3_24')
|
parser.add_argument('-CompatibilityMode', dest='CompatibilityMode', default='Version8_3_24')
|
||||||
parser.add_argument('-ConfigPath', dest='ConfigPath', default=None)
|
parser.add_argument('-ConfigPath', dest='ConfigPath', default=None)
|
||||||
parser.add_argument('-NoRole', dest='NoRole', action='store_true')
|
parser.add_argument('-NoRole', dest='NoRole', action='store_true')
|
||||||
args = parser.parse_args()
|
args = ci_parse_args(parser)
|
||||||
|
|
||||||
name = args.Name
|
name = args.Name
|
||||||
synonym = args.Synonym if args.Synonym else name
|
synonym = args.Synonym if args.Synonym else name
|
||||||
@@ -126,18 +168,23 @@ def main():
|
|||||||
# --- Synonym XML ---
|
# --- Synonym XML ---
|
||||||
synonym_xml = ""
|
synonym_xml = ""
|
||||||
if synonym:
|
if synonym:
|
||||||
synonym_xml = f"\r\n\t\t\t\t<v8:item>\r\n\t\t\t\t\t<v8:lang>ru</v8:lang>\r\n\t\t\t\t\t<v8:content>{esc_xml(synonym)}</v8:content>\r\n\t\t\t\t</v8:item>\r\n\t\t\t"
|
synonym_xml = f"\r\n\t\t\t\t<v8:item>\r\n\t\t\t\t\t<v8:lang>ru</v8:lang>\r\n\t\t\t\t\t<v8:content>{esc_xml_text(synonym)}</v8:content>\r\n\t\t\t\t</v8:item>\r\n\t\t\t"
|
||||||
|
|
||||||
vendor_xml = esc_xml(vendor) if vendor else ""
|
# Элемент целиком, а не значение внутри пары: при пустом значении Конфигуратор
|
||||||
version_xml = esc_xml(version) if version else ""
|
# пишет <Vendor/>, а не <Vendor></Vendor>.
|
||||||
|
vendor_el = f"<Vendor>{esc_xml_text(vendor)}</Vendor>" if vendor else "<Vendor/>"
|
||||||
|
version_el = f"<Version>{esc_xml_text(version)}</Version>" if version else "<Version/>"
|
||||||
|
|
||||||
# --- Role name ---
|
# --- Role name ---
|
||||||
role_name = f"{name_prefix}ОсновнаяРоль"
|
role_name = f"{name_prefix}ОсновнаяРоль"
|
||||||
|
|
||||||
# --- DefaultRoles XML ---
|
# --- DefaultRoles XML ---
|
||||||
default_roles_xml = ""
|
# Элемент целиком: без роли Конфигуратор пишет <DefaultRoles/>, а не пустую пару.
|
||||||
|
default_roles_el = "<DefaultRoles/>"
|
||||||
if not args.NoRole:
|
if not args.NoRole:
|
||||||
default_roles_xml = f'\r\n\t\t\t\t<xr:Item xsi:type="xr:MDObjectRef">Role.{role_name}</xr:Item>\r\n\t\t\t'
|
default_roles_el = ('<DefaultRoles>\r\n\t\t\t\t'
|
||||||
|
f'<xr:Item xsi:type="xr:MDObjectRef">Role.{role_name}</xr:Item>'
|
||||||
|
'\r\n\t\t\t</DefaultRoles>')
|
||||||
|
|
||||||
# --- ChildObjects ---
|
# --- ChildObjects ---
|
||||||
child_objects_xml = f"\r\n\t\t\t<Language>Русский</Language>"
|
child_objects_xml = f"\r\n\t\t\t<Language>Русский</Language>"
|
||||||
@@ -156,6 +203,40 @@ def main():
|
|||||||
]
|
]
|
||||||
|
|
||||||
contained_objects = ""
|
contained_objects = ""
|
||||||
|
|
||||||
|
# Объявления пространств имён — одной переменной: места эмиссии её только подставляют.
|
||||||
|
# Правки шапки (как xmlns:pal в формате 2.21) делаются здесь, в одном месте.
|
||||||
|
xmlns_decl = (
|
||||||
|
'xmlns="http://v8.1c.ru/8.3/MDClasses"'
|
||||||
|
' xmlns:app="http://v8.1c.ru/8.2/managed-application/core"'
|
||||||
|
' xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config"'
|
||||||
|
' xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi"'
|
||||||
|
' xmlns:ent="http://v8.1c.ru/8.1/data/enterprise"'
|
||||||
|
' xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform"'
|
||||||
|
' xmlns:style="http://v8.1c.ru/8.1/data/ui/style"'
|
||||||
|
' xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system"'
|
||||||
|
' xmlns:v8="http://v8.1c.ru/8.1/data/core"'
|
||||||
|
' xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"'
|
||||||
|
' xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web"'
|
||||||
|
' xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows"'
|
||||||
|
' xmlns:xen="http://v8.1c.ru/8.3/xcf/enums"'
|
||||||
|
' xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef"'
|
||||||
|
' xmlns:xr="http://v8.1c.ru/8.3/xcf/readable"'
|
||||||
|
' xmlns:xs="http://www.w3.org/2001/XMLSchema"'
|
||||||
|
' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
|
||||||
|
)
|
||||||
|
|
||||||
|
# 2.21 (8.5) добавила в шапку пространство палитры — ради <Color> у значений перечисления.
|
||||||
|
# Вставляем НА МЕСТО (после lf, перед style): платформа держит объявления по алфавиту,
|
||||||
|
# дописать в конец нельзя.
|
||||||
|
# Caption/ShortCaption — свойства корня из того же формата 2.21, между Version и
|
||||||
|
# DefaultLanguage (позиция снята с выгрузки расширения из базы 8.5).
|
||||||
|
f221_captions = ""
|
||||||
|
if format_rank(format_version) >= 221:
|
||||||
|
xmlns_decl = xmlns_decl.replace(
|
||||||
|
' xmlns:style=',
|
||||||
|
' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette" xmlns:style=')
|
||||||
|
f221_captions = "\r\n\t\t\t<Caption/>\r\n\t\t\t<ShortCaption/>"
|
||||||
for i in range(7):
|
for i in range(7):
|
||||||
contained_objects += f"""\t\t\t<xr:ContainedObject>
|
contained_objects += f"""\t\t\t<xr:ContainedObject>
|
||||||
\t\t\t\t<xr:ClassId>{class_ids[i]}</xr:ClassId>
|
\t\t\t\t<xr:ClassId>{class_ids[i]}</xr:ClassId>
|
||||||
@@ -163,27 +244,27 @@ def main():
|
|||||||
\t\t\t</xr:ContainedObject>\n"""
|
\t\t\t</xr:ContainedObject>\n"""
|
||||||
|
|
||||||
cfg_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
|
cfg_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="{format_version}">
|
<MetaDataObject {xmlns_decl} version="{format_version}">
|
||||||
\t<Configuration uuid="{uuid_cfg}">
|
\t<Configuration uuid="{uuid_cfg}">
|
||||||
\t\t<InternalInfo>
|
\t\t<InternalInfo>
|
||||||
{contained_objects}\t\t</InternalInfo>
|
{contained_objects}\t\t</InternalInfo>
|
||||||
\t\t<Properties>
|
\t\t<Properties>
|
||||||
\t\t\t<ObjectBelonging>Adopted</ObjectBelonging>
|
\t\t\t<ObjectBelonging>Adopted</ObjectBelonging>
|
||||||
\t\t\t<Name>{esc_xml(name)}</Name>
|
\t\t\t<Name>{esc_xml_text(name)}</Name>
|
||||||
\t\t\t<Synonym>{synonym_xml}</Synonym>
|
\t\t\t<Synonym>{synonym_xml}</Synonym>
|
||||||
\t\t\t<Comment/>
|
\t\t\t<Comment/>
|
||||||
\t\t\t<ConfigurationExtensionPurpose>{purpose}</ConfigurationExtensionPurpose>
|
\t\t\t<ConfigurationExtensionPurpose>{purpose}</ConfigurationExtensionPurpose>
|
||||||
\t\t\t<KeepMappingToExtendedConfigurationObjectsByIDs>true</KeepMappingToExtendedConfigurationObjectsByIDs>
|
\t\t\t<KeepMappingToExtendedConfigurationObjectsByIDs>true</KeepMappingToExtendedConfigurationObjectsByIDs>
|
||||||
\t\t\t<NamePrefix>{esc_xml(name_prefix)}</NamePrefix>
|
\t\t\t<NamePrefix>{esc_xml_text(name_prefix)}</NamePrefix>
|
||||||
\t\t\t<ConfigurationExtensionCompatibilityMode>{compat}</ConfigurationExtensionCompatibilityMode>
|
\t\t\t<ConfigurationExtensionCompatibilityMode>{compat}</ConfigurationExtensionCompatibilityMode>
|
||||||
\t\t\t<DefaultRunMode>ManagedApplication</DefaultRunMode>
|
\t\t\t<DefaultRunMode>ManagedApplication</DefaultRunMode>
|
||||||
\t\t\t<UsePurposes>
|
\t\t\t<UsePurposes>
|
||||||
\t\t\t\t<v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value>
|
\t\t\t\t<v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value>
|
||||||
\t\t\t</UsePurposes>
|
\t\t\t</UsePurposes>
|
||||||
\t\t\t<ScriptVariant>Russian</ScriptVariant>
|
\t\t\t<ScriptVariant>Russian</ScriptVariant>
|
||||||
\t\t\t<DefaultRoles>{default_roles_xml}</DefaultRoles>
|
\t\t\t{default_roles_el}
|
||||||
\t\t\t<Vendor>{vendor_xml}</Vendor>
|
\t\t\t{vendor_el}
|
||||||
\t\t\t<Version>{version_xml}</Version>
|
\t\t\t{version_el}{f221_captions}
|
||||||
\t\t\t<DefaultLanguage>Language.Русский</DefaultLanguage>
|
\t\t\t<DefaultLanguage>Language.Русский</DefaultLanguage>
|
||||||
\t\t\t<BriefInformation/>
|
\t\t\t<BriefInformation/>
|
||||||
\t\t\t<DetailedInformation/>
|
\t\t\t<DetailedInformation/>
|
||||||
@@ -198,7 +279,7 @@ def main():
|
|||||||
|
|
||||||
# --- Languages/Русский.xml (adopted format) ---
|
# --- Languages/Русский.xml (adopted format) ---
|
||||||
lang_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
|
lang_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="{format_version}">
|
<MetaDataObject {xmlns_decl} version="{format_version}">
|
||||||
\t<Language uuid="{uuid_lang}">
|
\t<Language uuid="{uuid_lang}">
|
||||||
\t\t<InternalInfo/>
|
\t\t<InternalInfo/>
|
||||||
\t\t<Properties>
|
\t\t<Properties>
|
||||||
@@ -213,10 +294,10 @@ def main():
|
|||||||
|
|
||||||
# --- Role XML ---
|
# --- Role XML ---
|
||||||
role_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
|
role_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="{format_version}">
|
<MetaDataObject {xmlns_decl} version="{format_version}">
|
||||||
\t<Role uuid="{uuid_role}">
|
\t<Role uuid="{uuid_role}">
|
||||||
\t\t<Properties>
|
\t\t<Properties>
|
||||||
\t\t\t<Name>{esc_xml(role_name)}</Name>
|
\t\t\t<Name>{esc_xml_text(role_name)}</Name>
|
||||||
\t\t\t<Synonym/>
|
\t\t\t<Synonym/>
|
||||||
\t\t\t<Comment/>
|
\t\t\t<Comment/>
|
||||||
\t\t</Properties>
|
\t\t</Properties>
|
||||||
@@ -229,9 +310,9 @@ def main():
|
|||||||
os.makedirs(lang_dir, exist_ok=True)
|
os.makedirs(lang_dir, exist_ok=True)
|
||||||
|
|
||||||
# --- Write files ---
|
# --- Write files ---
|
||||||
write_utf8_bom(cfg_file, cfg_xml)
|
write_xml_file(cfg_file, cfg_xml)
|
||||||
lang_file = os.path.join(lang_dir, "Русский.xml")
|
lang_file = os.path.join(lang_dir, "Русский.xml")
|
||||||
write_utf8_bom(lang_file, lang_xml)
|
write_xml_file(lang_file, lang_xml)
|
||||||
|
|
||||||
# --- Role ---
|
# --- Role ---
|
||||||
role_file = None
|
role_file = None
|
||||||
@@ -239,7 +320,7 @@ def main():
|
|||||||
role_dir = os.path.join(output_dir, "Roles")
|
role_dir = os.path.join(output_dir, "Roles")
|
||||||
os.makedirs(role_dir, exist_ok=True)
|
os.makedirs(role_dir, exist_ok=True)
|
||||||
role_file = os.path.join(role_dir, f"{role_name}.xml")
|
role_file = os.path.join(role_dir, f"{role_name}.xml")
|
||||||
write_utf8_bom(role_file, role_xml)
|
write_xml_file(role_file, role_xml)
|
||||||
|
|
||||||
# --- Output ---
|
# --- Output ---
|
||||||
print(f"[OK] Создано расширение: {name}")
|
print(f"[OK] Создано расширение: {name}")
|
||||||
|
|||||||
@@ -110,36 +110,36 @@ allowed-tools:
|
|||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-patch-method.ps1" -ExtensionPath src\cfe\ИмяРасширения -ConfigPath src\cf -ModulePath "Catalog.Контрагенты.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before
|
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-patch-method.ps1" -ExtensionPath src\cfe\extname -ConfigPath src\cf -ModulePath "Catalog.Контрагенты.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before
|
||||||
```
|
```
|
||||||
|
|
||||||
## Примеры
|
## Примеры
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Код перед записью
|
# Код перед записью
|
||||||
... -ExtensionPath src\cfe\ИмяРасширения -ConfigPath src\cf -ModulePath "Catalog.Контрагенты.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before
|
... -ExtensionPath src\cfe\extname -ConfigPath src\cf -ModulePath "Catalog.Контрагенты.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before
|
||||||
|
|
||||||
# Перехват После на форме
|
# Перехват После на форме
|
||||||
... -ExtensionPath src\cfe\ИмяРасширения -ConfigPath src\cf -ModulePath "Document.Заказ.Form.ФормаДокумента" -MethodName "ПослеЗаписиНаСервере" -InterceptorType After
|
... -ExtensionPath src\cfe\extname -ConfigPath src\cf -ModulePath "Document.Заказ.Form.ФормаДокумента" -MethodName "ПослеЗаписиНаСервере" -InterceptorType After
|
||||||
|
|
||||||
# Замена функции (ПродолжитьВызов)
|
# Замена функции (ПродолжитьВызов)
|
||||||
... -ExtensionPath src\cfe\ИмяРасширения -ConfigPath src\cf -ModulePath "CommonModule.ОбщийМодуль" -MethodName "ПолучитьДанные" -InterceptorType Instead
|
... -ExtensionPath src\cfe\extname -ConfigPath src\cf -ModulePath "CommonModule.ОбщийМодуль" -MethodName "ПолучитьДанные" -InterceptorType Instead
|
||||||
|
|
||||||
# ИзменениеИКонтроль — копия тела для правки маркерами
|
# ИзменениеИКонтроль — копия тела для правки маркерами
|
||||||
... -ExtensionPath src\cfe\ИмяРасширения -ConfigPath src\cf -ModulePath "Document.РеализацияТоваров.ObjectModule" -MethodName "ОбработкаПроведения" -InterceptorType ModificationAndControl
|
... -ExtensionPath src\cfe\extname -ConfigPath src\cf -ModulePath "Document.РеализацияТоваров.ObjectModule" -MethodName "ОбработкаПроведения" -InterceptorType ModificationAndControl
|
||||||
|
|
||||||
# ModulePath как путь к файлу модуля-источника (без -ConfigPath)
|
# ModulePath как путь к файлу модуля-источника (без -ConfigPath)
|
||||||
... -ExtensionPath src\cfe\ИмяРасширения -ModulePath "src\cf\CommonModules\ОбщийМодуль\Ext\Module.bsl" -MethodName "ПолучитьДанные" -InterceptorType Instead
|
... -ExtensionPath src\cfe\extname -ModulePath "src\cf\CommonModules\ОбщийМодуль\Ext\Module.bsl" -MethodName "ПолучитьДанные" -InterceptorType Instead
|
||||||
|
|
||||||
# Проверить все контролируемые методы расширения на дрейф
|
# Проверить все контролируемые методы расширения на дрейф
|
||||||
... -ExtensionPath src\cfe\ИмяРасширения -ConfigPath src\cf -Check
|
... -ExtensionPath src\cfe\extname -ConfigPath src\cf -Check
|
||||||
|
|
||||||
# Актуализировать дрейфнувшие контролируемые методы пачкой
|
# Актуализировать дрейфнувшие контролируемые методы пачкой
|
||||||
... -ExtensionPath src\cfe\ИмяРасширения -ConfigPath src\cf -Actualize
|
... -ExtensionPath src\cfe\extname -ConfigPath src\cf -Actualize
|
||||||
```
|
```
|
||||||
|
|
||||||
## Верификация
|
## Верификация
|
||||||
|
|
||||||
```
|
```
|
||||||
/cfe-validate <ExtensionPath>
|
/cfe-validate <ExtensionPath> -ConfigPath <ConfigPath>
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# cfe-patch-method v2.5 — Source-aware method interceptor for 1C extension (CFE)
|
# cfe-patch-method v2.7 — Source-aware method interceptor for 1C extension (CFE) (+прощающий ввод: имя каталога наравне с именем типа)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory)]
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# cfe-patch-method v2.5 — Source-aware method interceptor for 1C extension (CFE)
|
# cfe-patch-method v2.7 — Source-aware method interceptor for 1C extension (CFE) (+прощающий ввод: имя каталога наравне с именем типа)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -10,6 +10,28 @@ import sys
|
|||||||
import tempfile
|
import tempfile
|
||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
|
|
||||||
|
# Регистронезависимый ввод — паритет с 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)
|
||||||
|
|
||||||
|
|
||||||
TYPE_DIR_MAP = {
|
TYPE_DIR_MAP = {
|
||||||
"Catalog": "Catalogs", "Document": "Documents", "Enum": "Enums",
|
"Catalog": "Catalogs", "Document": "Documents", "Enum": "Enums",
|
||||||
"CommonModule": "CommonModules", "Report": "Reports", "DataProcessor": "DataProcessors",
|
"CommonModule": "CommonModules", "Report": "Reports", "DataProcessor": "DataProcessors",
|
||||||
@@ -19,6 +41,24 @@ TYPE_DIR_MAP = {
|
|||||||
"BusinessProcess": "BusinessProcesses", "Task": "Tasks",
|
"BusinessProcess": "BusinessProcesses", "Task": "Tasks",
|
||||||
"InformationRegister": "InformationRegisters", "AccumulationRegister": "AccumulationRegisters",
|
"InformationRegister": "InformationRegisters", "AccumulationRegister": "AccumulationRegisters",
|
||||||
"AccountingRegister": "AccountingRegisters", "CalculationRegister": "CalculationRegisters",
|
"AccountingRegister": "AccountingRegisters", "CalculationRegister": "CalculationRegisters",
|
||||||
|
# Прощающий ввод: имя каталога принимается наравне с именем типа (Catalogs.X ≡ Catalog.X) —
|
||||||
|
# PS1-порт так умел с самого начала, PY отставал.
|
||||||
|
"Catalogs": "Catalogs",
|
||||||
|
"Documents": "Documents",
|
||||||
|
"Enums": "Enums",
|
||||||
|
"CommonModules": "CommonModules",
|
||||||
|
"Reports": "Reports",
|
||||||
|
"DataProcessors": "DataProcessors",
|
||||||
|
"ExchangePlans": "ExchangePlans",
|
||||||
|
"ChartsOfAccounts": "ChartsOfAccounts",
|
||||||
|
"ChartsOfCharacteristicTypes": "ChartsOfCharacteristicTypes",
|
||||||
|
"ChartsOfCalculationTypes": "ChartsOfCalculationTypes",
|
||||||
|
"BusinessProcesses": "BusinessProcesses",
|
||||||
|
"Tasks": "Tasks",
|
||||||
|
"InformationRegisters": "InformationRegisters",
|
||||||
|
"AccumulationRegisters": "AccumulationRegisters",
|
||||||
|
"AccountingRegisters": "AccountingRegisters",
|
||||||
|
"CalculationRegisters": "CalculationRegisters",
|
||||||
}
|
}
|
||||||
# accept plural forms too
|
# accept plural forms too
|
||||||
for _v in list(TYPE_DIR_MAP.values()):
|
for _v in list(TYPE_DIR_MAP.values()):
|
||||||
@@ -539,7 +579,7 @@ def main():
|
|||||||
choices=["", "Before", "After", "Instead", "ModificationAndControl"])
|
choices=["", "Before", "After", "Instead", "ModificationAndControl"])
|
||||||
parser.add_argument("-Check", action="store_true")
|
parser.add_argument("-Check", action="store_true")
|
||||||
parser.add_argument("-Actualize", action="store_true")
|
parser.add_argument("-Actualize", action="store_true")
|
||||||
args = parser.parse_args()
|
args = ci_parse_args(parser)
|
||||||
|
|
||||||
extension_path = args.ExtensionPath
|
extension_path = args.ExtensionPath
|
||||||
config_path = args.ConfigPath
|
config_path = args.ConfigPath
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
name: cfe-validate
|
name: cfe-validate
|
||||||
description: Валидация расширения конфигурации 1С (CFE). Используй после создания или модификации расширения для проверки корректности
|
description: Валидация расширения конфигурации 1С (CFE). Используй после создания или модификации расширения для проверки корректности
|
||||||
argument-hint: <ExtensionPath> [-Detailed] [-MaxErrors 30]
|
argument-hint: <ExtensionPath> [-ConfigPath <ConfigDir>] [-Detailed] [-MaxErrors 30]
|
||||||
allowed-tools:
|
allowed-tools:
|
||||||
- Bash
|
- Bash
|
||||||
- Read
|
- Read
|
||||||
@@ -17,13 +17,24 @@ allowed-tools:
|
|||||||
| Параметр | Обяз. | Умолч. | Описание |
|
| Параметр | Обяз. | Умолч. | Описание |
|
||||||
|---------------|:-----:|---------|-------------------------------------------------|
|
|---------------|:-----:|---------|-------------------------------------------------|
|
||||||
| ExtensionPath | да | — | Путь к каталогу или Configuration.xml расширения |
|
| ExtensionPath | да | — | Путь к каталогу или Configuration.xml расширения |
|
||||||
|
| ConfigPath | нет | — | Каталог конфигурации, из которой заимствованы объекты |
|
||||||
| Detailed | нет | — | Подробный вывод (все проверки, включая успешные) |
|
| Detailed | нет | — | Подробный вывод (все проверки, включая успешные) |
|
||||||
| MaxErrors | нет | 30 | Остановиться после N ошибок |
|
| MaxErrors | нет | 30 | Остановиться после N ошибок |
|
||||||
| OutFile | нет | — | Записать результат в файл |
|
| OutFile | нет | — | Записать результат в файл |
|
||||||
|
|
||||||
|
### ConfigPath
|
||||||
|
|
||||||
|
Указывай всегда, когда конфигурация-источник доступна: без неё часть ошибок заимствованных форм не ловится, и расширение может пройти валидацию, а потом быть отвергнутым платформой при загрузке.
|
||||||
|
|
||||||
|
Если пользователь не указал путь — определи сам:
|
||||||
|
1. Прочитай `.v8-project.json` из корня проекта
|
||||||
|
2. Разреши целевую базу (по имени, ветке или `default`)
|
||||||
|
3. Возьми её поле `configSrc`
|
||||||
|
|
||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-validate.ps1" -ExtensionPath "src"
|
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-validate.ps1" -ExtensionPath "src\cfe\extname"
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-validate.ps1" -ExtensionPath "src/Configuration.xml"
|
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-validate.ps1" -ExtensionPath "src\cfe\extname\Configuration.xml"
|
||||||
|
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-validate.ps1" -ExtensionPath "src\cfe\extname" -ConfigPath "src\cf"
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# cfe-validate v1.4 — Validate 1C configuration extension structure (CFE)
|
# cfe-validate v1.8 — Validate 1C configuration extension structure (CFE) (полнота GeneratedType, ТЧ из AdditionalColumns, сверка путей с -ConfigPath)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory)]
|
||||||
@@ -9,7 +9,11 @@ param(
|
|||||||
|
|
||||||
[int]$MaxErrors = 30,
|
[int]$MaxErrors = 30,
|
||||||
|
|
||||||
[string]$OutFile
|
[string]$OutFile,
|
||||||
|
|
||||||
|
# Конфигурация-источник. Без неё проверки, требующие сравнения с основной конфигурацией,
|
||||||
|
# пропускаются (о чём сказано в отчёте), остальные работают как раньше.
|
||||||
|
[string]$ConfigPath
|
||||||
)
|
)
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
@@ -108,7 +112,7 @@ $validClassIds = @(
|
|||||||
$childObjectTypes = @(
|
$childObjectTypes = @(
|
||||||
"Language","Subsystem","StyleItem","Style",
|
"Language","Subsystem","StyleItem","Style",
|
||||||
"CommonPicture","SessionParameter","Role","CommonTemplate",
|
"CommonPicture","SessionParameter","Role","CommonTemplate",
|
||||||
"FilterCriterion","CommonModule","CommonAttribute","ExchangePlan",
|
"FilterCriterion","CommonModule","Bot","CommonAttribute","ExchangePlan",
|
||||||
"XDTOPackage","WebService","HTTPService","WSReference",
|
"XDTOPackage","WebService","HTTPService","WSReference",
|
||||||
"EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption",
|
"EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption",
|
||||||
"FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup",
|
"FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup",
|
||||||
@@ -122,7 +126,7 @@ $childObjectTypes = @(
|
|||||||
|
|
||||||
# Type -> directory mapping
|
# Type -> directory mapping
|
||||||
$childTypeDirMap = @{
|
$childTypeDirMap = @{
|
||||||
"Language"="Languages"; "Subsystem"="Subsystems"; "StyleItem"="StyleItems"; "Style"="Styles"
|
"Language"="Languages"; "Subsystem"="Subsystems"; "StyleItem"="StyleItems"; "Style"="Styles"; "Bot"="Bots"
|
||||||
"CommonPicture"="CommonPictures"; "SessionParameter"="SessionParameters"; "Role"="Roles"
|
"CommonPicture"="CommonPictures"; "SessionParameter"="SessionParameters"; "Role"="Roles"
|
||||||
"CommonTemplate"="CommonTemplates"; "FilterCriterion"="FilterCriteria"; "CommonModule"="CommonModules"
|
"CommonTemplate"="CommonTemplates"; "FilterCriterion"="FilterCriteria"; "CommonModule"="CommonModules"
|
||||||
"CommonAttribute"="CommonAttributes"; "ExchangePlan"="ExchangePlans"; "XDTOPackage"="XDTOPackages"
|
"CommonAttribute"="CommonAttributes"; "ExchangePlan"="ExchangePlans"; "XDTOPackage"="XDTOPackages"
|
||||||
@@ -144,6 +148,46 @@ $childTypeDirMap = @{
|
|||||||
"IntegrationService"="IntegrationServices"
|
"IntegrationService"="IntegrationServices"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Наборы GeneratedType по типу объекта (эталон — таблица §2.5 спецификации конфигурации).
|
||||||
|
# Неполный набор в заимствованной оболочке платформа отвергает при загрузке: «отсутствует один
|
||||||
|
# или более типов объекта <Тип>». Типы, у которых GeneratedType нет вовсе (общие модули,
|
||||||
|
# подписки, регламентные задания и т.п.), в карте отсутствуют — для них проверка не выполняется.
|
||||||
|
$generatedTypeCategories = @{
|
||||||
|
"Catalog" = @("Object","Ref","Selection","List","Manager")
|
||||||
|
"Document" = @("Object","Ref","Selection","List","Manager")
|
||||||
|
"Enum" = @("Ref","Manager","List")
|
||||||
|
"Constant" = @("Manager","ValueManager","ValueKey")
|
||||||
|
"Report" = @("Object","Manager")
|
||||||
|
"DataProcessor" = @("Object","Manager")
|
||||||
|
"ExchangePlan" = @("Object","Ref","Selection","List","Manager")
|
||||||
|
"Task" = @("Object","Ref","Selection","List","Manager")
|
||||||
|
"BusinessProcess" = @("Object","Ref","Selection","List","Manager","RoutePointRef")
|
||||||
|
"ChartOfCharacteristicTypes" = @("Object","Ref","Selection","List","Manager","Characteristic")
|
||||||
|
"ChartOfAccounts" = @("Object","Ref","Selection","List","Manager","ExtDimensionTypes","ExtDimensionTypesRow")
|
||||||
|
"ChartOfCalculationTypes" = @("Object","Ref","Selection","List","Manager","DisplacingCalculationTypes","DisplacingCalculationTypesRow","BaseCalculationTypes","BaseCalculationTypesRow","LeadingCalculationTypes","LeadingCalculationTypesRow")
|
||||||
|
"InformationRegister" = @("Record","Manager","Selection","List","RecordSet","RecordKey","RecordManager")
|
||||||
|
"AccumulationRegister" = @("Record","Manager","Selection","List","RecordSet","RecordKey")
|
||||||
|
"AccountingRegister" = @("Record","Manager","Selection","List","RecordSet","RecordKey","ExtDimensions")
|
||||||
|
"CalculationRegister" = @("Record","Manager","Selection","List","RecordSet","RecordKey","Recalcs")
|
||||||
|
"DocumentJournal" = @("Selection","List","Manager")
|
||||||
|
"Sequence" = @("Record","Manager","RecordSet")
|
||||||
|
"FilterCriterion" = @("Manager","List")
|
||||||
|
"SettingsStorage" = @("Manager")
|
||||||
|
"IntegrationService" = @("Manager")
|
||||||
|
"WSReference" = @("Manager")
|
||||||
|
"DefinedType" = @("DefinedType")
|
||||||
|
}
|
||||||
|
|
||||||
|
# Стандартные реквизиты объектов: в ChildObjects их нет, но пути Объект.<Стандартный> законны.
|
||||||
|
# Имена зависят от варианта встроенного языка, поэтому держим оба написания.
|
||||||
|
$script:standardObjectFields = @(
|
||||||
|
"Code","Description","Ref","Parent","Owner","DeletionMark","Predefined","IsFolder","LineNumber",
|
||||||
|
"Number","Date","Posted","PredefinedDataName","RegisterRecords","DataVersion","RowsCount",
|
||||||
|
"Код","Наименование","Ссылка","Родитель","Владелец","ПометкаУдаления","Предопределенный",
|
||||||
|
"ЭтоГруппа","НомерСтроки","Номер","Дата","Проведен","ИмяПредопределенныхДанных",
|
||||||
|
"Движения","ВерсияДанных","КоличествоСтрок"
|
||||||
|
)
|
||||||
|
|
||||||
# Valid enum values for extension properties
|
# Valid enum values for extension properties
|
||||||
$validEnumValues = @{
|
$validEnumValues = @{
|
||||||
"ConfigurationExtensionCompatibilityMode" = @("DontUse","Version8_1","Version8_2_13","Version8_2_16","Version8_3_1","Version8_3_2","Version8_3_3","Version8_3_4","Version8_3_5","Version8_3_6","Version8_3_7","Version8_3_8","Version8_3_9","Version8_3_10","Version8_3_11","Version8_3_12","Version8_3_13","Version8_3_14","Version8_3_15","Version8_3_16","Version8_3_17","Version8_3_18","Version8_3_19","Version8_3_20","Version8_3_21","Version8_3_22","Version8_3_23","Version8_3_24","Version8_3_25","Version8_3_26","Version8_3_27","Version8_3_28","Version8_5_1")
|
"ConfigurationExtensionCompatibilityMode" = @("DontUse","Version8_1","Version8_2_13","Version8_2_16","Version8_3_1","Version8_3_2","Version8_3_3","Version8_3_4","Version8_3_5","Version8_3_6","Version8_3_7","Version8_3_8","Version8_3_9","Version8_3_10","Version8_3_11","Version8_3_12","Version8_3_13","Version8_3_14","Version8_3_15","Version8_3_16","Version8_3_17","Version8_3_18","Version8_3_19","Version8_3_20","Version8_3_21","Version8_3_22","Version8_3_23","Version8_3_24","Version8_3_25","Version8_3_26","Version8_3_27","Version8_3_28","Version8_5_1")
|
||||||
@@ -197,8 +241,9 @@ if ($root.NamespaceURI -ne $expectedNs) {
|
|||||||
$version = $root.GetAttribute("version")
|
$version = $root.GetAttribute("version")
|
||||||
if (-not $version) {
|
if (-not $version) {
|
||||||
Report-Warn "1. Missing version attribute on MetaDataObject"
|
Report-Warn "1. Missing version attribute on MetaDataObject"
|
||||||
} elseif ($version -ne "2.17" -and $version -ne "2.20" -and $version -ne "2.21") {
|
} elseif ($version -notin @("2.17", "2.18", "2.19", "2.20", "2.21")) {
|
||||||
Report-Warn "1. Unusual version '$version' (expected 2.17, 2.20 or 2.21)"
|
# Лестница версий формата: 2.17 (8.3.20-8.3.24), 2.18 (8.3.25), 2.19 (8.3.26), 2.20 (8.3.27).
|
||||||
|
Report-Warn "1. Unusual version '$version' (expected 2.17-2.20 or 2.21)"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Must have Configuration child
|
# Must have Configuration child
|
||||||
@@ -536,6 +581,7 @@ if ($script:stopped) { & $finalize; exit 1 }
|
|||||||
|
|
||||||
# --- Check 9: Borrowed objects validation + Check 10: Sub-items ---
|
# --- Check 9: Borrowed objects validation + Check 10: Sub-items ---
|
||||||
$script:enumValuesIndex = @{}
|
$script:enumValuesIndex = @{}
|
||||||
|
$script:borrowedTSIndex = @{}
|
||||||
$script:formList = @()
|
$script:formList = @()
|
||||||
|
|
||||||
# Helper: check if sub-item has explicit borrowed metadata
|
# Helper: check if sub-item has explicit borrowed metadata
|
||||||
@@ -639,6 +685,25 @@ if ($childObjNode) {
|
|||||||
} else {
|
} else {
|
||||||
$borrowedOk++
|
$borrowedOk++
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Полнота набора GeneratedType: платформа отвергает оболочку с неполным набором
|
||||||
|
# («отсутствует один или более типов объекта ChartOfCharacteristicTypes»)
|
||||||
|
$expectedCats = $generatedTypeCategories[$typeName]
|
||||||
|
if ($expectedCats) {
|
||||||
|
$objInfo = $objEl.SelectSingleNode("md:InternalInfo", $objNs)
|
||||||
|
$foundCats = @{}
|
||||||
|
if ($objInfo) {
|
||||||
|
foreach ($gt in $objInfo.SelectNodes("xr:GeneratedType", $objNs)) {
|
||||||
|
$cat = $gt.GetAttribute("category")
|
||||||
|
if ($cat) { $foundCats[$cat] = $true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$missingCats = @($expectedCats | Where-Object { -not $foundCats.ContainsKey($_) })
|
||||||
|
if ($missingCats.Count -gt 0) {
|
||||||
|
Report-Error "9. Borrowed ${typeName}.${childName}: missing GeneratedType categor$(if ($missingCats.Count -eq 1) { 'y' } else { 'ies' }) $($missingCats -join ', ')"
|
||||||
|
$check9Ok = $false
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- Check 10: Sub-items (Attribute, TabularSection, EnumValue, Form) ---
|
# --- Check 10: Sub-items (Attribute, TabularSection, EnumValue, Form) ---
|
||||||
@@ -666,6 +731,12 @@ if ($childObjNode) {
|
|||||||
$tsInfo = $subItem.SelectSingleNode("md:InternalInfo", $objNs)
|
$tsInfo = $subItem.SelectSingleNode("md:InternalInfo", $objNs)
|
||||||
$tsName = $subItem.SelectSingleNode("md:Properties/md:Name", $objNs)
|
$tsName = $subItem.SelectSingleNode("md:Properties/md:Name", $objNs)
|
||||||
$tsLabel = if ($tsName) { $tsName.InnerText } else { "?" }
|
$tsLabel = if ($tsName) { $tsName.InnerText } else { "?" }
|
||||||
|
# Индекс заимствованных ТЧ — по нему Check 12 сверяет <AdditionalColumns table="Объект.X">
|
||||||
|
if ($tsName) {
|
||||||
|
$tsKey = "${typeName}.${childName}"
|
||||||
|
if (-not $script:borrowedTSIndex.ContainsKey($tsKey)) { $script:borrowedTSIndex[$tsKey] = @{} }
|
||||||
|
$script:borrowedTSIndex[$tsKey][$tsName.InnerText] = $true
|
||||||
|
}
|
||||||
if (-not $tsInfo) {
|
if (-not $tsInfo) {
|
||||||
Report-Error "10. ${ctx}: TabularSection.${tsLabel} missing InternalInfo"
|
Report-Error "10. ${ctx}: TabularSection.${tsLabel} missing InternalInfo"
|
||||||
$check10Ok = $false
|
$check10Ok = $false
|
||||||
@@ -895,6 +966,28 @@ foreach ($bf in $script:borrowedFormsWithTree) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# <AdditionalColumns table="Объект.X"> — доп. колонки табличной части, объявленные в самой форме.
|
||||||
|
# Колонки есть, а самой ТЧ в расширении нет → платформа отвергает загрузку: «Неверный путь к
|
||||||
|
# данным» плюс «Колонки не могут быть добавлены к реквизиту».
|
||||||
|
$acTables = @{}
|
||||||
|
foreach ($m in [regex]::Matches($raw, '<AdditionalColumns table="Объект\.(\w+)"')) {
|
||||||
|
$acTables[$m.Groups[1].Value] = $true
|
||||||
|
}
|
||||||
|
# Соседние проверки этого блока эвристичны (имя стиля добывается регуляркой), поэтому там
|
||||||
|
# предупреждение. Здесь сигнал точный — имя ТЧ берётся из атрибута, — а последствие жёсткое,
|
||||||
|
# поэтому ошибка.
|
||||||
|
if ($acTables.Count -gt 0) {
|
||||||
|
$ownerKey = ($ctx -split '\.Form\.')[0]
|
||||||
|
$ownerTS = $script:borrowedTSIndex[$ownerKey]
|
||||||
|
foreach ($tblName in $acTables.Keys) {
|
||||||
|
$depCheckCount++
|
||||||
|
if (-not $ownerTS -or -not $ownerTS.ContainsKey($tblName)) {
|
||||||
|
Report-Error "12. ${ctx}: <AdditionalColumns table=`"Объект.${tblName}`"> — TabularSection.${tblName} not borrowed in extension"
|
||||||
|
$check12Ok = $false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
foreach ($mi in $missingItems) {
|
foreach ($mi in $missingItems) {
|
||||||
Report-Warn "12. ${ctx}: references ${mi} not borrowed in extension"
|
Report-Warn "12. ${ctx}: references ${mi} not borrowed in extension"
|
||||||
$check12Ok = $false
|
$check12Ok = $false
|
||||||
@@ -930,6 +1023,114 @@ if ($script:borrowedFormsWithTree.Count -eq 0) {
|
|||||||
Report-OK "13. TypeLink: clean"
|
Report-OK "13. TypeLink: clean"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- Check 14: пути Объект.* заимствованных форм против конфигурации-источника ---
|
||||||
|
# Требует -ConfigPath: отличить живой путь от висячего можно только по исходному объекту.
|
||||||
|
# «Объект.Партнер» валиден и без заимствования реквизита (наследуется от базы), а «Объект.Товары.Артикул»
|
||||||
|
# не разрешится нигде, если Артикул — не реквизит объекта и не колонка из <Columns> самой формы.
|
||||||
|
# Такой путь платформа отвергает на загрузке: «Неверный путь к данным».
|
||||||
|
if (-not $script:stopped -and $script:borrowedFormsWithTree.Count -gt 0) {
|
||||||
|
if (-not $ConfigPath) {
|
||||||
|
Out-Line "[INFO] 14. Пути Объект.* против конфигурации-источника не проверялись: не задан -ConfigPath"
|
||||||
|
} else {
|
||||||
|
$cfgRoot = $ConfigPath
|
||||||
|
if (-not [System.IO.Path]::IsPathRooted($cfgRoot)) { $cfgRoot = Join-Path (Get-Location).Path $cfgRoot }
|
||||||
|
if ((Test-Path $cfgRoot) -and -not (Test-Path $cfgRoot -PathType Container)) { $cfgRoot = Split-Path $cfgRoot -Parent }
|
||||||
|
|
||||||
|
if (-not (Test-Path (Join-Path $cfgRoot "Configuration.xml"))) {
|
||||||
|
Report-Warn "14. -ConfigPath '$ConfigPath': Configuration.xml не найден — проверка путей пропущена"
|
||||||
|
} else {
|
||||||
|
$check14Ok = $true
|
||||||
|
$pathCheckCount = 0
|
||||||
|
|
||||||
|
foreach ($bf in $script:borrowedFormsWithTree) {
|
||||||
|
$raw = $bf.RawText
|
||||||
|
$ctx = $bf.Context
|
||||||
|
$ownerKey = ($ctx -split '\.Form\.')[0]
|
||||||
|
$ownerParts = $ownerKey -split '\.', 2
|
||||||
|
if ($ownerParts.Count -lt 2) { continue }
|
||||||
|
$ownerType = $ownerParts[0]; $ownerName = $ownerParts[1]
|
||||||
|
$ownerDir = $childTypeDirMap[$ownerType]
|
||||||
|
if (-not $ownerDir) { continue }
|
||||||
|
$srcObjFile = Join-Path (Join-Path $cfgRoot $ownerDir) "${ownerName}.xml"
|
||||||
|
if (-not (Test-Path $srcObjFile)) {
|
||||||
|
Report-Warn "14. ${ctx}: объект-источник не найден в конфигурации ($ownerDir/${ownerName}.xml)"
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
# Имена, доступные первым сегментом пути: реквизиты и ТЧ объекта-источника.
|
||||||
|
# Плюс для каждой ТЧ — её колонки: второй сегмент проверяем по ним (именно там
|
||||||
|
# и жил дефект — Объект.Товары.Артикул при живой ТЧ Товары).
|
||||||
|
$srcNames = @{}
|
||||||
|
$srcTSColumns = @{}
|
||||||
|
$srcDoc = New-Object System.Xml.XmlDocument
|
||||||
|
$srcDoc.PreserveWhitespace = $false
|
||||||
|
$srcDoc.Load($srcObjFile)
|
||||||
|
$srcObjEl = $null
|
||||||
|
foreach ($c in $srcDoc.DocumentElement.ChildNodes) {
|
||||||
|
if ($c.NodeType -eq 'Element') { $srcObjEl = $c; break }
|
||||||
|
}
|
||||||
|
$srcChildObjects = if ($srcObjEl) { $srcObjEl.SelectSingleNode("*[local-name()='ChildObjects']") } else { $null }
|
||||||
|
if ($srcChildObjects) {
|
||||||
|
foreach ($sub in $srcChildObjects.ChildNodes) {
|
||||||
|
if ($sub.NodeType -ne 'Element') { continue }
|
||||||
|
if ($sub.LocalName -notin @('Attribute','TabularSection')) { continue }
|
||||||
|
$nameNode = $sub.SelectSingleNode("*[local-name()='Properties']/*[local-name()='Name']")
|
||||||
|
if (-not $nameNode) { continue }
|
||||||
|
$subName = $nameNode.InnerText.Trim()
|
||||||
|
$srcNames[$subName] = $true
|
||||||
|
if ($sub.LocalName -ne 'TabularSection') { continue }
|
||||||
|
$cols = @{}
|
||||||
|
foreach ($colName in $sub.SelectNodes("*[local-name()='ChildObjects']/*[local-name()='Attribute']/*[local-name()='Properties']/*[local-name()='Name']")) {
|
||||||
|
$cols[$colName.InnerText.Trim()] = $true
|
||||||
|
}
|
||||||
|
$srcTSColumns[$subName] = $cols
|
||||||
|
}
|
||||||
|
}
|
||||||
|
# Плюс колонки, объявленные в самой форме через <Columns>/<AdditionalColumns table="Объект.X">
|
||||||
|
foreach ($acm in [regex]::Matches($raw, '(?s)<AdditionalColumns table="Объект\.(\w+)">(.*?)</AdditionalColumns>')) {
|
||||||
|
$tbl = $acm.Groups[1].Value
|
||||||
|
if (-not $srcTSColumns.ContainsKey($tbl)) { $srcTSColumns[$tbl] = @{} }
|
||||||
|
foreach ($cm in [regex]::Matches($acm.Groups[2].Value, '<Column name="(\w+)"')) {
|
||||||
|
$srcTSColumns[$tbl][$cm.Groups[1].Value] = $true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$badPaths = @{}
|
||||||
|
foreach ($m in [regex]::Matches($raw, '<(?:\w+:)?\w*DataPath[^>]*>Объект\.([^<]+)</(?:\w+:)?\w*DataPath>')) {
|
||||||
|
$segments = $m.Groups[1].Value -split '\.'
|
||||||
|
$seg0 = $segments[0]
|
||||||
|
$pathCheckCount++
|
||||||
|
if ($script:standardObjectFields -contains $seg0) { continue }
|
||||||
|
if (-not $srcNames.ContainsKey($seg0)) {
|
||||||
|
$badPaths["Объект.${seg0}"] = "у ${ownerKey} нет такого реквизита или табличной части"
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
# Второй сегмент проверяем только для табличных частей: у ссылочного реквизита
|
||||||
|
# он ведёт в чужой объект, и это уже другая проверка.
|
||||||
|
if ($segments.Count -lt 2 -or -not $srcTSColumns.ContainsKey($seg0)) { continue }
|
||||||
|
$seg1 = $segments[1]
|
||||||
|
if ($script:standardObjectFields -contains $seg1) { continue }
|
||||||
|
# Итог колонки — псевдополе платформы: Total<Колонка> при живой колонке законен
|
||||||
|
if ($seg1 -like "Total*" -and $srcTSColumns[$seg0].ContainsKey($seg1.Substring(5))) { continue }
|
||||||
|
if (-not $srcTSColumns[$seg0].ContainsKey($seg1)) {
|
||||||
|
$badPaths["Объект.${seg0}.${seg1}"] = "у табличной части ${seg0} нет колонки ${seg1}, и <Columns> формы её не объявляет"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foreach ($bad in ($badPaths.Keys | Sort-Object)) {
|
||||||
|
Report-Error "14. ${ctx}: путь '${bad}' — $($badPaths[$bad])"
|
||||||
|
$check14Ok = $false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($check14Ok) {
|
||||||
|
Report-OK "14. Object paths vs source config: $pathCheckCount checked"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($script:stopped) { & $finalize; exit 1 }
|
||||||
|
|
||||||
# --- Breadcrumb: controlled methods (&ИзменениеИКонтроль) drift is not checked here ---
|
# --- Breadcrumb: controlled methods (&ИзменениеИКонтроль) drift is not checked here ---
|
||||||
$extRootDir = Split-Path $resolvedPath -Parent
|
$extRootDir = Split-Path $resolvedPath -Parent
|
||||||
$ctrlCount = 0
|
$ctrlCount = 0
|
||||||
|
|||||||
@@ -1,10 +1,32 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# cfe-validate v1.4 — Validate 1C configuration extension XML structure (CFE)
|
# cfe-validate v1.8 — Validate 1C configuration extension XML structure (CFE) (полнота GeneratedType, ТЧ из AdditionalColumns, сверка путей с -ConfigPath)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
"""Validates extension Configuration.xml: root, InternalInfo, extension properties, ChildObjects, borrowed objects."""
|
"""Validates extension Configuration.xml: root, InternalInfo, extension properties, ChildObjects, borrowed objects."""
|
||||||
import sys, os, argparse, re
|
import sys, os, argparse, re
|
||||||
from lxml import etree
|
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)
|
||||||
|
|
||||||
|
|
||||||
NS = {
|
NS = {
|
||||||
'md': 'http://v8.1c.ru/8.3/MDClasses',
|
'md': 'http://v8.1c.ru/8.3/MDClasses',
|
||||||
'v8': 'http://v8.1c.ru/8.1/data/core',
|
'v8': 'http://v8.1c.ru/8.1/data/core',
|
||||||
@@ -37,7 +59,7 @@ VALID_CLASS_IDS = [
|
|||||||
CHILD_OBJECT_TYPES = [
|
CHILD_OBJECT_TYPES = [
|
||||||
'Language', 'Subsystem', 'StyleItem', 'Style',
|
'Language', 'Subsystem', 'StyleItem', 'Style',
|
||||||
'CommonPicture', 'SessionParameter', 'Role', 'CommonTemplate',
|
'CommonPicture', 'SessionParameter', 'Role', 'CommonTemplate',
|
||||||
'FilterCriterion', 'CommonModule', 'CommonAttribute', 'ExchangePlan',
|
'FilterCriterion', 'CommonModule', 'Bot', 'CommonAttribute', 'ExchangePlan',
|
||||||
'XDTOPackage', 'WebService', 'HTTPService', 'WSReference',
|
'XDTOPackage', 'WebService', 'HTTPService', 'WSReference',
|
||||||
'EventSubscription', 'ScheduledJob', 'SettingsStorage', 'FunctionalOption',
|
'EventSubscription', 'ScheduledJob', 'SettingsStorage', 'FunctionalOption',
|
||||||
'FunctionalOptionsParameter', 'DefinedType', 'CommonCommand', 'CommandGroup',
|
'FunctionalOptionsParameter', 'DefinedType', 'CommonCommand', 'CommandGroup',
|
||||||
@@ -54,6 +76,7 @@ CHILD_TYPE_DIR_MAP = {
|
|||||||
'Language': 'Languages', 'Subsystem': 'Subsystems', 'StyleItem': 'StyleItems', 'Style': 'Styles',
|
'Language': 'Languages', 'Subsystem': 'Subsystems', 'StyleItem': 'StyleItems', 'Style': 'Styles',
|
||||||
'CommonPicture': 'CommonPictures', 'SessionParameter': 'SessionParameters', 'Role': 'Roles',
|
'CommonPicture': 'CommonPictures', 'SessionParameter': 'SessionParameters', 'Role': 'Roles',
|
||||||
'CommonTemplate': 'CommonTemplates', 'FilterCriterion': 'FilterCriteria', 'CommonModule': 'CommonModules',
|
'CommonTemplate': 'CommonTemplates', 'FilterCriterion': 'FilterCriteria', 'CommonModule': 'CommonModules',
|
||||||
|
'Bot': 'Bots',
|
||||||
'CommonAttribute': 'CommonAttributes', 'ExchangePlan': 'ExchangePlans', 'XDTOPackage': 'XDTOPackages',
|
'CommonAttribute': 'CommonAttributes', 'ExchangePlan': 'ExchangePlans', 'XDTOPackage': 'XDTOPackages',
|
||||||
'WebService': 'WebServices', 'HTTPService': 'HTTPServices', 'WSReference': 'WSReferences',
|
'WebService': 'WebServices', 'HTTPService': 'HTTPServices', 'WSReference': 'WSReferences',
|
||||||
'EventSubscription': 'EventSubscriptions', 'ScheduledJob': 'ScheduledJobs',
|
'EventSubscription': 'EventSubscriptions', 'ScheduledJob': 'ScheduledJobs',
|
||||||
@@ -73,6 +96,46 @@ CHILD_TYPE_DIR_MAP = {
|
|||||||
'IntegrationService': 'IntegrationServices',
|
'IntegrationService': 'IntegrationServices',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Наборы GeneratedType по типу объекта (эталон — таблица §2.5 спецификации конфигурации).
|
||||||
|
# Неполный набор в заимствованной оболочке платформа отвергает при загрузке: «отсутствует один
|
||||||
|
# или более типов объекта <Тип>». Типы, у которых GeneratedType нет вовсе (общие модули,
|
||||||
|
# подписки, регламентные задания и т.п.), в карте отсутствуют — для них проверка не выполняется.
|
||||||
|
GENERATED_TYPE_CATEGORIES = {
|
||||||
|
'Catalog': ['Object', 'Ref', 'Selection', 'List', 'Manager'],
|
||||||
|
'Document': ['Object', 'Ref', 'Selection', 'List', 'Manager'],
|
||||||
|
'Enum': ['Ref', 'Manager', 'List'],
|
||||||
|
'Constant': ['Manager', 'ValueManager', 'ValueKey'],
|
||||||
|
'Report': ['Object', 'Manager'],
|
||||||
|
'DataProcessor': ['Object', 'Manager'],
|
||||||
|
'ExchangePlan': ['Object', 'Ref', 'Selection', 'List', 'Manager'],
|
||||||
|
'Task': ['Object', 'Ref', 'Selection', 'List', 'Manager'],
|
||||||
|
'BusinessProcess': ['Object', 'Ref', 'Selection', 'List', 'Manager', 'RoutePointRef'],
|
||||||
|
'ChartOfCharacteristicTypes': ['Object', 'Ref', 'Selection', 'List', 'Manager', 'Characteristic'],
|
||||||
|
'ChartOfAccounts': ['Object', 'Ref', 'Selection', 'List', 'Manager', 'ExtDimensionTypes', 'ExtDimensionTypesRow'],
|
||||||
|
'ChartOfCalculationTypes': ['Object', 'Ref', 'Selection', 'List', 'Manager', 'DisplacingCalculationTypes', 'DisplacingCalculationTypesRow', 'BaseCalculationTypes', 'BaseCalculationTypesRow', 'LeadingCalculationTypes', 'LeadingCalculationTypesRow'],
|
||||||
|
'InformationRegister': ['Record', 'Manager', 'Selection', 'List', 'RecordSet', 'RecordKey', 'RecordManager'],
|
||||||
|
'AccumulationRegister': ['Record', 'Manager', 'Selection', 'List', 'RecordSet', 'RecordKey'],
|
||||||
|
'AccountingRegister': ['Record', 'Manager', 'Selection', 'List', 'RecordSet', 'RecordKey', 'ExtDimensions'],
|
||||||
|
'CalculationRegister': ['Record', 'Manager', 'Selection', 'List', 'RecordSet', 'RecordKey', 'Recalcs'],
|
||||||
|
'DocumentJournal': ['Selection', 'List', 'Manager'],
|
||||||
|
'Sequence': ['Record', 'Manager', 'RecordSet'],
|
||||||
|
'FilterCriterion': ['Manager', 'List'],
|
||||||
|
'SettingsStorage': ['Manager'],
|
||||||
|
'IntegrationService': ['Manager'],
|
||||||
|
'WSReference': ['Manager'],
|
||||||
|
'DefinedType': ['DefinedType'],
|
||||||
|
}
|
||||||
|
|
||||||
|
# Стандартные реквизиты объектов: в ChildObjects их нет, но пути Объект.<Стандартный> законны.
|
||||||
|
# Имена зависят от варианта встроенного языка, поэтому держим оба написания.
|
||||||
|
STANDARD_OBJECT_FIELDS = {
|
||||||
|
'Code', 'Description', 'Ref', 'Parent', 'Owner', 'DeletionMark', 'Predefined', 'IsFolder', 'LineNumber',
|
||||||
|
'Number', 'Date', 'Posted', 'PredefinedDataName', 'RegisterRecords', 'DataVersion', 'RowsCount',
|
||||||
|
'Код', 'Наименование', 'Ссылка', 'Родитель', 'Владелец', 'ПометкаУдаления', 'Предопределенный',
|
||||||
|
'ЭтоГруппа', 'НомерСтроки', 'Номер', 'Дата', 'Проведен', 'ИмяПредопределенныхДанных',
|
||||||
|
'Движения', 'ВерсияДанных', 'КоличествоСтрок',
|
||||||
|
}
|
||||||
|
|
||||||
# Valid enum values for extension properties
|
# Valid enum values for extension properties
|
||||||
VALID_ENUM_VALUES = {
|
VALID_ENUM_VALUES = {
|
||||||
'ConfigurationExtensionCompatibilityMode': [
|
'ConfigurationExtensionCompatibilityMode': [
|
||||||
@@ -154,11 +217,15 @@ def main():
|
|||||||
parser.add_argument('-Detailed', action='store_true')
|
parser.add_argument('-Detailed', action='store_true')
|
||||||
parser.add_argument('-MaxErrors', dest='MaxErrors', type=int, default=30)
|
parser.add_argument('-MaxErrors', dest='MaxErrors', type=int, default=30)
|
||||||
parser.add_argument('-OutFile', dest='OutFile', default='')
|
parser.add_argument('-OutFile', dest='OutFile', default='')
|
||||||
args = parser.parse_args()
|
# Конфигурация-источник. Без неё проверки, требующие сравнения с основной конфигурацией,
|
||||||
|
# пропускаются (о чём сказано в отчёте), остальные работают как раньше.
|
||||||
|
parser.add_argument('-ConfigPath', dest='ConfigPath', default='')
|
||||||
|
args = ci_parse_args(parser)
|
||||||
|
|
||||||
extension_path = args.ExtensionPath
|
extension_path = args.ExtensionPath
|
||||||
max_errors = args.MaxErrors
|
max_errors = args.MaxErrors
|
||||||
out_file = args.OutFile
|
out_file = args.OutFile
|
||||||
|
config_path_arg = args.ConfigPath
|
||||||
|
|
||||||
# --- Resolve path ---
|
# --- Resolve path ---
|
||||||
if not os.path.isabs(extension_path):
|
if not os.path.isabs(extension_path):
|
||||||
@@ -216,8 +283,9 @@ def main():
|
|||||||
version = root.get('version', '')
|
version = root.get('version', '')
|
||||||
if not version:
|
if not version:
|
||||||
r.warn('1. Missing version attribute on MetaDataObject')
|
r.warn('1. Missing version attribute on MetaDataObject')
|
||||||
elif version not in ('2.17', '2.20', '2.21'):
|
elif version not in ('2.17', '2.18', '2.19', '2.20', '2.21'):
|
||||||
r.warn(f"1. Unusual version '{version}' (expected 2.17, 2.20 or 2.21)")
|
# Лестница версий формата: 2.17 (8.3.20-8.3.24), 2.18 (8.3.25), 2.19 (8.3.26), 2.20 (8.3.27).
|
||||||
|
r.warn(f"1. Unusual version '{version}' (expected 2.17-2.20 or 2.21)")
|
||||||
|
|
||||||
# Must have Configuration child
|
# Must have Configuration child
|
||||||
cfg_node = None
|
cfg_node = None
|
||||||
@@ -536,6 +604,7 @@ def main():
|
|||||||
MD = NS['md']
|
MD = NS['md']
|
||||||
XR = NS['xr']
|
XR = NS['xr']
|
||||||
enum_values_index = {}
|
enum_values_index = {}
|
||||||
|
borrowed_ts_index = {}
|
||||||
form_list = []
|
form_list = []
|
||||||
|
|
||||||
def is_borrowed_sub_item(sub_item):
|
def is_borrowed_sub_item(sub_item):
|
||||||
@@ -635,6 +704,23 @@ def main():
|
|||||||
else:
|
else:
|
||||||
borrowed_ok_count += 1
|
borrowed_ok_count += 1
|
||||||
|
|
||||||
|
# Полнота набора GeneratedType: платформа отвергает оболочку с неполным набором
|
||||||
|
# («отсутствует один или более типов объекта ChartOfCharacteristicTypes»)
|
||||||
|
expected_cats = GENERATED_TYPE_CATEGORIES.get(type_name)
|
||||||
|
if expected_cats:
|
||||||
|
obj_info = obj_el.find(f'{{{MD}}}InternalInfo')
|
||||||
|
found_cats = set()
|
||||||
|
if obj_info is not None:
|
||||||
|
for gt in obj_info.findall(f'{{{XR}}}GeneratedType'):
|
||||||
|
cat = gt.get('category')
|
||||||
|
if cat:
|
||||||
|
found_cats.add(cat)
|
||||||
|
missing_cats = [c for c in expected_cats if c not in found_cats]
|
||||||
|
if missing_cats:
|
||||||
|
word = 'category' if len(missing_cats) == 1 else 'categories'
|
||||||
|
r.error(f"9. Borrowed {type_name}.{child_name}: missing GeneratedType {word} {', '.join(missing_cats)}")
|
||||||
|
check9_ok = False
|
||||||
|
|
||||||
# --- Check 10: Sub-items (Attribute, TabularSection, EnumValue, Form) ---
|
# --- Check 10: Sub-items (Attribute, TabularSection, EnumValue, Form) ---
|
||||||
obj_child_objects = obj_el.find(f'{{{MD}}}ChildObjects')
|
obj_child_objects = obj_el.find(f'{{{MD}}}ChildObjects')
|
||||||
if obj_child_objects is not None:
|
if obj_child_objects is not None:
|
||||||
@@ -662,6 +748,9 @@ def main():
|
|||||||
ts_info = sub_item.find(f'{{{MD}}}InternalInfo')
|
ts_info = sub_item.find(f'{{{MD}}}InternalInfo')
|
||||||
ts_name_el = sub_item.find(f'{{{MD}}}Properties/{{{MD}}}Name')
|
ts_name_el = sub_item.find(f'{{{MD}}}Properties/{{{MD}}}Name')
|
||||||
ts_label = (ts_name_el.text or '?') if ts_name_el is not None else '?'
|
ts_label = (ts_name_el.text or '?') if ts_name_el is not None else '?'
|
||||||
|
# Индекс заимствованных ТЧ — по нему Check 12 сверяет <AdditionalColumns table="Объект.X">
|
||||||
|
if ts_name_el is not None and ts_name_el.text:
|
||||||
|
borrowed_ts_index.setdefault(f'{type_name}.{child_name}', {})[ts_name_el.text.strip()] = True
|
||||||
if ts_info is None:
|
if ts_info is None:
|
||||||
r.error(f'10. {ctx}: TabularSection.{ts_label} missing InternalInfo')
|
r.error(f'10. {ctx}: TabularSection.{ts_label} missing InternalInfo')
|
||||||
check10_ok = False
|
check10_ok = False
|
||||||
@@ -854,6 +943,22 @@ def main():
|
|||||||
elif entry['Enum'] not in enum_values_index or entry['Value'] not in enum_values_index.get(entry['Enum'], {}):
|
elif entry['Enum'] not in enum_values_index or entry['Value'] not in enum_values_index.get(entry['Enum'], {}):
|
||||||
missing_items.append(f"Enum.{entry['Enum']}.EnumValue.{entry['Value']}")
|
missing_items.append(f"Enum.{entry['Enum']}.EnumValue.{entry['Value']}")
|
||||||
|
|
||||||
|
# <AdditionalColumns table="Объект.X"> — доп. колонки табличной части, объявленные в самой форме.
|
||||||
|
# Колонки есть, а самой ТЧ в расширении нет → платформа отвергает загрузку: «Неверный путь к
|
||||||
|
# данным» плюс «Колонки не могут быть добавлены к реквизиту».
|
||||||
|
# Соседние проверки этого блока эвристичны (имя стиля добывается регуляркой), поэтому там
|
||||||
|
# предупреждение. Здесь сигнал точный — имя ТЧ берётся из атрибута, — а последствие жёсткое,
|
||||||
|
# поэтому ошибка.
|
||||||
|
ac_tables = set(re.findall(r'<AdditionalColumns table="Объект\.(\w+)"', raw))
|
||||||
|
if ac_tables:
|
||||||
|
owner_key = ctx.split('.Form.')[0]
|
||||||
|
owner_ts = borrowed_ts_index.get(owner_key, {})
|
||||||
|
for tbl_name in sorted(ac_tables):
|
||||||
|
dep_check_count += 1
|
||||||
|
if tbl_name not in owner_ts:
|
||||||
|
r.error(f'12. {ctx}: <AdditionalColumns table="Объект.{tbl_name}"> — TabularSection.{tbl_name} not borrowed in extension')
|
||||||
|
check12_ok = False
|
||||||
|
|
||||||
for mi in missing_items:
|
for mi in missing_items:
|
||||||
r.warn(f'12. {ctx}: references {mi} not borrowed in extension')
|
r.warn(f'12. {ctx}: references {mi} not borrowed in extension')
|
||||||
check12_ok = False
|
check12_ok = False
|
||||||
@@ -885,6 +990,115 @@ def main():
|
|||||||
elif check13_ok:
|
elif check13_ok:
|
||||||
r.ok('13. TypeLink: clean')
|
r.ok('13. TypeLink: clean')
|
||||||
|
|
||||||
|
# --- Check 14: пути Объект.* заимствованных форм против конфигурации-источника ---
|
||||||
|
# Требует -ConfigPath: отличить живой путь от висячего можно только по исходному объекту.
|
||||||
|
# «Объект.Партнер» валиден и без заимствования реквизита (наследуется от базы), а «Объект.Товары.Артикул»
|
||||||
|
# не разрешится нигде, если Артикул — не колонка ТЧ и не колонка из <Columns> самой формы.
|
||||||
|
# Такой путь платформа отвергает на загрузке: «Неверный путь к данным».
|
||||||
|
if not r.stopped and borrowed_forms_with_tree:
|
||||||
|
if not config_path_arg:
|
||||||
|
r.out('[INFO] 14. Пути Объект.* против конфигурации-источника не проверялись: не задан -ConfigPath')
|
||||||
|
else:
|
||||||
|
cfg_root = config_path_arg
|
||||||
|
if not os.path.isabs(cfg_root):
|
||||||
|
cfg_root = os.path.join(os.getcwd(), cfg_root)
|
||||||
|
if os.path.exists(cfg_root) and not os.path.isdir(cfg_root):
|
||||||
|
cfg_root = os.path.dirname(cfg_root)
|
||||||
|
|
||||||
|
if not os.path.isfile(os.path.join(cfg_root, 'Configuration.xml')):
|
||||||
|
r.warn(f"14. -ConfigPath '{config_path_arg}': Configuration.xml не найден — проверка путей пропущена")
|
||||||
|
else:
|
||||||
|
check14_ok = True
|
||||||
|
path_check_count = 0
|
||||||
|
|
||||||
|
for bf in borrowed_forms_with_tree:
|
||||||
|
raw = bf['RawText']
|
||||||
|
ctx = bf['Context']
|
||||||
|
owner_key = ctx.split('.Form.')[0]
|
||||||
|
owner_parts = owner_key.split('.', 1)
|
||||||
|
if len(owner_parts) < 2:
|
||||||
|
continue
|
||||||
|
owner_type, owner_name = owner_parts
|
||||||
|
owner_dir = CHILD_TYPE_DIR_MAP.get(owner_type)
|
||||||
|
if not owner_dir:
|
||||||
|
continue
|
||||||
|
src_obj_file = os.path.join(cfg_root, owner_dir, f'{owner_name}.xml')
|
||||||
|
if not os.path.isfile(src_obj_file):
|
||||||
|
r.warn(f'14. {ctx}: объект-источник не найден в конфигурации ({owner_dir}/{owner_name}.xml)')
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Имена, доступные первым сегментом пути: реквизиты и ТЧ объекта-источника.
|
||||||
|
# Плюс для каждой ТЧ — её колонки: второй сегмент проверяем по ним (именно там
|
||||||
|
# и жил дефект — Объект.Товары.Артикул при живой ТЧ Товары).
|
||||||
|
src_names = set()
|
||||||
|
src_ts_columns = {}
|
||||||
|
src_tree = etree.parse(src_obj_file, etree.XMLParser(remove_blank_text=True))
|
||||||
|
src_obj_el = None
|
||||||
|
for c in src_tree.getroot():
|
||||||
|
if isinstance(c.tag, str):
|
||||||
|
src_obj_el = c
|
||||||
|
break
|
||||||
|
src_child_objects = src_obj_el.find(f'{{{MD}}}ChildObjects') if src_obj_el is not None else None
|
||||||
|
if src_child_objects is not None:
|
||||||
|
for sub in src_child_objects:
|
||||||
|
if not isinstance(sub.tag, str):
|
||||||
|
continue
|
||||||
|
sub_ln = etree.QName(sub.tag).localname
|
||||||
|
if sub_ln not in ('Attribute', 'TabularSection'):
|
||||||
|
continue
|
||||||
|
name_el = sub.find(f'{{{MD}}}Properties/{{{MD}}}Name')
|
||||||
|
if name_el is None or not name_el.text:
|
||||||
|
continue
|
||||||
|
sub_name = name_el.text.strip()
|
||||||
|
src_names.add(sub_name)
|
||||||
|
if sub_ln != 'TabularSection':
|
||||||
|
continue
|
||||||
|
cols = set()
|
||||||
|
for col_name in sub.findall(f'{{{MD}}}ChildObjects/{{{MD}}}Attribute/{{{MD}}}Properties/{{{MD}}}Name'):
|
||||||
|
if col_name.text:
|
||||||
|
cols.add(col_name.text.strip())
|
||||||
|
src_ts_columns[sub_name] = cols
|
||||||
|
# Плюс колонки, объявленные в самой форме через <Columns>/<AdditionalColumns table="Объект.X">
|
||||||
|
for acm in re.finditer(r'<AdditionalColumns table="Объект\.(\w+)">(.*?)</AdditionalColumns>', raw, re.DOTALL):
|
||||||
|
tbl = acm.group(1)
|
||||||
|
cols = src_ts_columns.setdefault(tbl, set())
|
||||||
|
for cm in re.finditer(r'<Column name="(\w+)"', acm.group(2)):
|
||||||
|
cols.add(cm.group(1))
|
||||||
|
|
||||||
|
bad_paths = {}
|
||||||
|
for m in re.finditer(r'<(?:\w+:)?\w*DataPath[^>]*>Объект\.([^<]+)</(?:\w+:)?\w*DataPath>', raw):
|
||||||
|
segments = m.group(1).split('.')
|
||||||
|
seg0 = segments[0]
|
||||||
|
path_check_count += 1
|
||||||
|
if seg0 in STANDARD_OBJECT_FIELDS:
|
||||||
|
continue
|
||||||
|
if seg0 not in src_names:
|
||||||
|
bad_paths[f'Объект.{seg0}'] = f'у {owner_key} нет такого реквизита или табличной части'
|
||||||
|
continue
|
||||||
|
# Второй сегмент проверяем только для табличных частей: у ссылочного реквизита
|
||||||
|
# он ведёт в чужой объект, и это уже другая проверка.
|
||||||
|
if len(segments) < 2 or seg0 not in src_ts_columns:
|
||||||
|
continue
|
||||||
|
seg1 = segments[1]
|
||||||
|
if seg1 in STANDARD_OBJECT_FIELDS:
|
||||||
|
continue
|
||||||
|
# Итог колонки — псевдополе платформы: Total<Колонка> при живой колонке законен
|
||||||
|
if seg1.startswith('Total') and seg1[5:] in src_ts_columns[seg0]:
|
||||||
|
continue
|
||||||
|
if seg1 not in src_ts_columns[seg0]:
|
||||||
|
bad_paths[f'Объект.{seg0}.{seg1}'] = f'у табличной части {seg0} нет колонки {seg1}, и <Columns> формы её не объявляет'
|
||||||
|
|
||||||
|
for bad in sorted(bad_paths):
|
||||||
|
r.error(f"14. {ctx}: путь '{bad}' — {bad_paths[bad]}")
|
||||||
|
check14_ok = False
|
||||||
|
|
||||||
|
if check14_ok:
|
||||||
|
r.ok(f'14. Object paths vs source config: {path_check_count} checked')
|
||||||
|
|
||||||
|
if r.stopped:
|
||||||
|
r.finalize(out_file)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
# --- Breadcrumb: controlled methods (&ИзменениеИКонтроль) drift is not checked here ---
|
# --- Breadcrumb: controlled methods (&ИзменениеИКонтроль) drift is not checked here ---
|
||||||
ctrl_count = 0
|
ctrl_count = 0
|
||||||
for dp, _dn, files in os.walk(config_dir):
|
for dp, _dn, files in os.walk(config_dir):
|
||||||
|
|||||||
@@ -45,6 +45,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-create.ps1" <п
|
|||||||
| `-UseTemplate <файл>` | нет | Создать из шаблона (.cf или .dt) |
|
| `-UseTemplate <файл>` | нет | Создать из шаблона (.cf или .dt) |
|
||||||
| `-AddToList` | нет | Добавить в список баз 1С |
|
| `-AddToList` | нет | Добавить в список баз 1С |
|
||||||
| `-ListName <имя>` | нет | Имя базы в списке |
|
| `-ListName <имя>` | нет | Имя базы в списке |
|
||||||
|
| `-AdditionalV8Arguments <список>` | нет | Доп. аргументы запуска `1cv8.exe` через запятую, напр. `/UseHwLicenses+` |
|
||||||
|
| `-AdditionalIbcmdArguments <список>` | нет | Доп. аргументы `ibcmd` через запятую, в форме `--ключ=значение` |
|
||||||
|
|
||||||
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
|
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# db-create v1.7 — Create 1C information base
|
# db-create v1.11 — Create 1C information base
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||||
<#
|
<#
|
||||||
@@ -30,6 +30,12 @@
|
|||||||
.PARAMETER ListName
|
.PARAMETER ListName
|
||||||
Имя базы в списке
|
Имя базы в списке
|
||||||
|
|
||||||
|
.PARAMETER AdditionalV8Arguments
|
||||||
|
Дополнительные аргументы запуска 1cv8.exe (например /UseHwLicenses+)
|
||||||
|
|
||||||
|
.PARAMETER AdditionalIbcmdArguments
|
||||||
|
Дополнительные аргументы запуска ibcmd (форма --ключ=значение)
|
||||||
|
|
||||||
.EXAMPLE
|
.EXAMPLE
|
||||||
.\db-create.ps1 -InfoBasePath "C:\Bases\NewDB"
|
.\db-create.ps1 -InfoBasePath "C:\Bases\NewDB"
|
||||||
|
|
||||||
@@ -61,12 +67,163 @@ param(
|
|||||||
[switch]$AddToList,
|
[switch]$AddToList,
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
[Parameter(Mandatory=$false)]
|
||||||
[string]$ListName
|
[string]$ListName,
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[string[]]$AdditionalV8Arguments = @(),
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[string[]]$AdditionalIbcmdArguments = @()
|
||||||
)
|
)
|
||||||
|
|
||||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
|
||||||
|
# --- Additional platform arguments ---
|
||||||
|
$script:V8OwnedKeys = @(
|
||||||
|
'DESIGNER', 'ENTERPRISE', 'CREATEINFOBASE', 'CONFIG',
|
||||||
|
'/F', '/S', '/N', '/P', '/Out', '/DisableStartupDialogs',
|
||||||
|
'/UseTemplate', '/AddToList', '/Execute', '/C', '/URL', '/UC',
|
||||||
|
'/DumpIB', '/RestoreIB', '/DumpCfg', '/LoadCfg',
|
||||||
|
'/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg',
|
||||||
|
'/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles'
|
||||||
|
)
|
||||||
|
$script:IbcmdOwnedKeys = @(
|
||||||
|
'--db-path', '--data', '--out', '--file', '--load', '--restore',
|
||||||
|
'--import', '--export', '--apply', '--force', '--create-database',
|
||||||
|
'--user', '--password'
|
||||||
|
)
|
||||||
|
$script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP')
|
||||||
|
$script:IbcmdSecretKeys = @('--password', '--token', '--db-pwd')
|
||||||
|
|
||||||
|
function Test-ArgKeyMatch {
|
||||||
|
# A token matches a key when it equals the key, or starts with it and the next
|
||||||
|
# character is not a letter — catches glued /N"user" and --password=x, while
|
||||||
|
# keeping /ClearCache distinct from /C.
|
||||||
|
param([string]$Token, [string]$Key)
|
||||||
|
if ($Token.Length -lt $Key.Length) { return $false }
|
||||||
|
if (-not $Token.Substring(0, $Key.Length).Equals($Key, [System.StringComparison]::OrdinalIgnoreCase)) { return $false }
|
||||||
|
if ($Token.Length -eq $Key.Length) { return $true }
|
||||||
|
return -not [char]::IsLetter($Token[$Key.Length])
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-ProjectExtraArgs {
|
||||||
|
# v8args / ibcmdargs from .v8-project.json — same upward walk as v8path.
|
||||||
|
param([string]$Name)
|
||||||
|
$dir = (Get-Location).Path
|
||||||
|
while ($dir) {
|
||||||
|
$pf = Join-Path $dir ".v8-project.json"
|
||||||
|
if (Test-Path $pf) {
|
||||||
|
try {
|
||||||
|
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||||
|
if ($j.$Name) { return @($j.$Name | ForEach-Object { [string]$_ }) }
|
||||||
|
} catch {}
|
||||||
|
return @()
|
||||||
|
}
|
||||||
|
$parent = Split-Path $dir -Parent
|
||||||
|
if (-not $parent -or $parent -eq $dir) { break }
|
||||||
|
$dir = $parent
|
||||||
|
}
|
||||||
|
return @()
|
||||||
|
}
|
||||||
|
|
||||||
|
function Assert-ExtraArgs {
|
||||||
|
# The platform accepts only one batch operation, and a duplicate connection or
|
||||||
|
# output key fails with an opaque 1C error — reject what the skill owns itself.
|
||||||
|
param([string[]]$ExtraArgs, [string]$Engine, [hashtable]$Hints)
|
||||||
|
$paramName = if ($Engine -eq 'ibcmd') { '-AdditionalIbcmdArguments' } else { '-AdditionalV8Arguments' }
|
||||||
|
$owned = if ($Engine -eq 'ibcmd') { $script:IbcmdOwnedKeys } else { $script:V8OwnedKeys }
|
||||||
|
foreach ($tok in $ExtraArgs) {
|
||||||
|
if ($Engine -eq 'ibcmd' -and $tok -notmatch '^-') {
|
||||||
|
Write-Host "Error: '$tok' is a positional token — pass values as --key=value ($paramName cannot extend the ibcmd command)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
foreach ($k in $owned) {
|
||||||
|
if (Test-ArgKeyMatch $tok $k) {
|
||||||
|
$hint = ''
|
||||||
|
if ($Hints -and $Hints.ContainsKey($k)) { $hint = " (use $($Hints[$k]))" }
|
||||||
|
Write-Host "Error: $k is controlled by the skill and cannot be passed via $paramName$hint" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Resolve-ExtraArgs {
|
||||||
|
# Pick the argument list for the selected engine and validate it. An explicitly passed
|
||||||
|
# parameter for the other engine is an error; the same keys coming from .v8-project.json
|
||||||
|
# simply do not apply — a project may describe both engines.
|
||||||
|
param([string]$Engine, [string[]]$V8Extra, [string[]]$IbcmdExtra, [hashtable]$Hints)
|
||||||
|
# powershell.exe -File — how skills are invoked — cannot bind an array parameter:
|
||||||
|
# space-separated values spill into positional ones, a comma-joined list arrives as a
|
||||||
|
# single token. So accept the repo's list convention (comma-separated) and split here;
|
||||||
|
# a native array call keeps working. A value containing a comma is not supported.
|
||||||
|
$V8Extra = @($V8Extra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
|
||||||
|
$IbcmdExtra = @($IbcmdExtra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
|
||||||
|
if ($Engine -eq 'ibcmd' -and $V8Extra.Count -gt 0) {
|
||||||
|
Write-Host "Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd (use -AdditionalIbcmdArguments)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
if ($Engine -ne 'ibcmd' -and $IbcmdExtra.Count -gt 0) {
|
||||||
|
Write-Host "Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 (use -AdditionalV8Arguments)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
if ($Engine -eq 'ibcmd') {
|
||||||
|
$extra = @(Get-ProjectExtraArgs 'ibcmdargs') + @($IbcmdExtra)
|
||||||
|
} else {
|
||||||
|
$extra = @(Get-ProjectExtraArgs 'v8args') + @($V8Extra)
|
||||||
|
}
|
||||||
|
if ($extra.Count -gt 0) { Assert-ExtraArgs $extra $Engine $Hints }
|
||||||
|
# Plain return, no comma trick: the caller re-collects with @(...), and ,@() there
|
||||||
|
# would nest the array — the tokens would then be glued into one argument.
|
||||||
|
return $extra
|
||||||
|
}
|
||||||
|
|
||||||
|
function Format-ArgsForDisplay {
|
||||||
|
# Redact values of secret-prone keys in glued, =-joined and separate forms.
|
||||||
|
# Matching here is a plain prefix (no letter rule): over-masking costs nothing,
|
||||||
|
# a leaked password does.
|
||||||
|
param([string[]]$ArgList, [string]$Engine)
|
||||||
|
$keys = if ($Engine -eq 'ibcmd') { $script:IbcmdSecretKeys } else { $script:V8SecretKeys }
|
||||||
|
$res = @()
|
||||||
|
$maskNext = $false
|
||||||
|
foreach ($tok in $ArgList) {
|
||||||
|
if ($maskNext) { $res += '***'; $maskNext = $false; continue }
|
||||||
|
$hit = $null
|
||||||
|
foreach ($k in $keys) {
|
||||||
|
if ($tok.Length -ge $k.Length -and $tok.Substring(0, $k.Length).Equals($k, [System.StringComparison]::OrdinalIgnoreCase)) { $hit = $k; break }
|
||||||
|
}
|
||||||
|
if (-not $hit) { $res += $tok; continue }
|
||||||
|
if ($tok.Length -eq $hit.Length) { $res += $tok; $maskNext = $true }
|
||||||
|
elseif ($tok[$hit.Length] -eq '=') { $res += ($hit + '=***') }
|
||||||
|
else { $res += ($hit + '***') }
|
||||||
|
}
|
||||||
|
return ,$res
|
||||||
|
}
|
||||||
|
|
||||||
|
function ConvertTo-CleanPath {
|
||||||
|
# Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
|
||||||
|
# surrounding quotes that survived shell parsing, a trailing separator. A quote left
|
||||||
|
# inside afterwards cannot be part of a real path — reject it by name instead of letting
|
||||||
|
# 1C answer with its opaque "Неверные или отсутствующие параметры соединения".
|
||||||
|
param([string]$Value, [string]$ParamName)
|
||||||
|
if (-not $Value) { return $Value }
|
||||||
|
$v = $Value.Trim()
|
||||||
|
if ($v.Length -ge 2 -and $v[0] -eq $v[-1] -and ($v[0] -eq '"' -or $v[0] -eq "'")) {
|
||||||
|
$v = $v.Substring(1, $v.Length - 2).Trim()
|
||||||
|
}
|
||||||
|
if ($v.Length -gt 3 -and ($v[-1] -eq '\' -or $v[-1] -eq '/')) { $v = $v.Substring(0, $v.Length - 1) }
|
||||||
|
if ($v.Contains('"')) {
|
||||||
|
Write-Host "Error: $ParamName contains a quote character: $Value" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
return $v
|
||||||
|
}
|
||||||
|
|
||||||
|
$V8Path = ConvertTo-CleanPath $V8Path '-V8Path'
|
||||||
|
$InfoBasePath = ConvertTo-CleanPath $InfoBasePath '-InfoBasePath'
|
||||||
|
$UseTemplate = ConvertTo-CleanPath $UseTemplate '-UseTemplate'
|
||||||
|
|
||||||
# --- Resolve V8Path ---
|
# --- Resolve V8Path ---
|
||||||
function Find-ProjectV8Path {
|
function Find-ProjectV8Path {
|
||||||
$dir = (Get-Location).Path
|
$dir = (Get-Location).Path
|
||||||
@@ -111,32 +268,75 @@ if (-not (Test-Path $V8Path)) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
|
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
|
||||||
function Invoke-IbcmdProcess {
|
function ConvertFrom-PlatformBytes {
|
||||||
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
|
# ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
|
||||||
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
|
# OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes — guessing
|
||||||
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
|
# one of them outright mangles Cyrillic.
|
||||||
param([string]$Exe, [string[]]$IbArgs)
|
param([byte[]]$Bytes)
|
||||||
|
if (-not $Bytes -or $Bytes.Length -eq 0) { return '' }
|
||||||
|
try {
|
||||||
|
$strict = New-Object System.Text.UTF8Encoding($false, $true)
|
||||||
|
return $strict.GetString($Bytes)
|
||||||
|
} catch {
|
||||||
|
return [System.Text.Encoding]::GetEncoding(866).GetString($Bytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-PlatformProcess {
|
||||||
|
# Run the platform non-interactively and capture its console output. A closed stdin pipe
|
||||||
|
# (EOF) makes an auth prompt fast-fail instead of hanging; capturing keeps the child's
|
||||||
|
# text out of our stream until we print it labelled (and out of the wrong encoding).
|
||||||
|
# Returns @{ Output; ExitCode }.
|
||||||
|
#
|
||||||
|
# Quoting differs by engine, so the caller says which it built:
|
||||||
|
# ibcmd — tokens are bare (--db-path=C:\a b), the whole token gets quoted here;
|
||||||
|
# 1cv8 — -PreQuoted: the caller already put quotes inside the token (File="C:\a b"),
|
||||||
|
# which is where 1C's own parser expects them; quoting again breaks the value.
|
||||||
|
param([string]$Exe, [string[]]$ProcArgs, [switch]$PreQuoted)
|
||||||
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
||||||
$psi.FileName = $Exe
|
$psi.FileName = $Exe
|
||||||
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
|
$psi.Arguments = if ($PreQuoted) {
|
||||||
|
$ProcArgs -join ' '
|
||||||
|
} else {
|
||||||
|
($ProcArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
|
||||||
|
}
|
||||||
$psi.UseShellExecute = $false
|
$psi.UseShellExecute = $false
|
||||||
$psi.CreateNoWindow = $true
|
$psi.CreateNoWindow = $true
|
||||||
$psi.RedirectStandardInput = $true
|
$psi.RedirectStandardInput = $true
|
||||||
$psi.RedirectStandardOutput = $true
|
$psi.RedirectStandardOutput = $true
|
||||||
$psi.RedirectStandardError = $true
|
$psi.RedirectStandardError = $true
|
||||||
try {
|
|
||||||
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
|
|
||||||
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
|
|
||||||
} catch {}
|
|
||||||
$p = [System.Diagnostics.Process]::Start($psi)
|
$p = [System.Diagnostics.Process]::Start($psi)
|
||||||
$p.StandardInput.Close()
|
$p.StandardInput.Close()
|
||||||
$out = $p.StandardOutput.ReadToEnd()
|
# stderr is drained in parallel: reading the streams one after another deadlocks
|
||||||
$err = $p.StandardError.ReadToEnd()
|
# as soon as the other one fills its pipe buffer.
|
||||||
|
$errMs = New-Object System.IO.MemoryStream
|
||||||
|
$errTask = $p.StandardError.BaseStream.CopyToAsync($errMs)
|
||||||
|
$outMs = New-Object System.IO.MemoryStream
|
||||||
|
$p.StandardOutput.BaseStream.CopyTo($outMs)
|
||||||
|
$errTask.Wait()
|
||||||
$p.WaitForExit()
|
$p.WaitForExit()
|
||||||
|
$out = ConvertFrom-PlatformBytes $outMs.ToArray()
|
||||||
|
$err = ConvertFrom-PlatformBytes $errMs.ToArray()
|
||||||
if ($err) { $out += $err }
|
if ($err) { $out += $err }
|
||||||
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
|
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Write-PlatformOutput {
|
||||||
|
# Print what the platform wrote to the console as its own labelled block. Silence stays
|
||||||
|
# silent: in batch mode 1cv8 reports through /Out and prints nothing here.
|
||||||
|
param([string]$Text)
|
||||||
|
if (-not $Text) { return }
|
||||||
|
$t = $Text.TrimEnd()
|
||||||
|
if (-not $t) { return }
|
||||||
|
$limit = 65536
|
||||||
|
if ($t.Length -gt $limit) {
|
||||||
|
$t = "[... обрезано, показаны последние $limit символов ...]`r`n" + $t.Substring($t.Length - $limit)
|
||||||
|
}
|
||||||
|
Write-Host "--- Вывод платформы ---"
|
||||||
|
Write-Host $t
|
||||||
|
Write-Host "--- End ---"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function Test-FileIbCreated {
|
function Test-FileIbCreated {
|
||||||
# File-infobase postcondition: the platform must have produced a non-empty 1Cv8.1CD.
|
# File-infobase postcondition: the platform must have produced a non-empty 1Cv8.1CD.
|
||||||
@@ -148,6 +348,10 @@ function Test-FileIbCreated {
|
|||||||
|
|
||||||
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
||||||
|
|
||||||
|
# --- Resolve additional arguments for the selected engine ---
|
||||||
|
$argHints = @{ '/F' = '-InfoBasePath'; '/S' = '-InfoBaseServer + -InfoBaseRef'; '/UseTemplate' = '-UseTemplate'; '/AddToList' = '-AddToList'; '--db-path' = '-InfoBasePath'; '--load' = '-UseTemplate'; '--restore' = '-UseTemplate' }
|
||||||
|
$extraArgs = @(Resolve-ExtraArgs $engine $AdditionalV8Arguments $AdditionalIbcmdArguments $argHints)
|
||||||
|
|
||||||
# --- Validate connection ---
|
# --- Validate connection ---
|
||||||
if ($engine -eq "ibcmd") {
|
if ($engine -eq "ibcmd") {
|
||||||
if (-not $InfoBasePath) {
|
if (-not $InfoBasePath) {
|
||||||
@@ -181,8 +385,9 @@ try {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
$arguments += "--data=$tempDir"
|
$arguments += "--data=$tempDir"
|
||||||
Write-Host "Running: ibcmd $($arguments -join ' ')"
|
$arguments += $extraArgs
|
||||||
$__ib = Invoke-IbcmdProcess $V8Path $arguments
|
Write-Host "Running: ibcmd $((Format-ArgsForDisplay $arguments $engine) -join ' ')"
|
||||||
|
$__ib = Invoke-PlatformProcess $V8Path $arguments
|
||||||
$output = $__ib.Output
|
$output = $__ib.Output
|
||||||
$exitCode = $__ib.ExitCode
|
$exitCode = $__ib.ExitCode
|
||||||
$ibMissing = ($exitCode -eq 0) -and -not (Test-FileIbCreated $InfoBasePath)
|
$ibMissing = ($exitCode -eq 0) -and -not (Test-FileIbCreated $InfoBasePath)
|
||||||
@@ -194,7 +399,7 @@ try {
|
|||||||
} else {
|
} else {
|
||||||
Write-Host "Error creating information base (code: $exitCode)" -ForegroundColor Red
|
Write-Host "Error creating information base (code: $exitCode)" -ForegroundColor Red
|
||||||
}
|
}
|
||||||
if ($output) { Write-Host ($output | Out-String) }
|
Write-PlatformOutput $output
|
||||||
exit $exitCode
|
exit $exitCode
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,6 +407,8 @@ try {
|
|||||||
# --- Build arguments ---
|
# --- Build arguments ---
|
||||||
$arguments = @("CREATEINFOBASE")
|
$arguments = @("CREATEINFOBASE")
|
||||||
|
|
||||||
|
# Quotes go INSIDE the token (File="path"): 1C's own parser wants them there, quoting
|
||||||
|
# the whole token instead breaks a path with spaces. Hence -PreQuoted on the launch.
|
||||||
if ($InfoBaseServer -and $InfoBaseRef) {
|
if ($InfoBaseServer -and $InfoBaseRef) {
|
||||||
$arguments += "Srvr=`"$InfoBaseServer`";Ref=`"$InfoBaseRef`""
|
$arguments += "Srvr=`"$InfoBaseServer`";Ref=`"$InfoBaseRef`""
|
||||||
} else {
|
} else {
|
||||||
@@ -226,11 +433,12 @@ try {
|
|||||||
$outFile = Join-Path $tempDir "create_log.txt"
|
$outFile = Join-Path $tempDir "create_log.txt"
|
||||||
$arguments += "/Out", "`"$outFile`""
|
$arguments += "/Out", "`"$outFile`""
|
||||||
$arguments += "/DisableStartupDialogs"
|
$arguments += "/DisableStartupDialogs"
|
||||||
|
$arguments += $extraArgs
|
||||||
|
|
||||||
# --- Execute ---
|
# --- Execute ---
|
||||||
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
|
Write-Host "Running: 1cv8.exe $((Format-ArgsForDisplay $arguments $engine) -join ' ')"
|
||||||
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
|
$__v8 = Invoke-PlatformProcess $V8Path $arguments -PreQuoted
|
||||||
$exitCode = $process.ExitCode
|
$exitCode = $__v8.ExitCode
|
||||||
|
|
||||||
# --- Result ---
|
# --- Result ---
|
||||||
# Postcondition (file infobase only): exit 0 without a non-empty 1Cv8.1CD is a false success.
|
# Postcondition (file infobase only): exit 0 without a non-empty 1Cv8.1CD is a false success.
|
||||||
@@ -257,6 +465,7 @@ try {
|
|||||||
Write-Host "--- End ---"
|
Write-Host "--- End ---"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Write-PlatformOutput $__v8.Output
|
||||||
|
|
||||||
exit $exitCode
|
exit $exitCode
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# db-create v1.7 — Create 1C information base
|
# db-create v1.11 — Create 1C information base
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -14,6 +14,28 @@ import subprocess
|
|||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
|
# Регистронезависимый ввод — паритет с 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)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def _find_project_v8path():
|
def _find_project_v8path():
|
||||||
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
||||||
@@ -36,6 +58,163 @@ def _find_project_v8path():
|
|||||||
d = parent
|
d = parent
|
||||||
|
|
||||||
|
|
||||||
|
# --- Additional platform arguments ---
|
||||||
|
V8_OWNED_KEYS = [
|
||||||
|
"DESIGNER", "ENTERPRISE", "CREATEINFOBASE", "CONFIG",
|
||||||
|
"/F", "/S", "/N", "/P", "/Out", "/DisableStartupDialogs",
|
||||||
|
"/UseTemplate", "/AddToList", "/Execute", "/C", "/URL", "/UC",
|
||||||
|
"/DumpIB", "/RestoreIB", "/DumpCfg", "/LoadCfg",
|
||||||
|
"/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg",
|
||||||
|
"/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles",
|
||||||
|
]
|
||||||
|
IBCMD_OWNED_KEYS = [
|
||||||
|
"--db-path", "--data", "--out", "--file", "--load", "--restore",
|
||||||
|
"--import", "--export", "--apply", "--force", "--create-database",
|
||||||
|
"--user", "--password",
|
||||||
|
]
|
||||||
|
V8_SECRET_KEYS = ["/P", "/UC", "/WSP", "/AWSP"]
|
||||||
|
IBCMD_SECRET_KEYS = ["--password", "--token", "--db-pwd"]
|
||||||
|
|
||||||
|
|
||||||
|
def arg_key_match(token, key):
|
||||||
|
"""Token matches a key when it equals it, or starts with it and the next character
|
||||||
|
is not a letter — catches glued /N"user" and --password=x, while keeping
|
||||||
|
/ClearCache distinct from /C."""
|
||||||
|
if len(token) < len(key):
|
||||||
|
return False
|
||||||
|
if token[: len(key)].lower() != key.lower():
|
||||||
|
return False
|
||||||
|
if len(token) == len(key):
|
||||||
|
return True
|
||||||
|
return not token[len(key)].isalpha()
|
||||||
|
|
||||||
|
|
||||||
|
def project_extra_args(name):
|
||||||
|
"""v8args / ibcmdargs from .v8-project.json — same upward walk as v8path."""
|
||||||
|
d = os.getcwd()
|
||||||
|
while True:
|
||||||
|
pf = os.path.join(d, ".v8-project.json")
|
||||||
|
if os.path.isfile(pf):
|
||||||
|
try:
|
||||||
|
with open(pf, encoding="utf-8-sig") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
v = data.get(name)
|
||||||
|
if v:
|
||||||
|
return [str(x) for x in v]
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return []
|
||||||
|
parent = os.path.dirname(d)
|
||||||
|
if parent == d:
|
||||||
|
return []
|
||||||
|
d = parent
|
||||||
|
|
||||||
|
|
||||||
|
def assert_extra_args(extra, engine, hints):
|
||||||
|
"""The platform accepts only one batch operation, and a duplicate connection or
|
||||||
|
output key fails with an opaque 1C error — reject what the skill owns itself."""
|
||||||
|
param = "-AdditionalIbcmdArguments" if engine == "ibcmd" else "-AdditionalV8Arguments"
|
||||||
|
owned = IBCMD_OWNED_KEYS if engine == "ibcmd" else V8_OWNED_KEYS
|
||||||
|
for tok in extra:
|
||||||
|
if engine == "ibcmd" and not tok.startswith("-"):
|
||||||
|
print(
|
||||||
|
f"Error: '{tok}' is a positional token — pass values as --key=value "
|
||||||
|
f"({param} cannot extend the ibcmd command)",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
for k in owned:
|
||||||
|
if arg_key_match(tok, k):
|
||||||
|
hint = f" (use {hints[k]})" if hints and k in hints else ""
|
||||||
|
print(
|
||||||
|
f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def format_args_for_display(arglist, engine):
|
||||||
|
"""Redact values of secret-prone keys in glued, =-joined and separate forms.
|
||||||
|
Matching here is a plain prefix (no letter rule): over-masking costs nothing,
|
||||||
|
a leaked password does."""
|
||||||
|
keys = IBCMD_SECRET_KEYS if engine == "ibcmd" else V8_SECRET_KEYS
|
||||||
|
res = []
|
||||||
|
mask_next = False
|
||||||
|
for tok in arglist:
|
||||||
|
if mask_next:
|
||||||
|
res.append("***")
|
||||||
|
mask_next = False
|
||||||
|
continue
|
||||||
|
hit = None
|
||||||
|
for k in keys:
|
||||||
|
if tok[: len(k)].lower() == k.lower():
|
||||||
|
hit = k
|
||||||
|
break
|
||||||
|
if hit is None:
|
||||||
|
res.append(tok)
|
||||||
|
elif len(tok) == len(hit):
|
||||||
|
res.append(tok)
|
||||||
|
mask_next = True
|
||||||
|
elif tok[len(hit)] == "=":
|
||||||
|
res.append(hit + "=***")
|
||||||
|
else:
|
||||||
|
res.append(hit + "***")
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
def extract_extra_args(argv, known_opts):
|
||||||
|
"""argparse refuses values that start with '-' (every ibcmd key does), so pull the two
|
||||||
|
escape-hatch lists out of argv by hand: after the flag, take everything up to the next
|
||||||
|
declared skill option. Returns (remaining_argv, v8_extra, ibcmd_extra)."""
|
||||||
|
rest, v8, ibcmd = [], [], []
|
||||||
|
i = 0
|
||||||
|
while i < len(argv):
|
||||||
|
low = argv[i].lower()
|
||||||
|
if low in ("-additionalv8arguments", "-additionalibcmdarguments"):
|
||||||
|
target = v8 if low == "-additionalv8arguments" else ibcmd
|
||||||
|
i += 1
|
||||||
|
while i < len(argv) and argv[i].lower() not in known_opts:
|
||||||
|
target.append(argv[i])
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
rest.append(argv[i])
|
||||||
|
i += 1
|
||||||
|
return rest, v8, ibcmd
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints):
|
||||||
|
"""Pick the argument list for the selected engine and validate it. An explicitly
|
||||||
|
passed parameter for the other engine is an error; the same keys coming from
|
||||||
|
.v8-project.json simply do not apply — a project may describe both engines.
|
||||||
|
|
||||||
|
Comma-separated elements are split apart: PowerShell's -File cannot bind an array,
|
||||||
|
so that form is the documented one and both ports must accept it. A value containing
|
||||||
|
a comma is not supported."""
|
||||||
|
v8_extra = [p for tok in v8_extra for p in str(tok).split(",") if p]
|
||||||
|
ibcmd_extra = [p for tok in ibcmd_extra for p in str(tok).split(",") if p]
|
||||||
|
if engine == "ibcmd" and v8_extra:
|
||||||
|
print(
|
||||||
|
"Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd "
|
||||||
|
"(use -AdditionalIbcmdArguments)",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
if engine != "ibcmd" and ibcmd_extra:
|
||||||
|
print(
|
||||||
|
"Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 "
|
||||||
|
"(use -AdditionalV8Arguments)",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
if engine == "ibcmd":
|
||||||
|
extra = project_extra_args("ibcmdargs") + list(ibcmd_extra)
|
||||||
|
else:
|
||||||
|
extra = project_extra_args("v8args") + list(v8_extra)
|
||||||
|
if extra:
|
||||||
|
assert_extra_args(extra, engine, hints)
|
||||||
|
return extra
|
||||||
|
|
||||||
|
|
||||||
def _version_dir(p):
|
def _version_dir(p):
|
||||||
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
|
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
|
||||||
parent = os.path.dirname(p)
|
parent = os.path.dirname(p)
|
||||||
@@ -93,6 +272,18 @@ IBCMD_NOUSER_HINT = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def decode_platform_bytes(data):
|
||||||
|
"""ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
|
||||||
|
OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes — the locale
|
||||||
|
code page (what text=True uses) mangles both."""
|
||||||
|
if not data:
|
||||||
|
return ""
|
||||||
|
try:
|
||||||
|
return data.decode("utf-8")
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
return data.decode("cp866", errors="replace")
|
||||||
|
|
||||||
|
|
||||||
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
||||||
"""Run an ibcmd command non-interactively.
|
"""Run an ibcmd command non-interactively.
|
||||||
|
|
||||||
@@ -103,7 +294,67 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
|||||||
if warn_no_user and os.name == "nt" and not has_username:
|
if warn_no_user and os.name == "nt" and not has_username:
|
||||||
sys.stderr.write(IBCMD_NOUSER_HINT)
|
sys.stderr.write(IBCMD_NOUSER_HINT)
|
||||||
sys.stderr.flush()
|
sys.stderr.flush()
|
||||||
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
|
r = subprocess.run(cmd, input=b"", capture_output=True)
|
||||||
|
r.stdout = decode_platform_bytes(r.stdout)
|
||||||
|
r.stderr = decode_platform_bytes(r.stderr)
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
|
def clean_path(value, param=""):
|
||||||
|
"""Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
|
||||||
|
surrounding quotes that survived shell parsing, a trailing separator. A quote left
|
||||||
|
inside afterwards cannot be part of a real path — reject it by name instead of letting
|
||||||
|
1C answer with its opaque "Неверные или отсутствующие параметры соединения"."""
|
||||||
|
if not value:
|
||||||
|
return value
|
||||||
|
v = value.strip()
|
||||||
|
if len(v) >= 2 and v[0] == v[-1] and v[0] in "\"'":
|
||||||
|
v = v[1:-1].strip()
|
||||||
|
if len(v) > 3 and v[-1] in "\\/":
|
||||||
|
v = v[:-1]
|
||||||
|
if '"' in v:
|
||||||
|
print(f"Error: {param or 'path'} contains a quote character: {value}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
return v
|
||||||
|
|
||||||
|
|
||||||
|
def quote_if_needed(token):
|
||||||
|
"""Extra arguments come from the caller unquoted; the 1cv8 command line is joined
|
||||||
|
verbatim, so a token with a space needs quotes of its own."""
|
||||||
|
if token and (" " in token or "\t" in token) and '"' not in token:
|
||||||
|
return f'"{token}"'
|
||||||
|
return token
|
||||||
|
|
||||||
|
|
||||||
|
def run_v8(v8path, arguments):
|
||||||
|
"""Run 1cv8 in batch mode and capture its console output.
|
||||||
|
|
||||||
|
The arguments carry their own quotes inside the value (File="C:\\a b") — that is where
|
||||||
|
1C's parser expects them, on Windows and on *nix alike. Windows list2cmdline would
|
||||||
|
escape those quotes, so there the command line is handed over ready-made.
|
||||||
|
"""
|
||||||
|
if os.name == "nt":
|
||||||
|
cmd = '"' + v8path + '" ' + " ".join(arguments)
|
||||||
|
else:
|
||||||
|
cmd = [v8path] + arguments
|
||||||
|
r = subprocess.run(cmd, input=b"", capture_output=True)
|
||||||
|
r.stdout = decode_platform_bytes(r.stdout)
|
||||||
|
r.stderr = decode_platform_bytes(r.stderr)
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
|
def print_platform_output(result):
|
||||||
|
"""Print what the platform wrote to the console as its own labelled block. Silence stays
|
||||||
|
silent: in batch mode 1cv8 reports through /Out and prints nothing here."""
|
||||||
|
text = ((result.stdout or "") + (result.stderr or "")).rstrip()
|
||||||
|
if not text:
|
||||||
|
return
|
||||||
|
limit = 65536
|
||||||
|
if len(text) > limit:
|
||||||
|
text = f"[... обрезано, показаны последние {limit} символов ...]\n" + text[-limit:]
|
||||||
|
print("--- Вывод платформы ---")
|
||||||
|
print(text)
|
||||||
|
print("--- End ---")
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
@@ -120,11 +371,33 @@ def main():
|
|||||||
parser.add_argument("-UseTemplate", default="")
|
parser.add_argument("-UseTemplate", default="")
|
||||||
parser.add_argument("-AddToList", action="store_true")
|
parser.add_argument("-AddToList", action="store_true")
|
||||||
parser.add_argument("-ListName", default="")
|
parser.add_argument("-ListName", default="")
|
||||||
args = parser.parse_args()
|
parser.add_argument("-AdditionalV8Arguments", nargs="*", default=[],
|
||||||
|
help="Extra 1cv8 arguments, e.g. /UseHwLicenses+")
|
||||||
|
parser.add_argument("-AdditionalIbcmdArguments", nargs="*", default=[],
|
||||||
|
help="Extra ibcmd arguments in --key=value form")
|
||||||
|
known_opts = {s.lower() for a in parser._actions for s in a.option_strings}
|
||||||
|
argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts)
|
||||||
|
args = ci_parse_args(parser, argv)
|
||||||
|
|
||||||
|
args.V8Path = clean_path(args.V8Path, "-V8Path")
|
||||||
|
args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath")
|
||||||
|
args.UseTemplate = clean_path(args.UseTemplate, "-UseTemplate")
|
||||||
|
|
||||||
v8path = resolve_v8path(args.V8Path)
|
v8path = resolve_v8path(args.V8Path)
|
||||||
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
|
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
|
||||||
|
|
||||||
|
# --- Resolve additional arguments for the selected engine ---
|
||||||
|
arg_hints = {
|
||||||
|
"/F": "-InfoBasePath",
|
||||||
|
"/S": "-InfoBaseServer + -InfoBaseRef",
|
||||||
|
"/UseTemplate": "-UseTemplate",
|
||||||
|
"/AddToList": "-AddToList",
|
||||||
|
"--db-path": "-InfoBasePath",
|
||||||
|
"--load": "-UseTemplate",
|
||||||
|
"--restore": "-UseTemplate",
|
||||||
|
}
|
||||||
|
extra_args = resolve_extra_args(engine, v8_extra, ibcmd_extra, arg_hints)
|
||||||
|
|
||||||
# --- Validate connection ---
|
# --- Validate connection ---
|
||||||
if engine == "ibcmd":
|
if engine == "ibcmd":
|
||||||
if not args.InfoBasePath:
|
if not args.InfoBasePath:
|
||||||
@@ -150,7 +423,8 @@ def main():
|
|||||||
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
|
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
|
||||||
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
|
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
|
||||||
arguments.append(f"--data={ib_data}")
|
arguments.append(f"--data={ib_data}")
|
||||||
print(f"Running: ibcmd {' '.join(arguments)}")
|
arguments.extend(quote_if_needed(a) for a in extra_args)
|
||||||
|
print(f"Running: ibcmd {' '.join(format_args_for_display(arguments, engine))}")
|
||||||
result = run_ibcmd([v8path] + arguments, warn_no_user=False)
|
result = run_ibcmd([v8path] + arguments, warn_no_user=False)
|
||||||
exit_code = result.returncode
|
exit_code = result.returncode
|
||||||
ib_missing = exit_code == 0 and not file_ib_created(args.InfoBasePath)
|
ib_missing = exit_code == 0 and not file_ib_created(args.InfoBasePath)
|
||||||
@@ -166,10 +440,7 @@ def main():
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
print(f"Error creating information base (code: {exit_code})", file=sys.stderr)
|
print(f"Error creating information base (code: {exit_code})", file=sys.stderr)
|
||||||
if result.stdout:
|
print_platform_output(result)
|
||||||
print(result.stdout)
|
|
||||||
if result.stderr:
|
|
||||||
print(result.stderr, file=sys.stderr)
|
|
||||||
sys.exit(exit_code)
|
sys.exit(exit_code)
|
||||||
|
|
||||||
# --- Temp dir ---
|
# --- Temp dir ---
|
||||||
@@ -180,36 +451,33 @@ def main():
|
|||||||
# --- Build arguments ---
|
# --- Build arguments ---
|
||||||
arguments = ["CREATEINFOBASE"]
|
arguments = ["CREATEINFOBASE"]
|
||||||
|
|
||||||
|
# Quotes go INSIDE the token (File="path"): that is where 1C's parser expects them.
|
||||||
|
# Quoting the whole token instead breaks a path with spaces — on both OSes.
|
||||||
if args.InfoBaseServer and args.InfoBaseRef:
|
if args.InfoBaseServer and args.InfoBaseRef:
|
||||||
# No embedded quotes: subprocess quotes the whole token; 1C's argv parser
|
arguments.append(f'Srvr="{args.InfoBaseServer}";Ref="{args.InfoBaseRef}"')
|
||||||
# strips outer quotes. Inner quotes get escaped by list2cmdline and break parsing.
|
|
||||||
arguments.append(f'Srvr={args.InfoBaseServer};Ref={args.InfoBaseRef}')
|
|
||||||
else:
|
else:
|
||||||
arguments.append(f'File={args.InfoBasePath}')
|
arguments.append(f'File="{args.InfoBasePath}"')
|
||||||
|
|
||||||
# --- Template ---
|
# --- Template ---
|
||||||
if args.UseTemplate:
|
if args.UseTemplate:
|
||||||
arguments.extend(["/UseTemplate", args.UseTemplate])
|
arguments.extend(["/UseTemplate", f'"{args.UseTemplate}"'])
|
||||||
|
|
||||||
# --- Add to list ---
|
# --- Add to list ---
|
||||||
if args.AddToList:
|
if args.AddToList:
|
||||||
if args.ListName:
|
if args.ListName:
|
||||||
arguments.extend(["/AddToList", args.ListName])
|
arguments.extend(["/AddToList", f'"{args.ListName}"'])
|
||||||
else:
|
else:
|
||||||
arguments.append("/AddToList")
|
arguments.append("/AddToList")
|
||||||
|
|
||||||
# --- Output ---
|
# --- Output ---
|
||||||
out_file = os.path.join(temp_dir, "create_log.txt")
|
out_file = os.path.join(temp_dir, "create_log.txt")
|
||||||
arguments.extend(["/Out", out_file])
|
arguments.extend(["/Out", f'"{out_file}"'])
|
||||||
arguments.append("/DisableStartupDialogs")
|
arguments.append("/DisableStartupDialogs")
|
||||||
|
arguments.extend(quote_if_needed(a) for a in extra_args)
|
||||||
|
|
||||||
# --- Execute ---
|
# --- Execute ---
|
||||||
print(f"Running: 1cv8.exe {' '.join(arguments)}")
|
print(f"Running: 1cv8.exe {' '.join(format_args_for_display(arguments, engine))}")
|
||||||
result = subprocess.run(
|
result = run_v8(v8path, arguments)
|
||||||
[v8path] + arguments,
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
)
|
|
||||||
exit_code = result.returncode
|
exit_code = result.returncode
|
||||||
|
|
||||||
# --- Result ---
|
# --- Result ---
|
||||||
@@ -243,6 +511,7 @@ def main():
|
|||||||
print("--- End ---")
|
print("--- End ---")
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
print_platform_output(result)
|
||||||
|
|
||||||
sys.exit(exit_code)
|
sys.exit(exit_code)
|
||||||
|
|
||||||
|
|||||||
@@ -51,6 +51,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-cf.ps1" <п
|
|||||||
| `-OutputFile <путь>` | да | Путь к выходному CF-файлу |
|
| `-OutputFile <путь>` | да | Путь к выходному CF-файлу |
|
||||||
| `-Extension <имя>` | нет | Выгрузить расширение |
|
| `-Extension <имя>` | нет | Выгрузить расширение |
|
||||||
| `-AllExtensions` | нет | Выгрузить все расширения |
|
| `-AllExtensions` | нет | Выгрузить все расширения |
|
||||||
|
| `-AdditionalV8Arguments <список>` | нет | Доп. аргументы запуска `1cv8.exe` через запятую, напр. `/UseHwLicenses+` |
|
||||||
|
| `-AdditionalIbcmdArguments <список>` | нет | Доп. аргументы `ibcmd` через запятую, в форме `--ключ=значение` |
|
||||||
|
|
||||||
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
|
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# db-dump-cf v1.9 — Dump 1C configuration to CF file
|
# db-dump-cf v1.13 — Dump 1C configuration to CF file
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||||
<#
|
<#
|
||||||
@@ -36,6 +36,12 @@
|
|||||||
.PARAMETER AllExtensions
|
.PARAMETER AllExtensions
|
||||||
Выгрузить все расширения
|
Выгрузить все расширения
|
||||||
|
|
||||||
|
.PARAMETER AdditionalV8Arguments
|
||||||
|
Дополнительные аргументы запуска 1cv8.exe (например /UseHwLicenses+)
|
||||||
|
|
||||||
|
.PARAMETER AdditionalIbcmdArguments
|
||||||
|
Дополнительные аргументы запуска ibcmd (форма --ключ=значение)
|
||||||
|
|
||||||
.EXAMPLE
|
.EXAMPLE
|
||||||
.\db-dump-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -OutputFile "config.cf"
|
.\db-dump-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -OutputFile "config.cf"
|
||||||
|
|
||||||
@@ -70,7 +76,13 @@ param(
|
|||||||
[string]$Extension,
|
[string]$Extension,
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
[Parameter(Mandatory=$false)]
|
||||||
[switch]$AllExtensions
|
[switch]$AllExtensions,
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[string[]]$AdditionalV8Arguments = @(),
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[string[]]$AdditionalIbcmdArguments = @()
|
||||||
)
|
)
|
||||||
|
|
||||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
@@ -83,6 +95,164 @@ function Protect-Secrets {
|
|||||||
return $Text
|
return $Text
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- Additional platform arguments ---
|
||||||
|
$script:V8OwnedKeys = @(
|
||||||
|
'DESIGNER', 'ENTERPRISE', 'CREATEINFOBASE', 'CONFIG',
|
||||||
|
'/F', '/S', '/N', '/P', '/Out', '/DisableStartupDialogs',
|
||||||
|
'/UseTemplate', '/AddToList', '/Execute', '/C', '/URL', '/UC',
|
||||||
|
'/DumpIB', '/RestoreIB', '/DumpCfg', '/LoadCfg',
|
||||||
|
'/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg',
|
||||||
|
'/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles'
|
||||||
|
)
|
||||||
|
$script:IbcmdOwnedKeys = @(
|
||||||
|
'--db-path', '--data', '--out', '--file', '--load', '--restore',
|
||||||
|
'--import', '--export', '--apply', '--force', '--create-database',
|
||||||
|
'--user', '--password'
|
||||||
|
)
|
||||||
|
$script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP')
|
||||||
|
$script:IbcmdSecretKeys = @('--password', '--token', '--db-pwd')
|
||||||
|
|
||||||
|
function Test-ArgKeyMatch {
|
||||||
|
# A token matches a key when it equals the key, or starts with it and the next
|
||||||
|
# character is not a letter — catches glued /N"user" and --password=x, while
|
||||||
|
# keeping /ClearCache distinct from /C.
|
||||||
|
param([string]$Token, [string]$Key)
|
||||||
|
if ($Token.Length -lt $Key.Length) { return $false }
|
||||||
|
if (-not $Token.Substring(0, $Key.Length).Equals($Key, [System.StringComparison]::OrdinalIgnoreCase)) { return $false }
|
||||||
|
if ($Token.Length -eq $Key.Length) { return $true }
|
||||||
|
return -not [char]::IsLetter($Token[$Key.Length])
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-ProjectExtraArgs {
|
||||||
|
# v8args / ibcmdargs from .v8-project.json — same upward walk as v8path.
|
||||||
|
param([string]$Name)
|
||||||
|
$dir = (Get-Location).Path
|
||||||
|
while ($dir) {
|
||||||
|
$pf = Join-Path $dir ".v8-project.json"
|
||||||
|
if (Test-Path $pf) {
|
||||||
|
try {
|
||||||
|
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||||
|
if ($j.$Name) { return @($j.$Name | ForEach-Object { [string]$_ }) }
|
||||||
|
} catch {}
|
||||||
|
return @()
|
||||||
|
}
|
||||||
|
$parent = Split-Path $dir -Parent
|
||||||
|
if (-not $parent -or $parent -eq $dir) { break }
|
||||||
|
$dir = $parent
|
||||||
|
}
|
||||||
|
return @()
|
||||||
|
}
|
||||||
|
|
||||||
|
function Assert-ExtraArgs {
|
||||||
|
# The platform accepts only one batch operation, and a duplicate connection or
|
||||||
|
# output key fails with an opaque 1C error — reject what the skill owns itself.
|
||||||
|
param([string[]]$ExtraArgs, [string]$Engine, [hashtable]$Hints)
|
||||||
|
$paramName = if ($Engine -eq 'ibcmd') { '-AdditionalIbcmdArguments' } else { '-AdditionalV8Arguments' }
|
||||||
|
$owned = if ($Engine -eq 'ibcmd') { $script:IbcmdOwnedKeys } else { $script:V8OwnedKeys }
|
||||||
|
foreach ($tok in $ExtraArgs) {
|
||||||
|
if ($Engine -eq 'ibcmd' -and $tok -notmatch '^-') {
|
||||||
|
Write-Host "Error: '$tok' is a positional token — pass values as --key=value ($paramName cannot extend the ibcmd command)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
foreach ($k in $owned) {
|
||||||
|
if (Test-ArgKeyMatch $tok $k) {
|
||||||
|
$hint = ''
|
||||||
|
if ($Hints -and $Hints.ContainsKey($k)) { $hint = " (use $($Hints[$k]))" }
|
||||||
|
Write-Host "Error: $k is controlled by the skill and cannot be passed via $paramName$hint" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Resolve-ExtraArgs {
|
||||||
|
# Pick the argument list for the selected engine and validate it. An explicitly passed
|
||||||
|
# parameter for the other engine is an error; the same keys coming from .v8-project.json
|
||||||
|
# simply do not apply — a project may describe both engines.
|
||||||
|
param([string]$Engine, [string[]]$V8Extra, [string[]]$IbcmdExtra, [hashtable]$Hints)
|
||||||
|
# powershell.exe -File — how skills are invoked — cannot bind an array parameter:
|
||||||
|
# space-separated values spill into positional ones, a comma-joined list arrives as a
|
||||||
|
# single token. So accept the repo's list convention (comma-separated) and split here;
|
||||||
|
# a native array call keeps working. A value containing a comma is not supported.
|
||||||
|
$V8Extra = @($V8Extra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
|
||||||
|
$IbcmdExtra = @($IbcmdExtra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
|
||||||
|
if ($Engine -eq 'ibcmd' -and $V8Extra.Count -gt 0) {
|
||||||
|
Write-Host "Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd (use -AdditionalIbcmdArguments)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
if ($Engine -ne 'ibcmd' -and $IbcmdExtra.Count -gt 0) {
|
||||||
|
Write-Host "Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 (use -AdditionalV8Arguments)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
if ($Engine -eq 'ibcmd') {
|
||||||
|
$extra = @(Get-ProjectExtraArgs 'ibcmdargs') + @($IbcmdExtra)
|
||||||
|
} else {
|
||||||
|
$extra = @(Get-ProjectExtraArgs 'v8args') + @($V8Extra)
|
||||||
|
}
|
||||||
|
if ($extra.Count -gt 0) { Assert-ExtraArgs $extra $Engine $Hints }
|
||||||
|
# Plain return, no comma trick: the caller re-collects with @(...), and ,@() there
|
||||||
|
# would nest the array — the tokens would then be glued into one argument.
|
||||||
|
return $extra
|
||||||
|
}
|
||||||
|
|
||||||
|
function Format-ArgsForDisplay {
|
||||||
|
# Redact values of secret-prone keys in glued, =-joined and separate forms.
|
||||||
|
# Matching here is a plain prefix (no letter rule): over-masking costs nothing,
|
||||||
|
# a leaked password does.
|
||||||
|
param([string[]]$ArgList, [string]$Engine)
|
||||||
|
$keys = if ($Engine -eq 'ibcmd') { $script:IbcmdSecretKeys } else { $script:V8SecretKeys }
|
||||||
|
$res = @()
|
||||||
|
$maskNext = $false
|
||||||
|
foreach ($tok in $ArgList) {
|
||||||
|
if ($maskNext) { $res += '***'; $maskNext = $false; continue }
|
||||||
|
$hit = $null
|
||||||
|
foreach ($k in $keys) {
|
||||||
|
if ($tok.Length -ge $k.Length -and $tok.Substring(0, $k.Length).Equals($k, [System.StringComparison]::OrdinalIgnoreCase)) { $hit = $k; break }
|
||||||
|
}
|
||||||
|
if (-not $hit) { $res += $tok; continue }
|
||||||
|
if ($tok.Length -eq $hit.Length) { $res += $tok; $maskNext = $true }
|
||||||
|
elseif ($tok[$hit.Length] -eq '=') { $res += ($hit + '=***') }
|
||||||
|
else { $res += ($hit + '***') }
|
||||||
|
}
|
||||||
|
return ,$res
|
||||||
|
}
|
||||||
|
|
||||||
|
function ConvertTo-CleanPath {
|
||||||
|
# Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
|
||||||
|
# surrounding quotes that survived shell parsing, a trailing separator. A quote left
|
||||||
|
# inside afterwards cannot be part of a real path — reject it by name instead of letting
|
||||||
|
# 1C answer with its opaque "Неверные или отсутствующие параметры соединения".
|
||||||
|
param([string]$Value, [string]$ParamName)
|
||||||
|
if (-not $Value) { return $Value }
|
||||||
|
$v = $Value.Trim()
|
||||||
|
if ($v.Length -ge 2 -and $v[0] -eq $v[-1] -and ($v[0] -eq '"' -or $v[0] -eq "'")) {
|
||||||
|
$v = $v.Substring(1, $v.Length - 2).Trim()
|
||||||
|
}
|
||||||
|
if ($v.Length -gt 3 -and ($v[-1] -eq '\' -or $v[-1] -eq '/')) { $v = $v.Substring(0, $v.Length - 1) }
|
||||||
|
if ($v.Contains('"')) {
|
||||||
|
Write-Host "Error: $ParamName contains a quote character: $Value" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
return $v
|
||||||
|
}
|
||||||
|
|
||||||
|
$V8Path = ConvertTo-CleanPath $V8Path '-V8Path'
|
||||||
|
$InfoBasePath = ConvertTo-CleanPath $InfoBasePath '-InfoBasePath'
|
||||||
|
$OutputFile = ConvertTo-CleanPath $OutputFile '-OutputFile'
|
||||||
|
|
||||||
|
function Assert-InfoBaseExists {
|
||||||
|
# These skills work on a ready infobase. Saying so up front beats the platform's
|
||||||
|
# "Неверные или отсутствующие параметры соединения" after a launch.
|
||||||
|
param([string]$Path)
|
||||||
|
if (-not $Path) { return }
|
||||||
|
if (-not (Test-Path (Join-Path $Path "1Cv8.1CD"))) {
|
||||||
|
Write-Host "Error: information base not found at $Path (no 1Cv8.1CD)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert-InfoBaseExists $InfoBasePath
|
||||||
|
|
||||||
# --- Resolve V8Path ---
|
# --- Resolve V8Path ---
|
||||||
function Find-ProjectV8Path {
|
function Find-ProjectV8Path {
|
||||||
$dir = (Get-Location).Path
|
$dir = (Get-Location).Path
|
||||||
@@ -127,32 +297,75 @@ if (-not (Test-Path $V8Path)) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
|
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
|
||||||
function Invoke-IbcmdProcess {
|
function ConvertFrom-PlatformBytes {
|
||||||
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
|
# ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
|
||||||
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
|
# OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes — guessing
|
||||||
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
|
# one of them outright mangles Cyrillic.
|
||||||
param([string]$Exe, [string[]]$IbArgs)
|
param([byte[]]$Bytes)
|
||||||
|
if (-not $Bytes -or $Bytes.Length -eq 0) { return '' }
|
||||||
|
try {
|
||||||
|
$strict = New-Object System.Text.UTF8Encoding($false, $true)
|
||||||
|
return $strict.GetString($Bytes)
|
||||||
|
} catch {
|
||||||
|
return [System.Text.Encoding]::GetEncoding(866).GetString($Bytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-PlatformProcess {
|
||||||
|
# Run the platform non-interactively and capture its console output. A closed stdin pipe
|
||||||
|
# (EOF) makes an auth prompt fast-fail instead of hanging; capturing keeps the child's
|
||||||
|
# text out of our stream until we print it labelled (and out of the wrong encoding).
|
||||||
|
# Returns @{ Output; ExitCode }.
|
||||||
|
#
|
||||||
|
# Quoting differs by engine, so the caller says which it built:
|
||||||
|
# ibcmd — tokens are bare (--db-path=C:\a b), the whole token gets quoted here;
|
||||||
|
# 1cv8 — -PreQuoted: the caller already put quotes inside the token (File="C:\a b"),
|
||||||
|
# which is where 1C's own parser expects them; quoting again breaks the value.
|
||||||
|
param([string]$Exe, [string[]]$ProcArgs, [switch]$PreQuoted)
|
||||||
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
||||||
$psi.FileName = $Exe
|
$psi.FileName = $Exe
|
||||||
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
|
$psi.Arguments = if ($PreQuoted) {
|
||||||
|
$ProcArgs -join ' '
|
||||||
|
} else {
|
||||||
|
($ProcArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
|
||||||
|
}
|
||||||
$psi.UseShellExecute = $false
|
$psi.UseShellExecute = $false
|
||||||
$psi.CreateNoWindow = $true
|
$psi.CreateNoWindow = $true
|
||||||
$psi.RedirectStandardInput = $true
|
$psi.RedirectStandardInput = $true
|
||||||
$psi.RedirectStandardOutput = $true
|
$psi.RedirectStandardOutput = $true
|
||||||
$psi.RedirectStandardError = $true
|
$psi.RedirectStandardError = $true
|
||||||
try {
|
|
||||||
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
|
|
||||||
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
|
|
||||||
} catch {}
|
|
||||||
$p = [System.Diagnostics.Process]::Start($psi)
|
$p = [System.Diagnostics.Process]::Start($psi)
|
||||||
$p.StandardInput.Close()
|
$p.StandardInput.Close()
|
||||||
$out = $p.StandardOutput.ReadToEnd()
|
# stderr is drained in parallel: reading the streams one after another deadlocks
|
||||||
$err = $p.StandardError.ReadToEnd()
|
# as soon as the other one fills its pipe buffer.
|
||||||
|
$errMs = New-Object System.IO.MemoryStream
|
||||||
|
$errTask = $p.StandardError.BaseStream.CopyToAsync($errMs)
|
||||||
|
$outMs = New-Object System.IO.MemoryStream
|
||||||
|
$p.StandardOutput.BaseStream.CopyTo($outMs)
|
||||||
|
$errTask.Wait()
|
||||||
$p.WaitForExit()
|
$p.WaitForExit()
|
||||||
|
$out = ConvertFrom-PlatformBytes $outMs.ToArray()
|
||||||
|
$err = ConvertFrom-PlatformBytes $errMs.ToArray()
|
||||||
if ($err) { $out += $err }
|
if ($err) { $out += $err }
|
||||||
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
|
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Write-PlatformOutput {
|
||||||
|
# Print what the platform wrote to the console as its own labelled block. Silence stays
|
||||||
|
# silent: in batch mode 1cv8 reports through /Out and prints nothing here.
|
||||||
|
param([string]$Text)
|
||||||
|
if (-not $Text) { return }
|
||||||
|
$t = $Text.TrimEnd()
|
||||||
|
if (-not $t) { return }
|
||||||
|
$limit = 65536
|
||||||
|
if ($t.Length -gt $limit) {
|
||||||
|
$t = "[... обрезано, показаны последние $limit символов ...]`r`n" + $t.Substring($t.Length - $limit)
|
||||||
|
}
|
||||||
|
Write-Host "--- Вывод платформы ---"
|
||||||
|
Write-Host $t
|
||||||
|
Write-Host "--- End ---"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function Test-OutputNonEmpty {
|
function Test-OutputNonEmpty {
|
||||||
# Postcondition: the platform must have produced a non-empty output file.
|
# Postcondition: the platform must have produced a non-empty output file.
|
||||||
@@ -163,6 +376,10 @@ function Test-OutputNonEmpty {
|
|||||||
|
|
||||||
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
||||||
|
|
||||||
|
# --- Resolve additional arguments for the selected engine ---
|
||||||
|
$argHints = @{ '/F' = '-InfoBasePath'; '/S' = '-InfoBaseServer + -InfoBaseRef'; '/N' = '-UserName'; '/P' = '-Password'; '--db-path' = '-InfoBasePath'; '--user' = '-UserName'; '--password' = '-Password' }
|
||||||
|
$extraArgs = @(Resolve-ExtraArgs $engine $AdditionalV8Arguments $AdditionalIbcmdArguments $argHints)
|
||||||
|
|
||||||
# --- Validate connection ---
|
# --- Validate connection ---
|
||||||
if ($engine -eq "ibcmd") {
|
if ($engine -eq "ibcmd") {
|
||||||
if (-not $InfoBasePath) {
|
if (-not $InfoBasePath) {
|
||||||
@@ -197,8 +414,9 @@ try {
|
|||||||
if ($UserName) { $arguments += "--user=$UserName" }
|
if ($UserName) { $arguments += "--user=$UserName" }
|
||||||
if ($Password) { $arguments += "--password=$Password" }
|
if ($Password) { $arguments += "--password=$Password" }
|
||||||
$arguments += "--data=$tempDir"
|
$arguments += "--data=$tempDir"
|
||||||
Write-Host "Running: ibcmd $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))"
|
$arguments += $extraArgs
|
||||||
$__ib = Invoke-IbcmdProcess $V8Path $arguments
|
Write-Host "Running: ibcmd $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
|
||||||
|
$__ib = Invoke-PlatformProcess $V8Path $arguments
|
||||||
$output = $__ib.Output
|
$output = $__ib.Output
|
||||||
$exitCode = $__ib.ExitCode
|
$exitCode = $__ib.ExitCode
|
||||||
$outMissing = ($exitCode -eq 0) -and -not (Test-OutputNonEmpty $OutputFile)
|
$outMissing = ($exitCode -eq 0) -and -not (Test-OutputNonEmpty $OutputFile)
|
||||||
@@ -210,7 +428,7 @@ try {
|
|||||||
} else {
|
} else {
|
||||||
Write-Host "Error dumping configuration (code: $exitCode)" -ForegroundColor Red
|
Write-Host "Error dumping configuration (code: $exitCode)" -ForegroundColor Red
|
||||||
}
|
}
|
||||||
if ($output) { Write-Host ($output | Out-String) }
|
Write-PlatformOutput $output
|
||||||
exit $exitCode
|
exit $exitCode
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -240,11 +458,12 @@ try {
|
|||||||
$outFile = Join-Path $tempDir "dump_cf_log.txt"
|
$outFile = Join-Path $tempDir "dump_cf_log.txt"
|
||||||
$arguments += "/Out", "`"$outFile`""
|
$arguments += "/Out", "`"$outFile`""
|
||||||
$arguments += "/DisableStartupDialogs"
|
$arguments += "/DisableStartupDialogs"
|
||||||
|
$arguments += $extraArgs
|
||||||
|
|
||||||
# --- Execute ---
|
# --- Execute ---
|
||||||
Write-Host "Running: 1cv8.exe $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))"
|
Write-Host "Running: 1cv8.exe $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
|
||||||
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
|
$__v8 = Invoke-PlatformProcess $V8Path $arguments -PreQuoted
|
||||||
$exitCode = $process.ExitCode
|
$exitCode = $__v8.ExitCode
|
||||||
|
|
||||||
# --- Result ---
|
# --- Result ---
|
||||||
# Postcondition: exit 0 without a non-empty output file is a false success.
|
# Postcondition: exit 0 without a non-empty output file is a false success.
|
||||||
@@ -266,6 +485,7 @@ try {
|
|||||||
Write-Host "--- End ---"
|
Write-Host "--- End ---"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Write-PlatformOutput $__v8.Output
|
||||||
|
|
||||||
exit $exitCode
|
exit $exitCode
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# db-dump-cf v1.9 — Dump 1C configuration to CF file
|
# db-dump-cf v1.13 — Dump 1C configuration to CF file
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -14,6 +14,28 @@ import subprocess
|
|||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
|
# Регистронезависимый ввод — паритет с 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)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def _find_project_v8path():
|
def _find_project_v8path():
|
||||||
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
||||||
@@ -36,6 +58,163 @@ def _find_project_v8path():
|
|||||||
d = parent
|
d = parent
|
||||||
|
|
||||||
|
|
||||||
|
# --- Additional platform arguments ---
|
||||||
|
V8_OWNED_KEYS = [
|
||||||
|
"DESIGNER", "ENTERPRISE", "CREATEINFOBASE", "CONFIG",
|
||||||
|
"/F", "/S", "/N", "/P", "/Out", "/DisableStartupDialogs",
|
||||||
|
"/UseTemplate", "/AddToList", "/Execute", "/C", "/URL", "/UC",
|
||||||
|
"/DumpIB", "/RestoreIB", "/DumpCfg", "/LoadCfg",
|
||||||
|
"/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg",
|
||||||
|
"/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles",
|
||||||
|
]
|
||||||
|
IBCMD_OWNED_KEYS = [
|
||||||
|
"--db-path", "--data", "--out", "--file", "--load", "--restore",
|
||||||
|
"--import", "--export", "--apply", "--force", "--create-database",
|
||||||
|
"--user", "--password",
|
||||||
|
]
|
||||||
|
V8_SECRET_KEYS = ["/P", "/UC", "/WSP", "/AWSP"]
|
||||||
|
IBCMD_SECRET_KEYS = ["--password", "--token", "--db-pwd"]
|
||||||
|
|
||||||
|
|
||||||
|
def arg_key_match(token, key):
|
||||||
|
"""Token matches a key when it equals it, or starts with it and the next character
|
||||||
|
is not a letter — catches glued /N"user" and --password=x, while keeping
|
||||||
|
/ClearCache distinct from /C."""
|
||||||
|
if len(token) < len(key):
|
||||||
|
return False
|
||||||
|
if token[: len(key)].lower() != key.lower():
|
||||||
|
return False
|
||||||
|
if len(token) == len(key):
|
||||||
|
return True
|
||||||
|
return not token[len(key)].isalpha()
|
||||||
|
|
||||||
|
|
||||||
|
def project_extra_args(name):
|
||||||
|
"""v8args / ibcmdargs from .v8-project.json — same upward walk as v8path."""
|
||||||
|
d = os.getcwd()
|
||||||
|
while True:
|
||||||
|
pf = os.path.join(d, ".v8-project.json")
|
||||||
|
if os.path.isfile(pf):
|
||||||
|
try:
|
||||||
|
with open(pf, encoding="utf-8-sig") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
v = data.get(name)
|
||||||
|
if v:
|
||||||
|
return [str(x) for x in v]
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return []
|
||||||
|
parent = os.path.dirname(d)
|
||||||
|
if parent == d:
|
||||||
|
return []
|
||||||
|
d = parent
|
||||||
|
|
||||||
|
|
||||||
|
def assert_extra_args(extra, engine, hints):
|
||||||
|
"""The platform accepts only one batch operation, and a duplicate connection or
|
||||||
|
output key fails with an opaque 1C error — reject what the skill owns itself."""
|
||||||
|
param = "-AdditionalIbcmdArguments" if engine == "ibcmd" else "-AdditionalV8Arguments"
|
||||||
|
owned = IBCMD_OWNED_KEYS if engine == "ibcmd" else V8_OWNED_KEYS
|
||||||
|
for tok in extra:
|
||||||
|
if engine == "ibcmd" and not tok.startswith("-"):
|
||||||
|
print(
|
||||||
|
f"Error: '{tok}' is a positional token — pass values as --key=value "
|
||||||
|
f"({param} cannot extend the ibcmd command)",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
for k in owned:
|
||||||
|
if arg_key_match(tok, k):
|
||||||
|
hint = f" (use {hints[k]})" if hints and k in hints else ""
|
||||||
|
print(
|
||||||
|
f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def format_args_for_display(arglist, engine):
|
||||||
|
"""Redact values of secret-prone keys in glued, =-joined and separate forms.
|
||||||
|
Matching here is a plain prefix (no letter rule): over-masking costs nothing,
|
||||||
|
a leaked password does."""
|
||||||
|
keys = IBCMD_SECRET_KEYS if engine == "ibcmd" else V8_SECRET_KEYS
|
||||||
|
res = []
|
||||||
|
mask_next = False
|
||||||
|
for tok in arglist:
|
||||||
|
if mask_next:
|
||||||
|
res.append("***")
|
||||||
|
mask_next = False
|
||||||
|
continue
|
||||||
|
hit = None
|
||||||
|
for k in keys:
|
||||||
|
if tok[: len(k)].lower() == k.lower():
|
||||||
|
hit = k
|
||||||
|
break
|
||||||
|
if hit is None:
|
||||||
|
res.append(tok)
|
||||||
|
elif len(tok) == len(hit):
|
||||||
|
res.append(tok)
|
||||||
|
mask_next = True
|
||||||
|
elif tok[len(hit)] == "=":
|
||||||
|
res.append(hit + "=***")
|
||||||
|
else:
|
||||||
|
res.append(hit + "***")
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
def extract_extra_args(argv, known_opts):
|
||||||
|
"""argparse refuses values that start with '-' (every ibcmd key does), so pull the two
|
||||||
|
escape-hatch lists out of argv by hand: after the flag, take everything up to the next
|
||||||
|
declared skill option. Returns (remaining_argv, v8_extra, ibcmd_extra)."""
|
||||||
|
rest, v8, ibcmd = [], [], []
|
||||||
|
i = 0
|
||||||
|
while i < len(argv):
|
||||||
|
low = argv[i].lower()
|
||||||
|
if low in ("-additionalv8arguments", "-additionalibcmdarguments"):
|
||||||
|
target = v8 if low == "-additionalv8arguments" else ibcmd
|
||||||
|
i += 1
|
||||||
|
while i < len(argv) and argv[i].lower() not in known_opts:
|
||||||
|
target.append(argv[i])
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
rest.append(argv[i])
|
||||||
|
i += 1
|
||||||
|
return rest, v8, ibcmd
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints):
|
||||||
|
"""Pick the argument list for the selected engine and validate it. An explicitly
|
||||||
|
passed parameter for the other engine is an error; the same keys coming from
|
||||||
|
.v8-project.json simply do not apply — a project may describe both engines.
|
||||||
|
|
||||||
|
Comma-separated elements are split apart: PowerShell's -File cannot bind an array,
|
||||||
|
so that form is the documented one and both ports must accept it. A value containing
|
||||||
|
a comma is not supported."""
|
||||||
|
v8_extra = [p for tok in v8_extra for p in str(tok).split(",") if p]
|
||||||
|
ibcmd_extra = [p for tok in ibcmd_extra for p in str(tok).split(",") if p]
|
||||||
|
if engine == "ibcmd" and v8_extra:
|
||||||
|
print(
|
||||||
|
"Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd "
|
||||||
|
"(use -AdditionalIbcmdArguments)",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
if engine != "ibcmd" and ibcmd_extra:
|
||||||
|
print(
|
||||||
|
"Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 "
|
||||||
|
"(use -AdditionalV8Arguments)",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
if engine == "ibcmd":
|
||||||
|
extra = project_extra_args("ibcmdargs") + list(ibcmd_extra)
|
||||||
|
else:
|
||||||
|
extra = project_extra_args("v8args") + list(v8_extra)
|
||||||
|
if extra:
|
||||||
|
assert_extra_args(extra, engine, hints)
|
||||||
|
return extra
|
||||||
|
|
||||||
|
|
||||||
def _version_dir(p):
|
def _version_dir(p):
|
||||||
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
|
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
|
||||||
parent = os.path.dirname(p)
|
parent = os.path.dirname(p)
|
||||||
@@ -86,6 +265,85 @@ IBCMD_NOUSER_HINT = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def decode_platform_bytes(data):
|
||||||
|
"""ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
|
||||||
|
OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes — the locale
|
||||||
|
code page (what text=True uses) mangles both."""
|
||||||
|
if not data:
|
||||||
|
return ""
|
||||||
|
try:
|
||||||
|
return data.decode("utf-8")
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
return data.decode("cp866", errors="replace")
|
||||||
|
|
||||||
|
|
||||||
|
def assert_infobase_exists(path):
|
||||||
|
"""These skills work on a ready infobase. Saying so up front beats the platform's
|
||||||
|
"Неверные или отсутствующие параметры соединения" after a launch."""
|
||||||
|
if not path:
|
||||||
|
return
|
||||||
|
if not os.path.isfile(os.path.join(path, "1Cv8.1CD")):
|
||||||
|
print(f"Error: information base not found at {path} (no 1Cv8.1CD)", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def clean_path(value, param=""):
|
||||||
|
"""Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
|
||||||
|
surrounding quotes that survived shell parsing, a trailing separator. A quote left
|
||||||
|
inside afterwards cannot be part of a real path — reject it by name instead of letting
|
||||||
|
1C answer with its opaque "Неверные или отсутствующие параметры соединения"."""
|
||||||
|
if not value:
|
||||||
|
return value
|
||||||
|
v = value.strip()
|
||||||
|
if len(v) >= 2 and v[0] == v[-1] and v[0] in "\"'":
|
||||||
|
v = v[1:-1].strip()
|
||||||
|
if len(v) > 3 and v[-1] in "\\/":
|
||||||
|
v = v[:-1]
|
||||||
|
if '"' in v:
|
||||||
|
print(f"Error: {param or 'path'} contains a quote character: {value}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
return v
|
||||||
|
|
||||||
|
|
||||||
|
def quote_if_needed(token):
|
||||||
|
"""Extra arguments come from the caller unquoted; the 1cv8 command line is joined
|
||||||
|
verbatim, so a token with a space needs quotes of its own."""
|
||||||
|
if token and (" " in token or "\t" in token) and '"' not in token:
|
||||||
|
return f'"{token}"'
|
||||||
|
return token
|
||||||
|
|
||||||
|
|
||||||
|
def run_v8(v8path, arguments):
|
||||||
|
"""Run 1cv8 in batch mode and capture its console output.
|
||||||
|
|
||||||
|
The arguments carry their own quotes inside the value (File="C:\\a b") — that is where
|
||||||
|
1C's parser expects them, on Windows and on *nix alike. Windows list2cmdline would
|
||||||
|
escape those quotes, so there the command line is handed over ready-made.
|
||||||
|
"""
|
||||||
|
if os.name == "nt":
|
||||||
|
cmd = '"' + v8path + '" ' + " ".join(arguments)
|
||||||
|
else:
|
||||||
|
cmd = [v8path] + arguments
|
||||||
|
r = subprocess.run(cmd, input=b"", capture_output=True)
|
||||||
|
r.stdout = decode_platform_bytes(r.stdout)
|
||||||
|
r.stderr = decode_platform_bytes(r.stderr)
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
|
def print_platform_output(result):
|
||||||
|
"""Print what the platform wrote to the console as its own labelled block. Silence stays
|
||||||
|
silent: in batch mode 1cv8 reports through /Out and prints nothing here."""
|
||||||
|
text = ((result.stdout or "") + (result.stderr or "")).rstrip()
|
||||||
|
if not text:
|
||||||
|
return
|
||||||
|
limit = 65536
|
||||||
|
if len(text) > limit:
|
||||||
|
text = f"[... обрезано, показаны последние {limit} символов ...]\n" + text[-limit:]
|
||||||
|
print("--- Вывод платформы ---")
|
||||||
|
print(text)
|
||||||
|
print("--- End ---")
|
||||||
|
|
||||||
|
|
||||||
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
||||||
"""Run an ibcmd command non-interactively.
|
"""Run an ibcmd command non-interactively.
|
||||||
|
|
||||||
@@ -96,7 +354,10 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
|||||||
if warn_no_user and os.name == "nt" and not has_username:
|
if warn_no_user and os.name == "nt" and not has_username:
|
||||||
sys.stderr.write(IBCMD_NOUSER_HINT)
|
sys.stderr.write(IBCMD_NOUSER_HINT)
|
||||||
sys.stderr.flush()
|
sys.stderr.flush()
|
||||||
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
|
r = subprocess.run(cmd, input=b"", capture_output=True)
|
||||||
|
r.stdout = decode_platform_bytes(r.stdout)
|
||||||
|
r.stderr = decode_platform_bytes(r.stderr)
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
def output_nonempty(path):
|
def output_nonempty(path):
|
||||||
@@ -130,11 +391,34 @@ def main():
|
|||||||
parser.add_argument("-OutputFile", required=True)
|
parser.add_argument("-OutputFile", required=True)
|
||||||
parser.add_argument("-Extension", default="")
|
parser.add_argument("-Extension", default="")
|
||||||
parser.add_argument("-AllExtensions", action="store_true")
|
parser.add_argument("-AllExtensions", action="store_true")
|
||||||
args = parser.parse_args()
|
parser.add_argument("-AdditionalV8Arguments", nargs="*", default=[],
|
||||||
|
help="Extra 1cv8 arguments, e.g. /UseHwLicenses+")
|
||||||
|
parser.add_argument("-AdditionalIbcmdArguments", nargs="*", default=[],
|
||||||
|
help="Extra ibcmd arguments in --key=value form")
|
||||||
|
known_opts = {s.lower() for a in parser._actions for s in a.option_strings}
|
||||||
|
argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts)
|
||||||
|
args = ci_parse_args(parser, argv)
|
||||||
|
|
||||||
|
args.V8Path = clean_path(args.V8Path, "-V8Path")
|
||||||
|
args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath")
|
||||||
|
assert_infobase_exists(args.InfoBasePath)
|
||||||
|
args.OutputFile = clean_path(args.OutputFile, "-OutputFile")
|
||||||
|
|
||||||
v8path = resolve_v8path(args.V8Path)
|
v8path = resolve_v8path(args.V8Path)
|
||||||
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
|
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
|
||||||
|
|
||||||
|
# --- Resolve additional arguments for the selected engine ---
|
||||||
|
arg_hints = {
|
||||||
|
"/F": "-InfoBasePath",
|
||||||
|
"/S": "-InfoBaseServer + -InfoBaseRef",
|
||||||
|
"/N": "-UserName",
|
||||||
|
"/P": "-Password",
|
||||||
|
"--db-path": "-InfoBasePath",
|
||||||
|
"--user": "-UserName",
|
||||||
|
"--password": "-Password",
|
||||||
|
}
|
||||||
|
extra_args = resolve_extra_args(engine, v8_extra, ibcmd_extra, arg_hints)
|
||||||
|
|
||||||
# --- Validate connection ---
|
# --- Validate connection ---
|
||||||
if engine == "ibcmd":
|
if engine == "ibcmd":
|
||||||
if not args.InfoBasePath:
|
if not args.InfoBasePath:
|
||||||
@@ -165,7 +449,8 @@ def main():
|
|||||||
if args.Password:
|
if args.Password:
|
||||||
arguments.append(f"--password={args.Password}")
|
arguments.append(f"--password={args.Password}")
|
||||||
arguments.append(f"--data={ib_data}")
|
arguments.append(f"--data={ib_data}")
|
||||||
print(f"Running: ibcmd {_redact(' '.join(arguments), args.Password, args.UserName)}")
|
arguments.extend(extra_args)
|
||||||
|
print(f"Running: ibcmd {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
|
||||||
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
|
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
|
||||||
exit_code = result.returncode
|
exit_code = result.returncode
|
||||||
out_missing = exit_code == 0 and not output_nonempty(args.OutputFile)
|
out_missing = exit_code == 0 and not output_nonempty(args.OutputFile)
|
||||||
@@ -177,10 +462,6 @@ def main():
|
|||||||
print(f"Error: exit code 0 but no non-empty file at {args.OutputFile} — configuration was not dumped", file=sys.stderr)
|
print(f"Error: exit code 0 but no non-empty file at {args.OutputFile} — configuration was not dumped", file=sys.stderr)
|
||||||
else:
|
else:
|
||||||
print(f"Error dumping configuration (code: {exit_code})", file=sys.stderr)
|
print(f"Error dumping configuration (code: {exit_code})", file=sys.stderr)
|
||||||
if result.stdout:
|
|
||||||
print(result.stdout)
|
|
||||||
if result.stderr:
|
|
||||||
print(result.stderr, file=sys.stderr)
|
|
||||||
sys.exit(exit_code)
|
sys.exit(exit_code)
|
||||||
|
|
||||||
# --- Temp dir ---
|
# --- Temp dir ---
|
||||||
@@ -192,35 +473,32 @@ def main():
|
|||||||
arguments = ["DESIGNER"]
|
arguments = ["DESIGNER"]
|
||||||
|
|
||||||
if args.InfoBaseServer and args.InfoBaseRef:
|
if args.InfoBaseServer and args.InfoBaseRef:
|
||||||
arguments.extend(["/S", f"{args.InfoBaseServer}/{args.InfoBaseRef}"])
|
arguments.extend(["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"'])
|
||||||
else:
|
else:
|
||||||
arguments.extend(["/F", args.InfoBasePath])
|
arguments.extend(["/F", f'"{args.InfoBasePath}"'])
|
||||||
|
|
||||||
if args.UserName:
|
if args.UserName:
|
||||||
arguments.append(f"/N{args.UserName}")
|
arguments.append(f'/N"{args.UserName}"')
|
||||||
if args.Password:
|
if args.Password:
|
||||||
arguments.append(f"/P{args.Password}")
|
arguments.append(f'/P"{args.Password}"')
|
||||||
|
|
||||||
arguments.extend(["/DumpCfg", args.OutputFile])
|
arguments.extend(["/DumpCfg", f'"{args.OutputFile}"'])
|
||||||
|
|
||||||
# --- Extensions ---
|
# --- Extensions ---
|
||||||
if args.Extension:
|
if args.Extension:
|
||||||
arguments.extend(["-Extension", args.Extension])
|
arguments.extend(["-Extension", f'"{args.Extension}"'])
|
||||||
elif args.AllExtensions:
|
elif args.AllExtensions:
|
||||||
arguments.append("-AllExtensions")
|
arguments.append("-AllExtensions")
|
||||||
|
|
||||||
# --- Output ---
|
# --- Output ---
|
||||||
out_file = os.path.join(temp_dir, "dump_cf_log.txt")
|
out_file = os.path.join(temp_dir, "dump_cf_log.txt")
|
||||||
arguments.extend(["/Out", out_file])
|
arguments.extend(["/Out", f'"{out_file}"'])
|
||||||
arguments.append("/DisableStartupDialogs")
|
arguments.append("/DisableStartupDialogs")
|
||||||
|
arguments.extend(quote_if_needed(a) for a in extra_args)
|
||||||
|
|
||||||
# --- Execute ---
|
# --- Execute ---
|
||||||
print(f"Running: 1cv8.exe {_redact(' '.join(arguments), args.Password, args.UserName)}")
|
print(f"Running: 1cv8.exe {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
|
||||||
result = subprocess.run(
|
result = run_v8(v8path, arguments)
|
||||||
[v8path] + arguments,
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
)
|
|
||||||
exit_code = result.returncode
|
exit_code = result.returncode
|
||||||
|
|
||||||
# --- Result ---
|
# --- Result ---
|
||||||
@@ -246,6 +524,7 @@ def main():
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
print_platform_output(result)
|
||||||
sys.exit(exit_code)
|
sys.exit(exit_code)
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-dt.ps1" <п
|
|||||||
| `-UserName <имя>` | нет | Имя пользователя |
|
| `-UserName <имя>` | нет | Имя пользователя |
|
||||||
| `-Password <пароль>` | нет | Пароль |
|
| `-Password <пароль>` | нет | Пароль |
|
||||||
| `-OutputFile <путь>` | да | Путь к выходному DT-файлу |
|
| `-OutputFile <путь>` | да | Путь к выходному DT-файлу |
|
||||||
|
| `-AdditionalV8Arguments <список>` | нет | Доп. аргументы запуска `1cv8.exe` через запятую, напр. `/UseHwLicenses+` |
|
||||||
|
| `-AdditionalIbcmdArguments <список>` | нет | Доп. аргументы `ibcmd` через запятую, в форме `--ключ=значение` |
|
||||||
|
|
||||||
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
|
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# db-dump-dt v1.8 — Dump 1C information base to DT file
|
# db-dump-dt v1.12 — Dump 1C information base to DT file
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||||
<#
|
<#
|
||||||
@@ -29,6 +29,12 @@
|
|||||||
.PARAMETER OutputFile
|
.PARAMETER OutputFile
|
||||||
Путь к выходному DT-файлу
|
Путь к выходному DT-файлу
|
||||||
|
|
||||||
|
.PARAMETER AdditionalV8Arguments
|
||||||
|
Дополнительные аргументы запуска 1cv8.exe (например /UseHwLicenses+)
|
||||||
|
|
||||||
|
.PARAMETER AdditionalIbcmdArguments
|
||||||
|
Дополнительные аргументы запуска ibcmd (форма --ключ=значение)
|
||||||
|
|
||||||
.EXAMPLE
|
.EXAMPLE
|
||||||
.\db-dump-dt.ps1 -InfoBasePath "C:\Bases\MyDB" -OutputFile "backup.dt"
|
.\db-dump-dt.ps1 -InfoBasePath "C:\Bases\MyDB" -OutputFile "backup.dt"
|
||||||
#>
|
#>
|
||||||
@@ -54,7 +60,13 @@ param(
|
|||||||
[string]$Password,
|
[string]$Password,
|
||||||
|
|
||||||
[Parameter(Mandatory=$true)]
|
[Parameter(Mandatory=$true)]
|
||||||
[string]$OutputFile
|
[string]$OutputFile,
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[string[]]$AdditionalV8Arguments = @(),
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[string[]]$AdditionalIbcmdArguments = @()
|
||||||
)
|
)
|
||||||
|
|
||||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
@@ -67,6 +79,164 @@ function Protect-Secrets {
|
|||||||
return $Text
|
return $Text
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- Additional platform arguments ---
|
||||||
|
$script:V8OwnedKeys = @(
|
||||||
|
'DESIGNER', 'ENTERPRISE', 'CREATEINFOBASE', 'CONFIG',
|
||||||
|
'/F', '/S', '/N', '/P', '/Out', '/DisableStartupDialogs',
|
||||||
|
'/UseTemplate', '/AddToList', '/Execute', '/C', '/URL', '/UC',
|
||||||
|
'/DumpIB', '/RestoreIB', '/DumpCfg', '/LoadCfg',
|
||||||
|
'/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg',
|
||||||
|
'/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles'
|
||||||
|
)
|
||||||
|
$script:IbcmdOwnedKeys = @(
|
||||||
|
'--db-path', '--data', '--out', '--file', '--load', '--restore',
|
||||||
|
'--import', '--export', '--apply', '--force', '--create-database',
|
||||||
|
'--user', '--password'
|
||||||
|
)
|
||||||
|
$script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP')
|
||||||
|
$script:IbcmdSecretKeys = @('--password', '--token', '--db-pwd')
|
||||||
|
|
||||||
|
function Test-ArgKeyMatch {
|
||||||
|
# A token matches a key when it equals the key, or starts with it and the next
|
||||||
|
# character is not a letter — catches glued /N"user" and --password=x, while
|
||||||
|
# keeping /ClearCache distinct from /C.
|
||||||
|
param([string]$Token, [string]$Key)
|
||||||
|
if ($Token.Length -lt $Key.Length) { return $false }
|
||||||
|
if (-not $Token.Substring(0, $Key.Length).Equals($Key, [System.StringComparison]::OrdinalIgnoreCase)) { return $false }
|
||||||
|
if ($Token.Length -eq $Key.Length) { return $true }
|
||||||
|
return -not [char]::IsLetter($Token[$Key.Length])
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-ProjectExtraArgs {
|
||||||
|
# v8args / ibcmdargs from .v8-project.json — same upward walk as v8path.
|
||||||
|
param([string]$Name)
|
||||||
|
$dir = (Get-Location).Path
|
||||||
|
while ($dir) {
|
||||||
|
$pf = Join-Path $dir ".v8-project.json"
|
||||||
|
if (Test-Path $pf) {
|
||||||
|
try {
|
||||||
|
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||||
|
if ($j.$Name) { return @($j.$Name | ForEach-Object { [string]$_ }) }
|
||||||
|
} catch {}
|
||||||
|
return @()
|
||||||
|
}
|
||||||
|
$parent = Split-Path $dir -Parent
|
||||||
|
if (-not $parent -or $parent -eq $dir) { break }
|
||||||
|
$dir = $parent
|
||||||
|
}
|
||||||
|
return @()
|
||||||
|
}
|
||||||
|
|
||||||
|
function Assert-ExtraArgs {
|
||||||
|
# The platform accepts only one batch operation, and a duplicate connection or
|
||||||
|
# output key fails with an opaque 1C error — reject what the skill owns itself.
|
||||||
|
param([string[]]$ExtraArgs, [string]$Engine, [hashtable]$Hints)
|
||||||
|
$paramName = if ($Engine -eq 'ibcmd') { '-AdditionalIbcmdArguments' } else { '-AdditionalV8Arguments' }
|
||||||
|
$owned = if ($Engine -eq 'ibcmd') { $script:IbcmdOwnedKeys } else { $script:V8OwnedKeys }
|
||||||
|
foreach ($tok in $ExtraArgs) {
|
||||||
|
if ($Engine -eq 'ibcmd' -and $tok -notmatch '^-') {
|
||||||
|
Write-Host "Error: '$tok' is a positional token — pass values as --key=value ($paramName cannot extend the ibcmd command)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
foreach ($k in $owned) {
|
||||||
|
if (Test-ArgKeyMatch $tok $k) {
|
||||||
|
$hint = ''
|
||||||
|
if ($Hints -and $Hints.ContainsKey($k)) { $hint = " (use $($Hints[$k]))" }
|
||||||
|
Write-Host "Error: $k is controlled by the skill and cannot be passed via $paramName$hint" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Resolve-ExtraArgs {
|
||||||
|
# Pick the argument list for the selected engine and validate it. An explicitly passed
|
||||||
|
# parameter for the other engine is an error; the same keys coming from .v8-project.json
|
||||||
|
# simply do not apply — a project may describe both engines.
|
||||||
|
param([string]$Engine, [string[]]$V8Extra, [string[]]$IbcmdExtra, [hashtable]$Hints)
|
||||||
|
# powershell.exe -File — how skills are invoked — cannot bind an array parameter:
|
||||||
|
# space-separated values spill into positional ones, a comma-joined list arrives as a
|
||||||
|
# single token. So accept the repo's list convention (comma-separated) and split here;
|
||||||
|
# a native array call keeps working. A value containing a comma is not supported.
|
||||||
|
$V8Extra = @($V8Extra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
|
||||||
|
$IbcmdExtra = @($IbcmdExtra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
|
||||||
|
if ($Engine -eq 'ibcmd' -and $V8Extra.Count -gt 0) {
|
||||||
|
Write-Host "Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd (use -AdditionalIbcmdArguments)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
if ($Engine -ne 'ibcmd' -and $IbcmdExtra.Count -gt 0) {
|
||||||
|
Write-Host "Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 (use -AdditionalV8Arguments)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
if ($Engine -eq 'ibcmd') {
|
||||||
|
$extra = @(Get-ProjectExtraArgs 'ibcmdargs') + @($IbcmdExtra)
|
||||||
|
} else {
|
||||||
|
$extra = @(Get-ProjectExtraArgs 'v8args') + @($V8Extra)
|
||||||
|
}
|
||||||
|
if ($extra.Count -gt 0) { Assert-ExtraArgs $extra $Engine $Hints }
|
||||||
|
# Plain return, no comma trick: the caller re-collects with @(...), and ,@() there
|
||||||
|
# would nest the array — the tokens would then be glued into one argument.
|
||||||
|
return $extra
|
||||||
|
}
|
||||||
|
|
||||||
|
function Format-ArgsForDisplay {
|
||||||
|
# Redact values of secret-prone keys in glued, =-joined and separate forms.
|
||||||
|
# Matching here is a plain prefix (no letter rule): over-masking costs nothing,
|
||||||
|
# a leaked password does.
|
||||||
|
param([string[]]$ArgList, [string]$Engine)
|
||||||
|
$keys = if ($Engine -eq 'ibcmd') { $script:IbcmdSecretKeys } else { $script:V8SecretKeys }
|
||||||
|
$res = @()
|
||||||
|
$maskNext = $false
|
||||||
|
foreach ($tok in $ArgList) {
|
||||||
|
if ($maskNext) { $res += '***'; $maskNext = $false; continue }
|
||||||
|
$hit = $null
|
||||||
|
foreach ($k in $keys) {
|
||||||
|
if ($tok.Length -ge $k.Length -and $tok.Substring(0, $k.Length).Equals($k, [System.StringComparison]::OrdinalIgnoreCase)) { $hit = $k; break }
|
||||||
|
}
|
||||||
|
if (-not $hit) { $res += $tok; continue }
|
||||||
|
if ($tok.Length -eq $hit.Length) { $res += $tok; $maskNext = $true }
|
||||||
|
elseif ($tok[$hit.Length] -eq '=') { $res += ($hit + '=***') }
|
||||||
|
else { $res += ($hit + '***') }
|
||||||
|
}
|
||||||
|
return ,$res
|
||||||
|
}
|
||||||
|
|
||||||
|
function ConvertTo-CleanPath {
|
||||||
|
# Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
|
||||||
|
# surrounding quotes that survived shell parsing, a trailing separator. A quote left
|
||||||
|
# inside afterwards cannot be part of a real path — reject it by name instead of letting
|
||||||
|
# 1C answer with its opaque "Неверные или отсутствующие параметры соединения".
|
||||||
|
param([string]$Value, [string]$ParamName)
|
||||||
|
if (-not $Value) { return $Value }
|
||||||
|
$v = $Value.Trim()
|
||||||
|
if ($v.Length -ge 2 -and $v[0] -eq $v[-1] -and ($v[0] -eq '"' -or $v[0] -eq "'")) {
|
||||||
|
$v = $v.Substring(1, $v.Length - 2).Trim()
|
||||||
|
}
|
||||||
|
if ($v.Length -gt 3 -and ($v[-1] -eq '\' -or $v[-1] -eq '/')) { $v = $v.Substring(0, $v.Length - 1) }
|
||||||
|
if ($v.Contains('"')) {
|
||||||
|
Write-Host "Error: $ParamName contains a quote character: $Value" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
return $v
|
||||||
|
}
|
||||||
|
|
||||||
|
$V8Path = ConvertTo-CleanPath $V8Path '-V8Path'
|
||||||
|
$InfoBasePath = ConvertTo-CleanPath $InfoBasePath '-InfoBasePath'
|
||||||
|
$OutputFile = ConvertTo-CleanPath $OutputFile '-OutputFile'
|
||||||
|
|
||||||
|
function Assert-InfoBaseExists {
|
||||||
|
# These skills work on a ready infobase. Saying so up front beats the platform's
|
||||||
|
# "Неверные или отсутствующие параметры соединения" after a launch.
|
||||||
|
param([string]$Path)
|
||||||
|
if (-not $Path) { return }
|
||||||
|
if (-not (Test-Path (Join-Path $Path "1Cv8.1CD"))) {
|
||||||
|
Write-Host "Error: information base not found at $Path (no 1Cv8.1CD)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert-InfoBaseExists $InfoBasePath
|
||||||
|
|
||||||
# --- Resolve V8Path ---
|
# --- Resolve V8Path ---
|
||||||
function Find-ProjectV8Path {
|
function Find-ProjectV8Path {
|
||||||
$dir = (Get-Location).Path
|
$dir = (Get-Location).Path
|
||||||
@@ -111,32 +281,75 @@ if (-not (Test-Path $V8Path)) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
|
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
|
||||||
function Invoke-IbcmdProcess {
|
function ConvertFrom-PlatformBytes {
|
||||||
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
|
# ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
|
||||||
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
|
# OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes — guessing
|
||||||
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
|
# one of them outright mangles Cyrillic.
|
||||||
param([string]$Exe, [string[]]$IbArgs)
|
param([byte[]]$Bytes)
|
||||||
|
if (-not $Bytes -or $Bytes.Length -eq 0) { return '' }
|
||||||
|
try {
|
||||||
|
$strict = New-Object System.Text.UTF8Encoding($false, $true)
|
||||||
|
return $strict.GetString($Bytes)
|
||||||
|
} catch {
|
||||||
|
return [System.Text.Encoding]::GetEncoding(866).GetString($Bytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-PlatformProcess {
|
||||||
|
# Run the platform non-interactively and capture its console output. A closed stdin pipe
|
||||||
|
# (EOF) makes an auth prompt fast-fail instead of hanging; capturing keeps the child's
|
||||||
|
# text out of our stream until we print it labelled (and out of the wrong encoding).
|
||||||
|
# Returns @{ Output; ExitCode }.
|
||||||
|
#
|
||||||
|
# Quoting differs by engine, so the caller says which it built:
|
||||||
|
# ibcmd — tokens are bare (--db-path=C:\a b), the whole token gets quoted here;
|
||||||
|
# 1cv8 — -PreQuoted: the caller already put quotes inside the token (File="C:\a b"),
|
||||||
|
# which is where 1C's own parser expects them; quoting again breaks the value.
|
||||||
|
param([string]$Exe, [string[]]$ProcArgs, [switch]$PreQuoted)
|
||||||
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
||||||
$psi.FileName = $Exe
|
$psi.FileName = $Exe
|
||||||
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
|
$psi.Arguments = if ($PreQuoted) {
|
||||||
|
$ProcArgs -join ' '
|
||||||
|
} else {
|
||||||
|
($ProcArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
|
||||||
|
}
|
||||||
$psi.UseShellExecute = $false
|
$psi.UseShellExecute = $false
|
||||||
$psi.CreateNoWindow = $true
|
$psi.CreateNoWindow = $true
|
||||||
$psi.RedirectStandardInput = $true
|
$psi.RedirectStandardInput = $true
|
||||||
$psi.RedirectStandardOutput = $true
|
$psi.RedirectStandardOutput = $true
|
||||||
$psi.RedirectStandardError = $true
|
$psi.RedirectStandardError = $true
|
||||||
try {
|
|
||||||
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
|
|
||||||
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
|
|
||||||
} catch {}
|
|
||||||
$p = [System.Diagnostics.Process]::Start($psi)
|
$p = [System.Diagnostics.Process]::Start($psi)
|
||||||
$p.StandardInput.Close()
|
$p.StandardInput.Close()
|
||||||
$out = $p.StandardOutput.ReadToEnd()
|
# stderr is drained in parallel: reading the streams one after another deadlocks
|
||||||
$err = $p.StandardError.ReadToEnd()
|
# as soon as the other one fills its pipe buffer.
|
||||||
|
$errMs = New-Object System.IO.MemoryStream
|
||||||
|
$errTask = $p.StandardError.BaseStream.CopyToAsync($errMs)
|
||||||
|
$outMs = New-Object System.IO.MemoryStream
|
||||||
|
$p.StandardOutput.BaseStream.CopyTo($outMs)
|
||||||
|
$errTask.Wait()
|
||||||
$p.WaitForExit()
|
$p.WaitForExit()
|
||||||
|
$out = ConvertFrom-PlatformBytes $outMs.ToArray()
|
||||||
|
$err = ConvertFrom-PlatformBytes $errMs.ToArray()
|
||||||
if ($err) { $out += $err }
|
if ($err) { $out += $err }
|
||||||
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
|
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Write-PlatformOutput {
|
||||||
|
# Print what the platform wrote to the console as its own labelled block. Silence stays
|
||||||
|
# silent: in batch mode 1cv8 reports through /Out and prints nothing here.
|
||||||
|
param([string]$Text)
|
||||||
|
if (-not $Text) { return }
|
||||||
|
$t = $Text.TrimEnd()
|
||||||
|
if (-not $t) { return }
|
||||||
|
$limit = 65536
|
||||||
|
if ($t.Length -gt $limit) {
|
||||||
|
$t = "[... обрезано, показаны последние $limit символов ...]`r`n" + $t.Substring($t.Length - $limit)
|
||||||
|
}
|
||||||
|
Write-Host "--- Вывод платформы ---"
|
||||||
|
Write-Host $t
|
||||||
|
Write-Host "--- End ---"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function Test-OutputNonEmpty {
|
function Test-OutputNonEmpty {
|
||||||
# Postcondition: the platform must have produced a non-empty output file.
|
# Postcondition: the platform must have produced a non-empty output file.
|
||||||
@@ -147,6 +360,10 @@ function Test-OutputNonEmpty {
|
|||||||
|
|
||||||
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
||||||
|
|
||||||
|
# --- Resolve additional arguments for the selected engine ---
|
||||||
|
$argHints = @{ '/F' = '-InfoBasePath'; '/S' = '-InfoBaseServer + -InfoBaseRef'; '/N' = '-UserName'; '/P' = '-Password'; '--db-path' = '-InfoBasePath'; '--user' = '-UserName'; '--password' = '-Password' }
|
||||||
|
$extraArgs = @(Resolve-ExtraArgs $engine $AdditionalV8Arguments $AdditionalIbcmdArguments $argHints)
|
||||||
|
|
||||||
# --- Validate connection ---
|
# --- Validate connection ---
|
||||||
if ($engine -eq "ibcmd") {
|
if ($engine -eq "ibcmd") {
|
||||||
if (-not $InfoBasePath) {
|
if (-not $InfoBasePath) {
|
||||||
@@ -177,8 +394,10 @@ try {
|
|||||||
$arguments += "$OutputFile"
|
$arguments += "$OutputFile"
|
||||||
|
|
||||||
$arguments += "--data=$tempDir"
|
$arguments += "--data=$tempDir"
|
||||||
Write-Host "Running: ibcmd $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))"
|
|
||||||
$__ib = Invoke-IbcmdProcess $V8Path $arguments
|
$arguments += $extraArgs
|
||||||
|
Write-Host "Running: ibcmd $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
|
||||||
|
$__ib = Invoke-PlatformProcess $V8Path $arguments
|
||||||
$output = $__ib.Output
|
$output = $__ib.Output
|
||||||
$exitCode = $__ib.ExitCode
|
$exitCode = $__ib.ExitCode
|
||||||
$outMissing = ($exitCode -eq 0) -and -not (Test-OutputNonEmpty $OutputFile)
|
$outMissing = ($exitCode -eq 0) -and -not (Test-OutputNonEmpty $OutputFile)
|
||||||
@@ -190,7 +409,7 @@ try {
|
|||||||
} else {
|
} else {
|
||||||
Write-Host "Error dumping information base (code: $exitCode)" -ForegroundColor Red
|
Write-Host "Error dumping information base (code: $exitCode)" -ForegroundColor Red
|
||||||
}
|
}
|
||||||
if ($output) { Write-Host ($output | Out-String) }
|
Write-PlatformOutput $output
|
||||||
exit $exitCode
|
exit $exitCode
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -213,11 +432,12 @@ try {
|
|||||||
$outFile = Join-Path $tempDir "dump_dt_log.txt"
|
$outFile = Join-Path $tempDir "dump_dt_log.txt"
|
||||||
$arguments += "/Out", "`"$outFile`""
|
$arguments += "/Out", "`"$outFile`""
|
||||||
$arguments += "/DisableStartupDialogs"
|
$arguments += "/DisableStartupDialogs"
|
||||||
|
$arguments += $extraArgs
|
||||||
|
|
||||||
# --- Execute ---
|
# --- Execute ---
|
||||||
Write-Host "Running: 1cv8.exe $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))"
|
Write-Host "Running: 1cv8.exe $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
|
||||||
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
|
$__v8 = Invoke-PlatformProcess $V8Path $arguments -PreQuoted
|
||||||
$exitCode = $process.ExitCode
|
$exitCode = $__v8.ExitCode
|
||||||
|
|
||||||
# --- Result ---
|
# --- Result ---
|
||||||
# Postcondition: exit 0 without a non-empty output file is a false success.
|
# Postcondition: exit 0 without a non-empty output file is a false success.
|
||||||
@@ -239,6 +459,7 @@ try {
|
|||||||
Write-Host "--- End ---"
|
Write-Host "--- End ---"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Write-PlatformOutput $__v8.Output
|
||||||
|
|
||||||
exit $exitCode
|
exit $exitCode
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# db-dump-dt v1.8 — Dump 1C information base to DT file
|
# db-dump-dt v1.12 — Dump 1C information base to DT file
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -14,6 +14,28 @@ import subprocess
|
|||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
|
# Регистронезависимый ввод — паритет с 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)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def _find_project_v8path():
|
def _find_project_v8path():
|
||||||
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
||||||
@@ -36,6 +58,163 @@ def _find_project_v8path():
|
|||||||
d = parent
|
d = parent
|
||||||
|
|
||||||
|
|
||||||
|
# --- Additional platform arguments ---
|
||||||
|
V8_OWNED_KEYS = [
|
||||||
|
"DESIGNER", "ENTERPRISE", "CREATEINFOBASE", "CONFIG",
|
||||||
|
"/F", "/S", "/N", "/P", "/Out", "/DisableStartupDialogs",
|
||||||
|
"/UseTemplate", "/AddToList", "/Execute", "/C", "/URL", "/UC",
|
||||||
|
"/DumpIB", "/RestoreIB", "/DumpCfg", "/LoadCfg",
|
||||||
|
"/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg",
|
||||||
|
"/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles",
|
||||||
|
]
|
||||||
|
IBCMD_OWNED_KEYS = [
|
||||||
|
"--db-path", "--data", "--out", "--file", "--load", "--restore",
|
||||||
|
"--import", "--export", "--apply", "--force", "--create-database",
|
||||||
|
"--user", "--password",
|
||||||
|
]
|
||||||
|
V8_SECRET_KEYS = ["/P", "/UC", "/WSP", "/AWSP"]
|
||||||
|
IBCMD_SECRET_KEYS = ["--password", "--token", "--db-pwd"]
|
||||||
|
|
||||||
|
|
||||||
|
def arg_key_match(token, key):
|
||||||
|
"""Token matches a key when it equals it, or starts with it and the next character
|
||||||
|
is not a letter — catches glued /N"user" and --password=x, while keeping
|
||||||
|
/ClearCache distinct from /C."""
|
||||||
|
if len(token) < len(key):
|
||||||
|
return False
|
||||||
|
if token[: len(key)].lower() != key.lower():
|
||||||
|
return False
|
||||||
|
if len(token) == len(key):
|
||||||
|
return True
|
||||||
|
return not token[len(key)].isalpha()
|
||||||
|
|
||||||
|
|
||||||
|
def project_extra_args(name):
|
||||||
|
"""v8args / ibcmdargs from .v8-project.json — same upward walk as v8path."""
|
||||||
|
d = os.getcwd()
|
||||||
|
while True:
|
||||||
|
pf = os.path.join(d, ".v8-project.json")
|
||||||
|
if os.path.isfile(pf):
|
||||||
|
try:
|
||||||
|
with open(pf, encoding="utf-8-sig") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
v = data.get(name)
|
||||||
|
if v:
|
||||||
|
return [str(x) for x in v]
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return []
|
||||||
|
parent = os.path.dirname(d)
|
||||||
|
if parent == d:
|
||||||
|
return []
|
||||||
|
d = parent
|
||||||
|
|
||||||
|
|
||||||
|
def assert_extra_args(extra, engine, hints):
|
||||||
|
"""The platform accepts only one batch operation, and a duplicate connection or
|
||||||
|
output key fails with an opaque 1C error — reject what the skill owns itself."""
|
||||||
|
param = "-AdditionalIbcmdArguments" if engine == "ibcmd" else "-AdditionalV8Arguments"
|
||||||
|
owned = IBCMD_OWNED_KEYS if engine == "ibcmd" else V8_OWNED_KEYS
|
||||||
|
for tok in extra:
|
||||||
|
if engine == "ibcmd" and not tok.startswith("-"):
|
||||||
|
print(
|
||||||
|
f"Error: '{tok}' is a positional token — pass values as --key=value "
|
||||||
|
f"({param} cannot extend the ibcmd command)",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
for k in owned:
|
||||||
|
if arg_key_match(tok, k):
|
||||||
|
hint = f" (use {hints[k]})" if hints and k in hints else ""
|
||||||
|
print(
|
||||||
|
f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def format_args_for_display(arglist, engine):
|
||||||
|
"""Redact values of secret-prone keys in glued, =-joined and separate forms.
|
||||||
|
Matching here is a plain prefix (no letter rule): over-masking costs nothing,
|
||||||
|
a leaked password does."""
|
||||||
|
keys = IBCMD_SECRET_KEYS if engine == "ibcmd" else V8_SECRET_KEYS
|
||||||
|
res = []
|
||||||
|
mask_next = False
|
||||||
|
for tok in arglist:
|
||||||
|
if mask_next:
|
||||||
|
res.append("***")
|
||||||
|
mask_next = False
|
||||||
|
continue
|
||||||
|
hit = None
|
||||||
|
for k in keys:
|
||||||
|
if tok[: len(k)].lower() == k.lower():
|
||||||
|
hit = k
|
||||||
|
break
|
||||||
|
if hit is None:
|
||||||
|
res.append(tok)
|
||||||
|
elif len(tok) == len(hit):
|
||||||
|
res.append(tok)
|
||||||
|
mask_next = True
|
||||||
|
elif tok[len(hit)] == "=":
|
||||||
|
res.append(hit + "=***")
|
||||||
|
else:
|
||||||
|
res.append(hit + "***")
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
def extract_extra_args(argv, known_opts):
|
||||||
|
"""argparse refuses values that start with '-' (every ibcmd key does), so pull the two
|
||||||
|
escape-hatch lists out of argv by hand: after the flag, take everything up to the next
|
||||||
|
declared skill option. Returns (remaining_argv, v8_extra, ibcmd_extra)."""
|
||||||
|
rest, v8, ibcmd = [], [], []
|
||||||
|
i = 0
|
||||||
|
while i < len(argv):
|
||||||
|
low = argv[i].lower()
|
||||||
|
if low in ("-additionalv8arguments", "-additionalibcmdarguments"):
|
||||||
|
target = v8 if low == "-additionalv8arguments" else ibcmd
|
||||||
|
i += 1
|
||||||
|
while i < len(argv) and argv[i].lower() not in known_opts:
|
||||||
|
target.append(argv[i])
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
rest.append(argv[i])
|
||||||
|
i += 1
|
||||||
|
return rest, v8, ibcmd
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints):
|
||||||
|
"""Pick the argument list for the selected engine and validate it. An explicitly
|
||||||
|
passed parameter for the other engine is an error; the same keys coming from
|
||||||
|
.v8-project.json simply do not apply — a project may describe both engines.
|
||||||
|
|
||||||
|
Comma-separated elements are split apart: PowerShell's -File cannot bind an array,
|
||||||
|
so that form is the documented one and both ports must accept it. A value containing
|
||||||
|
a comma is not supported."""
|
||||||
|
v8_extra = [p for tok in v8_extra for p in str(tok).split(",") if p]
|
||||||
|
ibcmd_extra = [p for tok in ibcmd_extra for p in str(tok).split(",") if p]
|
||||||
|
if engine == "ibcmd" and v8_extra:
|
||||||
|
print(
|
||||||
|
"Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd "
|
||||||
|
"(use -AdditionalIbcmdArguments)",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
if engine != "ibcmd" and ibcmd_extra:
|
||||||
|
print(
|
||||||
|
"Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 "
|
||||||
|
"(use -AdditionalV8Arguments)",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
if engine == "ibcmd":
|
||||||
|
extra = project_extra_args("ibcmdargs") + list(ibcmd_extra)
|
||||||
|
else:
|
||||||
|
extra = project_extra_args("v8args") + list(v8_extra)
|
||||||
|
if extra:
|
||||||
|
assert_extra_args(extra, engine, hints)
|
||||||
|
return extra
|
||||||
|
|
||||||
|
|
||||||
def _version_dir(p):
|
def _version_dir(p):
|
||||||
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
|
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
|
||||||
parent = os.path.dirname(p)
|
parent = os.path.dirname(p)
|
||||||
@@ -86,6 +265,85 @@ IBCMD_NOUSER_HINT = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def decode_platform_bytes(data):
|
||||||
|
"""ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
|
||||||
|
OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes — the locale
|
||||||
|
code page (what text=True uses) mangles both."""
|
||||||
|
if not data:
|
||||||
|
return ""
|
||||||
|
try:
|
||||||
|
return data.decode("utf-8")
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
return data.decode("cp866", errors="replace")
|
||||||
|
|
||||||
|
|
||||||
|
def assert_infobase_exists(path):
|
||||||
|
"""These skills work on a ready infobase. Saying so up front beats the platform's
|
||||||
|
"Неверные или отсутствующие параметры соединения" after a launch."""
|
||||||
|
if not path:
|
||||||
|
return
|
||||||
|
if not os.path.isfile(os.path.join(path, "1Cv8.1CD")):
|
||||||
|
print(f"Error: information base not found at {path} (no 1Cv8.1CD)", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def clean_path(value, param=""):
|
||||||
|
"""Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
|
||||||
|
surrounding quotes that survived shell parsing, a trailing separator. A quote left
|
||||||
|
inside afterwards cannot be part of a real path — reject it by name instead of letting
|
||||||
|
1C answer with its opaque "Неверные или отсутствующие параметры соединения"."""
|
||||||
|
if not value:
|
||||||
|
return value
|
||||||
|
v = value.strip()
|
||||||
|
if len(v) >= 2 and v[0] == v[-1] and v[0] in "\"'":
|
||||||
|
v = v[1:-1].strip()
|
||||||
|
if len(v) > 3 and v[-1] in "\\/":
|
||||||
|
v = v[:-1]
|
||||||
|
if '"' in v:
|
||||||
|
print(f"Error: {param or 'path'} contains a quote character: {value}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
return v
|
||||||
|
|
||||||
|
|
||||||
|
def quote_if_needed(token):
|
||||||
|
"""Extra arguments come from the caller unquoted; the 1cv8 command line is joined
|
||||||
|
verbatim, so a token with a space needs quotes of its own."""
|
||||||
|
if token and (" " in token or "\t" in token) and '"' not in token:
|
||||||
|
return f'"{token}"'
|
||||||
|
return token
|
||||||
|
|
||||||
|
|
||||||
|
def run_v8(v8path, arguments):
|
||||||
|
"""Run 1cv8 in batch mode and capture its console output.
|
||||||
|
|
||||||
|
The arguments carry their own quotes inside the value (File="C:\\a b") — that is where
|
||||||
|
1C's parser expects them, on Windows and on *nix alike. Windows list2cmdline would
|
||||||
|
escape those quotes, so there the command line is handed over ready-made.
|
||||||
|
"""
|
||||||
|
if os.name == "nt":
|
||||||
|
cmd = '"' + v8path + '" ' + " ".join(arguments)
|
||||||
|
else:
|
||||||
|
cmd = [v8path] + arguments
|
||||||
|
r = subprocess.run(cmd, input=b"", capture_output=True)
|
||||||
|
r.stdout = decode_platform_bytes(r.stdout)
|
||||||
|
r.stderr = decode_platform_bytes(r.stderr)
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
|
def print_platform_output(result):
|
||||||
|
"""Print what the platform wrote to the console as its own labelled block. Silence stays
|
||||||
|
silent: in batch mode 1cv8 reports through /Out and prints nothing here."""
|
||||||
|
text = ((result.stdout or "") + (result.stderr or "")).rstrip()
|
||||||
|
if not text:
|
||||||
|
return
|
||||||
|
limit = 65536
|
||||||
|
if len(text) > limit:
|
||||||
|
text = f"[... обрезано, показаны последние {limit} символов ...]\n" + text[-limit:]
|
||||||
|
print("--- Вывод платформы ---")
|
||||||
|
print(text)
|
||||||
|
print("--- End ---")
|
||||||
|
|
||||||
|
|
||||||
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
||||||
"""Run an ibcmd command non-interactively.
|
"""Run an ibcmd command non-interactively.
|
||||||
|
|
||||||
@@ -96,7 +354,10 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
|||||||
if warn_no_user and os.name == "nt" and not has_username:
|
if warn_no_user and os.name == "nt" and not has_username:
|
||||||
sys.stderr.write(IBCMD_NOUSER_HINT)
|
sys.stderr.write(IBCMD_NOUSER_HINT)
|
||||||
sys.stderr.flush()
|
sys.stderr.flush()
|
||||||
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
|
r = subprocess.run(cmd, input=b"", capture_output=True)
|
||||||
|
r.stdout = decode_platform_bytes(r.stdout)
|
||||||
|
r.stderr = decode_platform_bytes(r.stderr)
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
def output_nonempty(path):
|
def output_nonempty(path):
|
||||||
@@ -128,11 +389,34 @@ def main():
|
|||||||
parser.add_argument("-UserName", default="")
|
parser.add_argument("-UserName", default="")
|
||||||
parser.add_argument("-Password", default="")
|
parser.add_argument("-Password", default="")
|
||||||
parser.add_argument("-OutputFile", required=True)
|
parser.add_argument("-OutputFile", required=True)
|
||||||
args = parser.parse_args()
|
parser.add_argument("-AdditionalV8Arguments", nargs="*", default=[],
|
||||||
|
help="Extra 1cv8 arguments, e.g. /UseHwLicenses+")
|
||||||
|
parser.add_argument("-AdditionalIbcmdArguments", nargs="*", default=[],
|
||||||
|
help="Extra ibcmd arguments in --key=value form")
|
||||||
|
known_opts = {s.lower() for a in parser._actions for s in a.option_strings}
|
||||||
|
argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts)
|
||||||
|
args = ci_parse_args(parser, argv)
|
||||||
|
|
||||||
|
args.V8Path = clean_path(args.V8Path, "-V8Path")
|
||||||
|
args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath")
|
||||||
|
assert_infobase_exists(args.InfoBasePath)
|
||||||
|
args.OutputFile = clean_path(args.OutputFile, "-OutputFile")
|
||||||
|
|
||||||
v8path = resolve_v8path(args.V8Path)
|
v8path = resolve_v8path(args.V8Path)
|
||||||
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
|
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
|
||||||
|
|
||||||
|
# --- Resolve additional arguments for the selected engine ---
|
||||||
|
arg_hints = {
|
||||||
|
"/F": "-InfoBasePath",
|
||||||
|
"/S": "-InfoBaseServer + -InfoBaseRef",
|
||||||
|
"/N": "-UserName",
|
||||||
|
"/P": "-Password",
|
||||||
|
"--db-path": "-InfoBasePath",
|
||||||
|
"--user": "-UserName",
|
||||||
|
"--password": "-Password",
|
||||||
|
}
|
||||||
|
extra_args = resolve_extra_args(engine, v8_extra, ibcmd_extra, arg_hints)
|
||||||
|
|
||||||
# --- Validate connection ---
|
# --- Validate connection ---
|
||||||
if engine == "ibcmd":
|
if engine == "ibcmd":
|
||||||
if not args.InfoBasePath:
|
if not args.InfoBasePath:
|
||||||
@@ -158,7 +442,8 @@ def main():
|
|||||||
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
|
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
|
||||||
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
|
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
|
||||||
arguments.append(f"--data={ib_data}")
|
arguments.append(f"--data={ib_data}")
|
||||||
print(f"Running: ibcmd {_redact(' '.join(arguments), args.Password, args.UserName)}")
|
arguments.extend(extra_args)
|
||||||
|
print(f"Running: ibcmd {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
|
||||||
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
|
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
|
||||||
exit_code = result.returncode
|
exit_code = result.returncode
|
||||||
out_missing = exit_code == 0 and not output_nonempty(args.OutputFile)
|
out_missing = exit_code == 0 and not output_nonempty(args.OutputFile)
|
||||||
@@ -170,10 +455,6 @@ def main():
|
|||||||
print(f"Error: exit code 0 but no non-empty file at {args.OutputFile} — information base was not dumped", file=sys.stderr)
|
print(f"Error: exit code 0 but no non-empty file at {args.OutputFile} — information base was not dumped", file=sys.stderr)
|
||||||
else:
|
else:
|
||||||
print(f"Error dumping information base (code: {exit_code})", file=sys.stderr)
|
print(f"Error dumping information base (code: {exit_code})", file=sys.stderr)
|
||||||
if result.stdout:
|
|
||||||
print(result.stdout)
|
|
||||||
if result.stderr:
|
|
||||||
print(result.stderr, file=sys.stderr)
|
|
||||||
sys.exit(exit_code)
|
sys.exit(exit_code)
|
||||||
|
|
||||||
# --- Temp dir ---
|
# --- Temp dir ---
|
||||||
@@ -185,29 +466,26 @@ def main():
|
|||||||
arguments = ["DESIGNER"]
|
arguments = ["DESIGNER"]
|
||||||
|
|
||||||
if args.InfoBaseServer and args.InfoBaseRef:
|
if args.InfoBaseServer and args.InfoBaseRef:
|
||||||
arguments.extend(["/S", f"{args.InfoBaseServer}/{args.InfoBaseRef}"])
|
arguments.extend(["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"'])
|
||||||
else:
|
else:
|
||||||
arguments.extend(["/F", args.InfoBasePath])
|
arguments.extend(["/F", f'"{args.InfoBasePath}"'])
|
||||||
|
|
||||||
if args.UserName:
|
if args.UserName:
|
||||||
arguments.append(f"/N{args.UserName}")
|
arguments.append(f'/N"{args.UserName}"')
|
||||||
if args.Password:
|
if args.Password:
|
||||||
arguments.append(f"/P{args.Password}")
|
arguments.append(f'/P"{args.Password}"')
|
||||||
|
|
||||||
arguments.extend(["/DumpIB", args.OutputFile])
|
arguments.extend(["/DumpIB", f'"{args.OutputFile}"'])
|
||||||
|
|
||||||
# --- Output ---
|
# --- Output ---
|
||||||
out_file = os.path.join(temp_dir, "dump_dt_log.txt")
|
out_file = os.path.join(temp_dir, "dump_dt_log.txt")
|
||||||
arguments.extend(["/Out", out_file])
|
arguments.extend(["/Out", f'"{out_file}"'])
|
||||||
arguments.append("/DisableStartupDialogs")
|
arguments.append("/DisableStartupDialogs")
|
||||||
|
arguments.extend(quote_if_needed(a) for a in extra_args)
|
||||||
|
|
||||||
# --- Execute ---
|
# --- Execute ---
|
||||||
print(f"Running: 1cv8.exe {_redact(' '.join(arguments), args.Password, args.UserName)}")
|
print(f"Running: 1cv8.exe {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
|
||||||
result = subprocess.run(
|
result = run_v8(v8path, arguments)
|
||||||
[v8path] + arguments,
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
)
|
|
||||||
exit_code = result.returncode
|
exit_code = result.returncode
|
||||||
|
|
||||||
# --- Result ---
|
# --- Result ---
|
||||||
@@ -233,6 +511,7 @@ def main():
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
print_platform_output(result)
|
||||||
sys.exit(exit_code)
|
sys.exit(exit_code)
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
|
|||||||
@@ -56,6 +56,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" <
|
|||||||
| `-Extension <имя>` | нет | Выгрузить расширение |
|
| `-Extension <имя>` | нет | Выгрузить расширение |
|
||||||
| `-AllExtensions` | нет | Выгрузить все расширения |
|
| `-AllExtensions` | нет | Выгрузить все расширения |
|
||||||
| `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` |
|
| `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` |
|
||||||
|
| `-AdditionalV8Arguments <список>` | нет | Доп. аргументы запуска `1cv8.exe` через запятую, напр. `/UseHwLicenses+` |
|
||||||
|
| `-AdditionalIbcmdArguments <список>` | нет | Доп. аргументы `ibcmd` через запятую, в форме `--ключ=значение` |
|
||||||
|
|
||||||
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
|
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# db-dump-xml v1.11 — Dump 1C configuration to XML files
|
# db-dump-xml v1.15 — Dump 1C configuration to XML files
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||||
<#
|
<#
|
||||||
@@ -48,6 +48,12 @@
|
|||||||
.PARAMETER Format
|
.PARAMETER Format
|
||||||
Формат выгрузки: Hierarchical или Plain (по умолчанию Hierarchical)
|
Формат выгрузки: Hierarchical или Plain (по умолчанию Hierarchical)
|
||||||
|
|
||||||
|
.PARAMETER AdditionalV8Arguments
|
||||||
|
Дополнительные аргументы запуска 1cv8.exe (например /UseHwLicenses+)
|
||||||
|
|
||||||
|
.PARAMETER AdditionalIbcmdArguments
|
||||||
|
Дополнительные аргументы запуска ibcmd (форма --ключ=значение)
|
||||||
|
|
||||||
.EXAMPLE
|
.EXAMPLE
|
||||||
.\db-dump-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Mode Full
|
.\db-dump-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Mode Full
|
||||||
|
|
||||||
@@ -93,7 +99,13 @@ param(
|
|||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
[Parameter(Mandatory=$false)]
|
||||||
[ValidateSet("Hierarchical", "Plain")]
|
[ValidateSet("Hierarchical", "Plain")]
|
||||||
[string]$Format = "Hierarchical"
|
[string]$Format = "Hierarchical",
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[string[]]$AdditionalV8Arguments = @(),
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[string[]]$AdditionalIbcmdArguments = @()
|
||||||
)
|
)
|
||||||
|
|
||||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
@@ -106,6 +118,164 @@ function Protect-Secrets {
|
|||||||
return $Text
|
return $Text
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- Additional platform arguments ---
|
||||||
|
$script:V8OwnedKeys = @(
|
||||||
|
'DESIGNER', 'ENTERPRISE', 'CREATEINFOBASE', 'CONFIG',
|
||||||
|
'/F', '/S', '/N', '/P', '/Out', '/DisableStartupDialogs',
|
||||||
|
'/UseTemplate', '/AddToList', '/Execute', '/C', '/URL', '/UC',
|
||||||
|
'/DumpIB', '/RestoreIB', '/DumpCfg', '/LoadCfg',
|
||||||
|
'/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg',
|
||||||
|
'/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles'
|
||||||
|
)
|
||||||
|
$script:IbcmdOwnedKeys = @(
|
||||||
|
'--db-path', '--data', '--out', '--file', '--load', '--restore',
|
||||||
|
'--import', '--export', '--apply', '--force', '--create-database',
|
||||||
|
'--user', '--password'
|
||||||
|
)
|
||||||
|
$script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP')
|
||||||
|
$script:IbcmdSecretKeys = @('--password', '--token', '--db-pwd')
|
||||||
|
|
||||||
|
function Test-ArgKeyMatch {
|
||||||
|
# A token matches a key when it equals the key, or starts with it and the next
|
||||||
|
# character is not a letter — catches glued /N"user" and --password=x, while
|
||||||
|
# keeping /ClearCache distinct from /C.
|
||||||
|
param([string]$Token, [string]$Key)
|
||||||
|
if ($Token.Length -lt $Key.Length) { return $false }
|
||||||
|
if (-not $Token.Substring(0, $Key.Length).Equals($Key, [System.StringComparison]::OrdinalIgnoreCase)) { return $false }
|
||||||
|
if ($Token.Length -eq $Key.Length) { return $true }
|
||||||
|
return -not [char]::IsLetter($Token[$Key.Length])
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-ProjectExtraArgs {
|
||||||
|
# v8args / ibcmdargs from .v8-project.json — same upward walk as v8path.
|
||||||
|
param([string]$Name)
|
||||||
|
$dir = (Get-Location).Path
|
||||||
|
while ($dir) {
|
||||||
|
$pf = Join-Path $dir ".v8-project.json"
|
||||||
|
if (Test-Path $pf) {
|
||||||
|
try {
|
||||||
|
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||||
|
if ($j.$Name) { return @($j.$Name | ForEach-Object { [string]$_ }) }
|
||||||
|
} catch {}
|
||||||
|
return @()
|
||||||
|
}
|
||||||
|
$parent = Split-Path $dir -Parent
|
||||||
|
if (-not $parent -or $parent -eq $dir) { break }
|
||||||
|
$dir = $parent
|
||||||
|
}
|
||||||
|
return @()
|
||||||
|
}
|
||||||
|
|
||||||
|
function Assert-ExtraArgs {
|
||||||
|
# The platform accepts only one batch operation, and a duplicate connection or
|
||||||
|
# output key fails with an opaque 1C error — reject what the skill owns itself.
|
||||||
|
param([string[]]$ExtraArgs, [string]$Engine, [hashtable]$Hints)
|
||||||
|
$paramName = if ($Engine -eq 'ibcmd') { '-AdditionalIbcmdArguments' } else { '-AdditionalV8Arguments' }
|
||||||
|
$owned = if ($Engine -eq 'ibcmd') { $script:IbcmdOwnedKeys } else { $script:V8OwnedKeys }
|
||||||
|
foreach ($tok in $ExtraArgs) {
|
||||||
|
if ($Engine -eq 'ibcmd' -and $tok -notmatch '^-') {
|
||||||
|
Write-Host "Error: '$tok' is a positional token — pass values as --key=value ($paramName cannot extend the ibcmd command)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
foreach ($k in $owned) {
|
||||||
|
if (Test-ArgKeyMatch $tok $k) {
|
||||||
|
$hint = ''
|
||||||
|
if ($Hints -and $Hints.ContainsKey($k)) { $hint = " (use $($Hints[$k]))" }
|
||||||
|
Write-Host "Error: $k is controlled by the skill and cannot be passed via $paramName$hint" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Resolve-ExtraArgs {
|
||||||
|
# Pick the argument list for the selected engine and validate it. An explicitly passed
|
||||||
|
# parameter for the other engine is an error; the same keys coming from .v8-project.json
|
||||||
|
# simply do not apply — a project may describe both engines.
|
||||||
|
param([string]$Engine, [string[]]$V8Extra, [string[]]$IbcmdExtra, [hashtable]$Hints)
|
||||||
|
# powershell.exe -File — how skills are invoked — cannot bind an array parameter:
|
||||||
|
# space-separated values spill into positional ones, a comma-joined list arrives as a
|
||||||
|
# single token. So accept the repo's list convention (comma-separated) and split here;
|
||||||
|
# a native array call keeps working. A value containing a comma is not supported.
|
||||||
|
$V8Extra = @($V8Extra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
|
||||||
|
$IbcmdExtra = @($IbcmdExtra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
|
||||||
|
if ($Engine -eq 'ibcmd' -and $V8Extra.Count -gt 0) {
|
||||||
|
Write-Host "Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd (use -AdditionalIbcmdArguments)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
if ($Engine -ne 'ibcmd' -and $IbcmdExtra.Count -gt 0) {
|
||||||
|
Write-Host "Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 (use -AdditionalV8Arguments)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
if ($Engine -eq 'ibcmd') {
|
||||||
|
$extra = @(Get-ProjectExtraArgs 'ibcmdargs') + @($IbcmdExtra)
|
||||||
|
} else {
|
||||||
|
$extra = @(Get-ProjectExtraArgs 'v8args') + @($V8Extra)
|
||||||
|
}
|
||||||
|
if ($extra.Count -gt 0) { Assert-ExtraArgs $extra $Engine $Hints }
|
||||||
|
# Plain return, no comma trick: the caller re-collects with @(...), and ,@() there
|
||||||
|
# would nest the array — the tokens would then be glued into one argument.
|
||||||
|
return $extra
|
||||||
|
}
|
||||||
|
|
||||||
|
function Format-ArgsForDisplay {
|
||||||
|
# Redact values of secret-prone keys in glued, =-joined and separate forms.
|
||||||
|
# Matching here is a plain prefix (no letter rule): over-masking costs nothing,
|
||||||
|
# a leaked password does.
|
||||||
|
param([string[]]$ArgList, [string]$Engine)
|
||||||
|
$keys = if ($Engine -eq 'ibcmd') { $script:IbcmdSecretKeys } else { $script:V8SecretKeys }
|
||||||
|
$res = @()
|
||||||
|
$maskNext = $false
|
||||||
|
foreach ($tok in $ArgList) {
|
||||||
|
if ($maskNext) { $res += '***'; $maskNext = $false; continue }
|
||||||
|
$hit = $null
|
||||||
|
foreach ($k in $keys) {
|
||||||
|
if ($tok.Length -ge $k.Length -and $tok.Substring(0, $k.Length).Equals($k, [System.StringComparison]::OrdinalIgnoreCase)) { $hit = $k; break }
|
||||||
|
}
|
||||||
|
if (-not $hit) { $res += $tok; continue }
|
||||||
|
if ($tok.Length -eq $hit.Length) { $res += $tok; $maskNext = $true }
|
||||||
|
elseif ($tok[$hit.Length] -eq '=') { $res += ($hit + '=***') }
|
||||||
|
else { $res += ($hit + '***') }
|
||||||
|
}
|
||||||
|
return ,$res
|
||||||
|
}
|
||||||
|
|
||||||
|
function ConvertTo-CleanPath {
|
||||||
|
# Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
|
||||||
|
# surrounding quotes that survived shell parsing, a trailing separator. A quote left
|
||||||
|
# inside afterwards cannot be part of a real path — reject it by name instead of letting
|
||||||
|
# 1C answer with its opaque "Неверные или отсутствующие параметры соединения".
|
||||||
|
param([string]$Value, [string]$ParamName)
|
||||||
|
if (-not $Value) { return $Value }
|
||||||
|
$v = $Value.Trim()
|
||||||
|
if ($v.Length -ge 2 -and $v[0] -eq $v[-1] -and ($v[0] -eq '"' -or $v[0] -eq "'")) {
|
||||||
|
$v = $v.Substring(1, $v.Length - 2).Trim()
|
||||||
|
}
|
||||||
|
if ($v.Length -gt 3 -and ($v[-1] -eq '\' -or $v[-1] -eq '/')) { $v = $v.Substring(0, $v.Length - 1) }
|
||||||
|
if ($v.Contains('"')) {
|
||||||
|
Write-Host "Error: $ParamName contains a quote character: $Value" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
return $v
|
||||||
|
}
|
||||||
|
|
||||||
|
$V8Path = ConvertTo-CleanPath $V8Path '-V8Path'
|
||||||
|
$InfoBasePath = ConvertTo-CleanPath $InfoBasePath '-InfoBasePath'
|
||||||
|
$ConfigDir = ConvertTo-CleanPath $ConfigDir '-ConfigDir'
|
||||||
|
|
||||||
|
function Assert-InfoBaseExists {
|
||||||
|
# These skills work on a ready infobase. Saying so up front beats the platform's
|
||||||
|
# "Неверные или отсутствующие параметры соединения" after a launch.
|
||||||
|
param([string]$Path)
|
||||||
|
if (-not $Path) { return }
|
||||||
|
if (-not (Test-Path (Join-Path $Path "1Cv8.1CD"))) {
|
||||||
|
Write-Host "Error: information base not found at $Path (no 1Cv8.1CD)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert-InfoBaseExists $InfoBasePath
|
||||||
|
|
||||||
# --- Resolve V8Path ---
|
# --- Resolve V8Path ---
|
||||||
function Find-ProjectV8Path {
|
function Find-ProjectV8Path {
|
||||||
$dir = (Get-Location).Path
|
$dir = (Get-Location).Path
|
||||||
@@ -150,32 +320,75 @@ if (-not (Test-Path $V8Path)) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
|
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
|
||||||
function Invoke-IbcmdProcess {
|
function ConvertFrom-PlatformBytes {
|
||||||
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
|
# ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
|
||||||
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
|
# OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes — guessing
|
||||||
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
|
# one of them outright mangles Cyrillic.
|
||||||
param([string]$Exe, [string[]]$IbArgs)
|
param([byte[]]$Bytes)
|
||||||
|
if (-not $Bytes -or $Bytes.Length -eq 0) { return '' }
|
||||||
|
try {
|
||||||
|
$strict = New-Object System.Text.UTF8Encoding($false, $true)
|
||||||
|
return $strict.GetString($Bytes)
|
||||||
|
} catch {
|
||||||
|
return [System.Text.Encoding]::GetEncoding(866).GetString($Bytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-PlatformProcess {
|
||||||
|
# Run the platform non-interactively and capture its console output. A closed stdin pipe
|
||||||
|
# (EOF) makes an auth prompt fast-fail instead of hanging; capturing keeps the child's
|
||||||
|
# text out of our stream until we print it labelled (and out of the wrong encoding).
|
||||||
|
# Returns @{ Output; ExitCode }.
|
||||||
|
#
|
||||||
|
# Quoting differs by engine, so the caller says which it built:
|
||||||
|
# ibcmd — tokens are bare (--db-path=C:\a b), the whole token gets quoted here;
|
||||||
|
# 1cv8 — -PreQuoted: the caller already put quotes inside the token (File="C:\a b"),
|
||||||
|
# which is where 1C's own parser expects them; quoting again breaks the value.
|
||||||
|
param([string]$Exe, [string[]]$ProcArgs, [switch]$PreQuoted)
|
||||||
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
||||||
$psi.FileName = $Exe
|
$psi.FileName = $Exe
|
||||||
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
|
$psi.Arguments = if ($PreQuoted) {
|
||||||
|
$ProcArgs -join ' '
|
||||||
|
} else {
|
||||||
|
($ProcArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
|
||||||
|
}
|
||||||
$psi.UseShellExecute = $false
|
$psi.UseShellExecute = $false
|
||||||
$psi.CreateNoWindow = $true
|
$psi.CreateNoWindow = $true
|
||||||
$psi.RedirectStandardInput = $true
|
$psi.RedirectStandardInput = $true
|
||||||
$psi.RedirectStandardOutput = $true
|
$psi.RedirectStandardOutput = $true
|
||||||
$psi.RedirectStandardError = $true
|
$psi.RedirectStandardError = $true
|
||||||
try {
|
|
||||||
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
|
|
||||||
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
|
|
||||||
} catch {}
|
|
||||||
$p = [System.Diagnostics.Process]::Start($psi)
|
$p = [System.Diagnostics.Process]::Start($psi)
|
||||||
$p.StandardInput.Close()
|
$p.StandardInput.Close()
|
||||||
$out = $p.StandardOutput.ReadToEnd()
|
# stderr is drained in parallel: reading the streams one after another deadlocks
|
||||||
$err = $p.StandardError.ReadToEnd()
|
# as soon as the other one fills its pipe buffer.
|
||||||
|
$errMs = New-Object System.IO.MemoryStream
|
||||||
|
$errTask = $p.StandardError.BaseStream.CopyToAsync($errMs)
|
||||||
|
$outMs = New-Object System.IO.MemoryStream
|
||||||
|
$p.StandardOutput.BaseStream.CopyTo($outMs)
|
||||||
|
$errTask.Wait()
|
||||||
$p.WaitForExit()
|
$p.WaitForExit()
|
||||||
|
$out = ConvertFrom-PlatformBytes $outMs.ToArray()
|
||||||
|
$err = ConvertFrom-PlatformBytes $errMs.ToArray()
|
||||||
if ($err) { $out += $err }
|
if ($err) { $out += $err }
|
||||||
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
|
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Write-PlatformOutput {
|
||||||
|
# Print what the platform wrote to the console as its own labelled block. Silence stays
|
||||||
|
# silent: in batch mode 1cv8 reports through /Out and prints nothing here.
|
||||||
|
param([string]$Text)
|
||||||
|
if (-not $Text) { return }
|
||||||
|
$t = $Text.TrimEnd()
|
||||||
|
if (-not $t) { return }
|
||||||
|
$limit = 65536
|
||||||
|
if ($t.Length -gt $limit) {
|
||||||
|
$t = "[... обрезано, показаны последние $limit символов ...]`r`n" + $t.Substring($t.Length - $limit)
|
||||||
|
}
|
||||||
|
Write-Host "--- Вывод платформы ---"
|
||||||
|
Write-Host $t
|
||||||
|
Write-Host "--- End ---"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function Test-DirNonEmpty {
|
function Test-DirNonEmpty {
|
||||||
# Postcondition: the platform must have written files into the output directory.
|
# Postcondition: the platform must have written files into the output directory.
|
||||||
@@ -186,6 +399,10 @@ function Test-DirNonEmpty {
|
|||||||
|
|
||||||
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
||||||
|
|
||||||
|
# --- Resolve additional arguments for the selected engine ---
|
||||||
|
$argHints = @{ '/F' = '-InfoBasePath'; '/S' = '-InfoBaseServer + -InfoBaseRef'; '/N' = '-UserName'; '/P' = '-Password'; '--db-path' = '-InfoBasePath'; '--user' = '-UserName'; '--password' = '-Password' }
|
||||||
|
$extraArgs = @(Resolve-ExtraArgs $engine $AdditionalV8Arguments $AdditionalIbcmdArguments $argHints)
|
||||||
|
|
||||||
# --- Validate connection ---
|
# --- Validate connection ---
|
||||||
if ($engine -eq "ibcmd") {
|
if ($engine -eq "ibcmd") {
|
||||||
if (-not $InfoBasePath) {
|
if (-not $InfoBasePath) {
|
||||||
@@ -238,8 +455,9 @@ try {
|
|||||||
if ($UserName) { $arguments += "--user=$UserName" }
|
if ($UserName) { $arguments += "--user=$UserName" }
|
||||||
if ($Password) { $arguments += "--password=$Password" }
|
if ($Password) { $arguments += "--password=$Password" }
|
||||||
$arguments += "--data=$tempDir"
|
$arguments += "--data=$tempDir"
|
||||||
Write-Host "Running: ibcmd $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))"
|
$arguments += $extraArgs
|
||||||
$__ib = Invoke-IbcmdProcess $V8Path $arguments
|
Write-Host "Running: ibcmd $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
|
||||||
|
$__ib = Invoke-PlatformProcess $V8Path $arguments
|
||||||
$output = $__ib.Output
|
$output = $__ib.Output
|
||||||
$exitCode = $__ib.ExitCode
|
$exitCode = $__ib.ExitCode
|
||||||
$outMissing = ($exitCode -eq 0) -and -not (Test-DirNonEmpty $ConfigDir)
|
$outMissing = ($exitCode -eq 0) -and -not (Test-DirNonEmpty $ConfigDir)
|
||||||
@@ -251,7 +469,7 @@ try {
|
|||||||
} else {
|
} else {
|
||||||
Write-Host "Error exporting configuration (code: $exitCode)" -ForegroundColor Red
|
Write-Host "Error exporting configuration (code: $exitCode)" -ForegroundColor Red
|
||||||
}
|
}
|
||||||
if ($output) { Write-Host ($output | Out-String) }
|
Write-PlatformOutput $output
|
||||||
exit $exitCode
|
exit $exitCode
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -309,11 +527,12 @@ try {
|
|||||||
$outFile = Join-Path $tempDir "dump_log.txt"
|
$outFile = Join-Path $tempDir "dump_log.txt"
|
||||||
$arguments += "/Out", "`"$outFile`""
|
$arguments += "/Out", "`"$outFile`""
|
||||||
$arguments += "/DisableStartupDialogs"
|
$arguments += "/DisableStartupDialogs"
|
||||||
|
$arguments += $extraArgs
|
||||||
|
|
||||||
# --- Execute ---
|
# --- Execute ---
|
||||||
Write-Host "Running: 1cv8.exe $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))"
|
Write-Host "Running: 1cv8.exe $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
|
||||||
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
|
$__v8 = Invoke-PlatformProcess $V8Path $arguments -PreQuoted
|
||||||
$exitCode = $process.ExitCode
|
$exitCode = $__v8.ExitCode
|
||||||
|
|
||||||
# --- Result ---
|
# --- Result ---
|
||||||
# Postcondition: exit 0 with an empty output directory is a false success.
|
# Postcondition: exit 0 with an empty output directory is a false success.
|
||||||
@@ -336,6 +555,7 @@ try {
|
|||||||
Write-Host "--- End ---"
|
Write-Host "--- End ---"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Write-PlatformOutput $__v8.Output
|
||||||
|
|
||||||
exit $exitCode
|
exit $exitCode
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# db-dump-xml v1.11 — Dump 1C configuration to XML files
|
# db-dump-xml v1.15 — Dump 1C configuration to XML files
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -14,6 +14,28 @@ import subprocess
|
|||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
|
# Регистронезависимый ввод — паритет с 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)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def _find_project_v8path():
|
def _find_project_v8path():
|
||||||
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
||||||
@@ -36,6 +58,163 @@ def _find_project_v8path():
|
|||||||
d = parent
|
d = parent
|
||||||
|
|
||||||
|
|
||||||
|
# --- Additional platform arguments ---
|
||||||
|
V8_OWNED_KEYS = [
|
||||||
|
"DESIGNER", "ENTERPRISE", "CREATEINFOBASE", "CONFIG",
|
||||||
|
"/F", "/S", "/N", "/P", "/Out", "/DisableStartupDialogs",
|
||||||
|
"/UseTemplate", "/AddToList", "/Execute", "/C", "/URL", "/UC",
|
||||||
|
"/DumpIB", "/RestoreIB", "/DumpCfg", "/LoadCfg",
|
||||||
|
"/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg",
|
||||||
|
"/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles",
|
||||||
|
]
|
||||||
|
IBCMD_OWNED_KEYS = [
|
||||||
|
"--db-path", "--data", "--out", "--file", "--load", "--restore",
|
||||||
|
"--import", "--export", "--apply", "--force", "--create-database",
|
||||||
|
"--user", "--password",
|
||||||
|
]
|
||||||
|
V8_SECRET_KEYS = ["/P", "/UC", "/WSP", "/AWSP"]
|
||||||
|
IBCMD_SECRET_KEYS = ["--password", "--token", "--db-pwd"]
|
||||||
|
|
||||||
|
|
||||||
|
def arg_key_match(token, key):
|
||||||
|
"""Token matches a key when it equals it, or starts with it and the next character
|
||||||
|
is not a letter — catches glued /N"user" and --password=x, while keeping
|
||||||
|
/ClearCache distinct from /C."""
|
||||||
|
if len(token) < len(key):
|
||||||
|
return False
|
||||||
|
if token[: len(key)].lower() != key.lower():
|
||||||
|
return False
|
||||||
|
if len(token) == len(key):
|
||||||
|
return True
|
||||||
|
return not token[len(key)].isalpha()
|
||||||
|
|
||||||
|
|
||||||
|
def project_extra_args(name):
|
||||||
|
"""v8args / ibcmdargs from .v8-project.json — same upward walk as v8path."""
|
||||||
|
d = os.getcwd()
|
||||||
|
while True:
|
||||||
|
pf = os.path.join(d, ".v8-project.json")
|
||||||
|
if os.path.isfile(pf):
|
||||||
|
try:
|
||||||
|
with open(pf, encoding="utf-8-sig") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
v = data.get(name)
|
||||||
|
if v:
|
||||||
|
return [str(x) for x in v]
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return []
|
||||||
|
parent = os.path.dirname(d)
|
||||||
|
if parent == d:
|
||||||
|
return []
|
||||||
|
d = parent
|
||||||
|
|
||||||
|
|
||||||
|
def assert_extra_args(extra, engine, hints):
|
||||||
|
"""The platform accepts only one batch operation, and a duplicate connection or
|
||||||
|
output key fails with an opaque 1C error — reject what the skill owns itself."""
|
||||||
|
param = "-AdditionalIbcmdArguments" if engine == "ibcmd" else "-AdditionalV8Arguments"
|
||||||
|
owned = IBCMD_OWNED_KEYS if engine == "ibcmd" else V8_OWNED_KEYS
|
||||||
|
for tok in extra:
|
||||||
|
if engine == "ibcmd" and not tok.startswith("-"):
|
||||||
|
print(
|
||||||
|
f"Error: '{tok}' is a positional token — pass values as --key=value "
|
||||||
|
f"({param} cannot extend the ibcmd command)",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
for k in owned:
|
||||||
|
if arg_key_match(tok, k):
|
||||||
|
hint = f" (use {hints[k]})" if hints and k in hints else ""
|
||||||
|
print(
|
||||||
|
f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def format_args_for_display(arglist, engine):
|
||||||
|
"""Redact values of secret-prone keys in glued, =-joined and separate forms.
|
||||||
|
Matching here is a plain prefix (no letter rule): over-masking costs nothing,
|
||||||
|
a leaked password does."""
|
||||||
|
keys = IBCMD_SECRET_KEYS if engine == "ibcmd" else V8_SECRET_KEYS
|
||||||
|
res = []
|
||||||
|
mask_next = False
|
||||||
|
for tok in arglist:
|
||||||
|
if mask_next:
|
||||||
|
res.append("***")
|
||||||
|
mask_next = False
|
||||||
|
continue
|
||||||
|
hit = None
|
||||||
|
for k in keys:
|
||||||
|
if tok[: len(k)].lower() == k.lower():
|
||||||
|
hit = k
|
||||||
|
break
|
||||||
|
if hit is None:
|
||||||
|
res.append(tok)
|
||||||
|
elif len(tok) == len(hit):
|
||||||
|
res.append(tok)
|
||||||
|
mask_next = True
|
||||||
|
elif tok[len(hit)] == "=":
|
||||||
|
res.append(hit + "=***")
|
||||||
|
else:
|
||||||
|
res.append(hit + "***")
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
def extract_extra_args(argv, known_opts):
|
||||||
|
"""argparse refuses values that start with '-' (every ibcmd key does), so pull the two
|
||||||
|
escape-hatch lists out of argv by hand: after the flag, take everything up to the next
|
||||||
|
declared skill option. Returns (remaining_argv, v8_extra, ibcmd_extra)."""
|
||||||
|
rest, v8, ibcmd = [], [], []
|
||||||
|
i = 0
|
||||||
|
while i < len(argv):
|
||||||
|
low = argv[i].lower()
|
||||||
|
if low in ("-additionalv8arguments", "-additionalibcmdarguments"):
|
||||||
|
target = v8 if low == "-additionalv8arguments" else ibcmd
|
||||||
|
i += 1
|
||||||
|
while i < len(argv) and argv[i].lower() not in known_opts:
|
||||||
|
target.append(argv[i])
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
rest.append(argv[i])
|
||||||
|
i += 1
|
||||||
|
return rest, v8, ibcmd
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints):
|
||||||
|
"""Pick the argument list for the selected engine and validate it. An explicitly
|
||||||
|
passed parameter for the other engine is an error; the same keys coming from
|
||||||
|
.v8-project.json simply do not apply — a project may describe both engines.
|
||||||
|
|
||||||
|
Comma-separated elements are split apart: PowerShell's -File cannot bind an array,
|
||||||
|
so that form is the documented one and both ports must accept it. A value containing
|
||||||
|
a comma is not supported."""
|
||||||
|
v8_extra = [p for tok in v8_extra for p in str(tok).split(",") if p]
|
||||||
|
ibcmd_extra = [p for tok in ibcmd_extra for p in str(tok).split(",") if p]
|
||||||
|
if engine == "ibcmd" and v8_extra:
|
||||||
|
print(
|
||||||
|
"Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd "
|
||||||
|
"(use -AdditionalIbcmdArguments)",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
if engine != "ibcmd" and ibcmd_extra:
|
||||||
|
print(
|
||||||
|
"Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 "
|
||||||
|
"(use -AdditionalV8Arguments)",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
if engine == "ibcmd":
|
||||||
|
extra = project_extra_args("ibcmdargs") + list(ibcmd_extra)
|
||||||
|
else:
|
||||||
|
extra = project_extra_args("v8args") + list(v8_extra)
|
||||||
|
if extra:
|
||||||
|
assert_extra_args(extra, engine, hints)
|
||||||
|
return extra
|
||||||
|
|
||||||
|
|
||||||
def _version_dir(p):
|
def _version_dir(p):
|
||||||
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
|
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
|
||||||
parent = os.path.dirname(p)
|
parent = os.path.dirname(p)
|
||||||
@@ -86,6 +265,85 @@ IBCMD_NOUSER_HINT = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def decode_platform_bytes(data):
|
||||||
|
"""ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
|
||||||
|
OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes — the locale
|
||||||
|
code page (what text=True uses) mangles both."""
|
||||||
|
if not data:
|
||||||
|
return ""
|
||||||
|
try:
|
||||||
|
return data.decode("utf-8")
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
return data.decode("cp866", errors="replace")
|
||||||
|
|
||||||
|
|
||||||
|
def assert_infobase_exists(path):
|
||||||
|
"""These skills work on a ready infobase. Saying so up front beats the platform's
|
||||||
|
"Неверные или отсутствующие параметры соединения" after a launch."""
|
||||||
|
if not path:
|
||||||
|
return
|
||||||
|
if not os.path.isfile(os.path.join(path, "1Cv8.1CD")):
|
||||||
|
print(f"Error: information base not found at {path} (no 1Cv8.1CD)", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def clean_path(value, param=""):
|
||||||
|
"""Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
|
||||||
|
surrounding quotes that survived shell parsing, a trailing separator. A quote left
|
||||||
|
inside afterwards cannot be part of a real path — reject it by name instead of letting
|
||||||
|
1C answer with its opaque "Неверные или отсутствующие параметры соединения"."""
|
||||||
|
if not value:
|
||||||
|
return value
|
||||||
|
v = value.strip()
|
||||||
|
if len(v) >= 2 and v[0] == v[-1] and v[0] in "\"'":
|
||||||
|
v = v[1:-1].strip()
|
||||||
|
if len(v) > 3 and v[-1] in "\\/":
|
||||||
|
v = v[:-1]
|
||||||
|
if '"' in v:
|
||||||
|
print(f"Error: {param or 'path'} contains a quote character: {value}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
return v
|
||||||
|
|
||||||
|
|
||||||
|
def quote_if_needed(token):
|
||||||
|
"""Extra arguments come from the caller unquoted; the 1cv8 command line is joined
|
||||||
|
verbatim, so a token with a space needs quotes of its own."""
|
||||||
|
if token and (" " in token or "\t" in token) and '"' not in token:
|
||||||
|
return f'"{token}"'
|
||||||
|
return token
|
||||||
|
|
||||||
|
|
||||||
|
def run_v8(v8path, arguments):
|
||||||
|
"""Run 1cv8 in batch mode and capture its console output.
|
||||||
|
|
||||||
|
The arguments carry their own quotes inside the value (File="C:\\a b") — that is where
|
||||||
|
1C's parser expects them, on Windows and on *nix alike. Windows list2cmdline would
|
||||||
|
escape those quotes, so there the command line is handed over ready-made.
|
||||||
|
"""
|
||||||
|
if os.name == "nt":
|
||||||
|
cmd = '"' + v8path + '" ' + " ".join(arguments)
|
||||||
|
else:
|
||||||
|
cmd = [v8path] + arguments
|
||||||
|
r = subprocess.run(cmd, input=b"", capture_output=True)
|
||||||
|
r.stdout = decode_platform_bytes(r.stdout)
|
||||||
|
r.stderr = decode_platform_bytes(r.stderr)
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
|
def print_platform_output(result):
|
||||||
|
"""Print what the platform wrote to the console as its own labelled block. Silence stays
|
||||||
|
silent: in batch mode 1cv8 reports through /Out and prints nothing here."""
|
||||||
|
text = ((result.stdout or "") + (result.stderr or "")).rstrip()
|
||||||
|
if not text:
|
||||||
|
return
|
||||||
|
limit = 65536
|
||||||
|
if len(text) > limit:
|
||||||
|
text = f"[... обрезано, показаны последние {limit} символов ...]\n" + text[-limit:]
|
||||||
|
print("--- Вывод платформы ---")
|
||||||
|
print(text)
|
||||||
|
print("--- End ---")
|
||||||
|
|
||||||
|
|
||||||
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
||||||
"""Run an ibcmd command non-interactively.
|
"""Run an ibcmd command non-interactively.
|
||||||
|
|
||||||
@@ -96,7 +354,10 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
|||||||
if warn_no_user and os.name == "nt" and not has_username:
|
if warn_no_user and os.name == "nt" and not has_username:
|
||||||
sys.stderr.write(IBCMD_NOUSER_HINT)
|
sys.stderr.write(IBCMD_NOUSER_HINT)
|
||||||
sys.stderr.flush()
|
sys.stderr.flush()
|
||||||
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
|
r = subprocess.run(cmd, input=b"", capture_output=True)
|
||||||
|
r.stdout = decode_platform_bytes(r.stdout)
|
||||||
|
r.stderr = decode_platform_bytes(r.stderr)
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
def dir_nonempty(path):
|
def dir_nonempty(path):
|
||||||
@@ -143,12 +404,35 @@ def main():
|
|||||||
choices=["Hierarchical", "Plain"],
|
choices=["Hierarchical", "Plain"],
|
||||||
help="Dump format (default: Hierarchical)",
|
help="Dump format (default: Hierarchical)",
|
||||||
)
|
)
|
||||||
args = parser.parse_args()
|
parser.add_argument("-AdditionalV8Arguments", nargs="*", default=[],
|
||||||
|
help="Extra 1cv8 arguments, e.g. /UseHwLicenses+")
|
||||||
|
parser.add_argument("-AdditionalIbcmdArguments", nargs="*", default=[],
|
||||||
|
help="Extra ibcmd arguments in --key=value form")
|
||||||
|
known_opts = {s.lower() for a in parser._actions for s in a.option_strings}
|
||||||
|
argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts)
|
||||||
|
args = ci_parse_args(parser, argv)
|
||||||
|
|
||||||
|
args.V8Path = clean_path(args.V8Path, "-V8Path")
|
||||||
|
args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath")
|
||||||
|
assert_infobase_exists(args.InfoBasePath)
|
||||||
|
args.ConfigDir = clean_path(args.ConfigDir, "-ConfigDir")
|
||||||
|
|
||||||
# --- Resolve V8Path ---
|
# --- Resolve V8Path ---
|
||||||
v8path = resolve_v8path(args.V8Path)
|
v8path = resolve_v8path(args.V8Path)
|
||||||
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
|
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
|
||||||
|
|
||||||
|
# --- Resolve additional arguments for the selected engine ---
|
||||||
|
arg_hints = {
|
||||||
|
"/F": "-InfoBasePath",
|
||||||
|
"/S": "-InfoBaseServer + -InfoBaseRef",
|
||||||
|
"/N": "-UserName",
|
||||||
|
"/P": "-Password",
|
||||||
|
"--db-path": "-InfoBasePath",
|
||||||
|
"--user": "-UserName",
|
||||||
|
"--password": "-Password",
|
||||||
|
}
|
||||||
|
extra_args = resolve_extra_args(engine, v8_extra, ibcmd_extra, arg_hints)
|
||||||
|
|
||||||
# --- Validate connection ---
|
# --- Validate connection ---
|
||||||
if engine == "ibcmd":
|
if engine == "ibcmd":
|
||||||
if not args.InfoBasePath:
|
if not args.InfoBasePath:
|
||||||
@@ -196,7 +480,8 @@ def main():
|
|||||||
if args.Password:
|
if args.Password:
|
||||||
arguments.append(f"--password={args.Password}")
|
arguments.append(f"--password={args.Password}")
|
||||||
arguments.append(f"--data={ib_data}")
|
arguments.append(f"--data={ib_data}")
|
||||||
print(f"Running: ibcmd {_redact(' '.join(arguments), args.Password, args.UserName)}")
|
arguments.extend(extra_args)
|
||||||
|
print(f"Running: ibcmd {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
|
||||||
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
|
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
|
||||||
exit_code = result.returncode
|
exit_code = result.returncode
|
||||||
out_missing = exit_code == 0 and not dir_nonempty(args.ConfigDir)
|
out_missing = exit_code == 0 and not dir_nonempty(args.ConfigDir)
|
||||||
@@ -208,10 +493,6 @@ def main():
|
|||||||
print(f"Error: exit code 0 but no files under {args.ConfigDir} — configuration was not exported", file=sys.stderr)
|
print(f"Error: exit code 0 but no files under {args.ConfigDir} — configuration was not exported", file=sys.stderr)
|
||||||
else:
|
else:
|
||||||
print(f"Error exporting configuration (code: {exit_code})", file=sys.stderr)
|
print(f"Error exporting configuration (code: {exit_code})", file=sys.stderr)
|
||||||
if result.stdout:
|
|
||||||
print(result.stdout)
|
|
||||||
if result.stderr:
|
|
||||||
print(result.stderr, file=sys.stderr)
|
|
||||||
sys.exit(exit_code)
|
sys.exit(exit_code)
|
||||||
|
|
||||||
# --- Temp dir ---
|
# --- Temp dir ---
|
||||||
@@ -223,16 +504,16 @@ def main():
|
|||||||
arguments = ["DESIGNER"]
|
arguments = ["DESIGNER"]
|
||||||
|
|
||||||
if args.InfoBaseServer and args.InfoBaseRef:
|
if args.InfoBaseServer and args.InfoBaseRef:
|
||||||
arguments += ["/S", f"{args.InfoBaseServer}/{args.InfoBaseRef}"]
|
arguments += ["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"']
|
||||||
else:
|
else:
|
||||||
arguments += ["/F", args.InfoBasePath]
|
arguments += ["/F", f'"{args.InfoBasePath}"']
|
||||||
|
|
||||||
if args.UserName:
|
if args.UserName:
|
||||||
arguments.append(f"/N{args.UserName}")
|
arguments.append(f'/N"{args.UserName}"')
|
||||||
if args.Password:
|
if args.Password:
|
||||||
arguments.append(f"/P{args.Password}")
|
arguments.append(f'/P"{args.Password}"')
|
||||||
|
|
||||||
arguments += ["/DumpConfigToFiles", args.ConfigDir]
|
arguments += ["/DumpConfigToFiles", f'"{args.ConfigDir}"']
|
||||||
arguments += ["-Format", args.Format]
|
arguments += ["-Format", args.Format]
|
||||||
|
|
||||||
if args.Mode == "Full":
|
if args.Mode == "Full":
|
||||||
@@ -249,7 +530,7 @@ def main():
|
|||||||
with open(list_file, "w", encoding="utf-8-sig") as f:
|
with open(list_file, "w", encoding="utf-8-sig") as f:
|
||||||
f.write("\n".join(object_list))
|
f.write("\n".join(object_list))
|
||||||
|
|
||||||
arguments += ["-listFile", list_file]
|
arguments += ["-listFile", f'"{list_file}"']
|
||||||
print(f"Objects to dump: {len(object_list)}")
|
print(f"Objects to dump: {len(object_list)}")
|
||||||
for obj in object_list:
|
for obj in object_list:
|
||||||
print(f" {obj}")
|
print(f" {obj}")
|
||||||
@@ -259,22 +540,19 @@ def main():
|
|||||||
|
|
||||||
# --- Extensions ---
|
# --- Extensions ---
|
||||||
if args.Extension:
|
if args.Extension:
|
||||||
arguments += ["-Extension", args.Extension]
|
arguments += ["-Extension", f'"{args.Extension}"']
|
||||||
elif args.AllExtensions:
|
elif args.AllExtensions:
|
||||||
arguments.append("-AllExtensions")
|
arguments.append("-AllExtensions")
|
||||||
|
|
||||||
# --- Output ---
|
# --- Output ---
|
||||||
out_file = os.path.join(temp_dir, "dump_log.txt")
|
out_file = os.path.join(temp_dir, "dump_log.txt")
|
||||||
arguments += ["/Out", out_file]
|
arguments += ["/Out", f'"{out_file}"']
|
||||||
arguments.append("/DisableStartupDialogs")
|
arguments.append("/DisableStartupDialogs")
|
||||||
|
arguments.extend(quote_if_needed(a) for a in extra_args)
|
||||||
|
|
||||||
# --- Execute ---
|
# --- Execute ---
|
||||||
print(f"Running: 1cv8.exe {_redact(' '.join(arguments), args.Password, args.UserName)}")
|
print(f"Running: 1cv8.exe {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
|
||||||
result = subprocess.run(
|
result = run_v8(v8path, arguments)
|
||||||
[v8path] + arguments,
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
)
|
|
||||||
exit_code = result.returncode
|
exit_code = result.returncode
|
||||||
|
|
||||||
# --- Result ---
|
# --- Result ---
|
||||||
@@ -301,6 +579,7 @@ def main():
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
print_platform_output(result)
|
||||||
sys.exit(exit_code)
|
sys.exit(exit_code)
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ allowed-tools:
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"v8path": "C:\\Program Files\\1cv8\\8.3.25.1257\\bin",
|
"v8path": "C:\\Program Files\\1cv8\\8.3.25.1257\\bin",
|
||||||
|
"v8args": ["/UseHwLicenses+"],
|
||||||
"databases": [
|
"databases": [
|
||||||
{
|
{
|
||||||
"id": "dev",
|
"id": "dev",
|
||||||
@@ -61,6 +62,8 @@ allowed-tools:
|
|||||||
| Поле | Тип | Описание |
|
| Поле | Тип | Описание |
|
||||||
|------|-----|----------|
|
|------|-----|----------|
|
||||||
| `v8path` | string | Каталог bin платформы 1С. Необязательный — если не задан, автоопределение |
|
| `v8path` | string | Каталог bin платформы 1С. Необязательный — если не задан, автоопределение |
|
||||||
|
| `v8args` | array | Доп. аргументы запуска `1cv8.exe` для всех навыков, напр. `["/UseHwLicenses+"]` |
|
||||||
|
| `ibcmdargs` | array | Доп. аргументы `ibcmd` (форма `--ключ=значение`) |
|
||||||
| `databases` | array | Массив баз данных |
|
| `databases` | array | Массив баз данных |
|
||||||
| `default` | string | id базы по умолчанию |
|
| `default` | string | id базы по умолчанию |
|
||||||
|
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-cf.ps1" <п
|
|||||||
| `-InputFile <путь>` | да | Путь к CF-файлу |
|
| `-InputFile <путь>` | да | Путь к CF-файлу |
|
||||||
| `-Extension <имя>` | нет | Загрузить как расширение |
|
| `-Extension <имя>` | нет | Загрузить как расширение |
|
||||||
| `-AllExtensions` | нет | Загрузить все расширения из архива |
|
| `-AllExtensions` | нет | Загрузить все расширения из архива |
|
||||||
|
| `-AdditionalV8Arguments <список>` | нет | Доп. аргументы запуска `1cv8.exe` через запятую, напр. `/UseHwLicenses+` |
|
||||||
|
| `-AdditionalIbcmdArguments <список>` | нет | Доп. аргументы `ibcmd` через запятую, в форме `--ключ=значение` |
|
||||||
|
|
||||||
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
|
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# db-load-cf v1.10 — Load 1C configuration from CF file
|
# db-load-cf v1.14 — Load 1C configuration from CF file
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||||
<#
|
<#
|
||||||
@@ -36,6 +36,12 @@
|
|||||||
.PARAMETER AllExtensions
|
.PARAMETER AllExtensions
|
||||||
Загрузить все расширения из архива
|
Загрузить все расширения из архива
|
||||||
|
|
||||||
|
.PARAMETER AdditionalV8Arguments
|
||||||
|
Дополнительные аргументы запуска 1cv8.exe (например /UseHwLicenses+)
|
||||||
|
|
||||||
|
.PARAMETER AdditionalIbcmdArguments
|
||||||
|
Дополнительные аргументы запуска ibcmd (форма --ключ=значение)
|
||||||
|
|
||||||
.EXAMPLE
|
.EXAMPLE
|
||||||
.\db-load-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "config.cf"
|
.\db-load-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "config.cf"
|
||||||
|
|
||||||
@@ -70,7 +76,13 @@ param(
|
|||||||
[string]$Extension,
|
[string]$Extension,
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
[Parameter(Mandatory=$false)]
|
||||||
[switch]$AllExtensions
|
[switch]$AllExtensions,
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[string[]]$AdditionalV8Arguments = @(),
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[string[]]$AdditionalIbcmdArguments = @()
|
||||||
)
|
)
|
||||||
|
|
||||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
@@ -100,6 +112,164 @@ function Get-ExitAnnotation {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- Additional platform arguments ---
|
||||||
|
$script:V8OwnedKeys = @(
|
||||||
|
'DESIGNER', 'ENTERPRISE', 'CREATEINFOBASE', 'CONFIG',
|
||||||
|
'/F', '/S', '/N', '/P', '/Out', '/DisableStartupDialogs',
|
||||||
|
'/UseTemplate', '/AddToList', '/Execute', '/C', '/URL', '/UC',
|
||||||
|
'/DumpIB', '/RestoreIB', '/DumpCfg', '/LoadCfg',
|
||||||
|
'/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg',
|
||||||
|
'/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles'
|
||||||
|
)
|
||||||
|
$script:IbcmdOwnedKeys = @(
|
||||||
|
'--db-path', '--data', '--out', '--file', '--load', '--restore',
|
||||||
|
'--import', '--export', '--apply', '--force', '--create-database',
|
||||||
|
'--user', '--password'
|
||||||
|
)
|
||||||
|
$script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP')
|
||||||
|
$script:IbcmdSecretKeys = @('--password', '--token', '--db-pwd')
|
||||||
|
|
||||||
|
function Test-ArgKeyMatch {
|
||||||
|
# A token matches a key when it equals the key, or starts with it and the next
|
||||||
|
# character is not a letter — catches glued /N"user" and --password=x, while
|
||||||
|
# keeping /ClearCache distinct from /C.
|
||||||
|
param([string]$Token, [string]$Key)
|
||||||
|
if ($Token.Length -lt $Key.Length) { return $false }
|
||||||
|
if (-not $Token.Substring(0, $Key.Length).Equals($Key, [System.StringComparison]::OrdinalIgnoreCase)) { return $false }
|
||||||
|
if ($Token.Length -eq $Key.Length) { return $true }
|
||||||
|
return -not [char]::IsLetter($Token[$Key.Length])
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-ProjectExtraArgs {
|
||||||
|
# v8args / ibcmdargs from .v8-project.json — same upward walk as v8path.
|
||||||
|
param([string]$Name)
|
||||||
|
$dir = (Get-Location).Path
|
||||||
|
while ($dir) {
|
||||||
|
$pf = Join-Path $dir ".v8-project.json"
|
||||||
|
if (Test-Path $pf) {
|
||||||
|
try {
|
||||||
|
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||||
|
if ($j.$Name) { return @($j.$Name | ForEach-Object { [string]$_ }) }
|
||||||
|
} catch {}
|
||||||
|
return @()
|
||||||
|
}
|
||||||
|
$parent = Split-Path $dir -Parent
|
||||||
|
if (-not $parent -or $parent -eq $dir) { break }
|
||||||
|
$dir = $parent
|
||||||
|
}
|
||||||
|
return @()
|
||||||
|
}
|
||||||
|
|
||||||
|
function Assert-ExtraArgs {
|
||||||
|
# The platform accepts only one batch operation, and a duplicate connection or
|
||||||
|
# output key fails with an opaque 1C error — reject what the skill owns itself.
|
||||||
|
param([string[]]$ExtraArgs, [string]$Engine, [hashtable]$Hints)
|
||||||
|
$paramName = if ($Engine -eq 'ibcmd') { '-AdditionalIbcmdArguments' } else { '-AdditionalV8Arguments' }
|
||||||
|
$owned = if ($Engine -eq 'ibcmd') { $script:IbcmdOwnedKeys } else { $script:V8OwnedKeys }
|
||||||
|
foreach ($tok in $ExtraArgs) {
|
||||||
|
if ($Engine -eq 'ibcmd' -and $tok -notmatch '^-') {
|
||||||
|
Write-Host "Error: '$tok' is a positional token — pass values as --key=value ($paramName cannot extend the ibcmd command)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
foreach ($k in $owned) {
|
||||||
|
if (Test-ArgKeyMatch $tok $k) {
|
||||||
|
$hint = ''
|
||||||
|
if ($Hints -and $Hints.ContainsKey($k)) { $hint = " (use $($Hints[$k]))" }
|
||||||
|
Write-Host "Error: $k is controlled by the skill and cannot be passed via $paramName$hint" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Resolve-ExtraArgs {
|
||||||
|
# Pick the argument list for the selected engine and validate it. An explicitly passed
|
||||||
|
# parameter for the other engine is an error; the same keys coming from .v8-project.json
|
||||||
|
# simply do not apply — a project may describe both engines.
|
||||||
|
param([string]$Engine, [string[]]$V8Extra, [string[]]$IbcmdExtra, [hashtable]$Hints)
|
||||||
|
# powershell.exe -File — how skills are invoked — cannot bind an array parameter:
|
||||||
|
# space-separated values spill into positional ones, a comma-joined list arrives as a
|
||||||
|
# single token. So accept the repo's list convention (comma-separated) and split here;
|
||||||
|
# a native array call keeps working. A value containing a comma is not supported.
|
||||||
|
$V8Extra = @($V8Extra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
|
||||||
|
$IbcmdExtra = @($IbcmdExtra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
|
||||||
|
if ($Engine -eq 'ibcmd' -and $V8Extra.Count -gt 0) {
|
||||||
|
Write-Host "Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd (use -AdditionalIbcmdArguments)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
if ($Engine -ne 'ibcmd' -and $IbcmdExtra.Count -gt 0) {
|
||||||
|
Write-Host "Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 (use -AdditionalV8Arguments)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
if ($Engine -eq 'ibcmd') {
|
||||||
|
$extra = @(Get-ProjectExtraArgs 'ibcmdargs') + @($IbcmdExtra)
|
||||||
|
} else {
|
||||||
|
$extra = @(Get-ProjectExtraArgs 'v8args') + @($V8Extra)
|
||||||
|
}
|
||||||
|
if ($extra.Count -gt 0) { Assert-ExtraArgs $extra $Engine $Hints }
|
||||||
|
# Plain return, no comma trick: the caller re-collects with @(...), and ,@() there
|
||||||
|
# would nest the array — the tokens would then be glued into one argument.
|
||||||
|
return $extra
|
||||||
|
}
|
||||||
|
|
||||||
|
function Format-ArgsForDisplay {
|
||||||
|
# Redact values of secret-prone keys in glued, =-joined and separate forms.
|
||||||
|
# Matching here is a plain prefix (no letter rule): over-masking costs nothing,
|
||||||
|
# a leaked password does.
|
||||||
|
param([string[]]$ArgList, [string]$Engine)
|
||||||
|
$keys = if ($Engine -eq 'ibcmd') { $script:IbcmdSecretKeys } else { $script:V8SecretKeys }
|
||||||
|
$res = @()
|
||||||
|
$maskNext = $false
|
||||||
|
foreach ($tok in $ArgList) {
|
||||||
|
if ($maskNext) { $res += '***'; $maskNext = $false; continue }
|
||||||
|
$hit = $null
|
||||||
|
foreach ($k in $keys) {
|
||||||
|
if ($tok.Length -ge $k.Length -and $tok.Substring(0, $k.Length).Equals($k, [System.StringComparison]::OrdinalIgnoreCase)) { $hit = $k; break }
|
||||||
|
}
|
||||||
|
if (-not $hit) { $res += $tok; continue }
|
||||||
|
if ($tok.Length -eq $hit.Length) { $res += $tok; $maskNext = $true }
|
||||||
|
elseif ($tok[$hit.Length] -eq '=') { $res += ($hit + '=***') }
|
||||||
|
else { $res += ($hit + '***') }
|
||||||
|
}
|
||||||
|
return ,$res
|
||||||
|
}
|
||||||
|
|
||||||
|
function ConvertTo-CleanPath {
|
||||||
|
# Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
|
||||||
|
# surrounding quotes that survived shell parsing, a trailing separator. A quote left
|
||||||
|
# inside afterwards cannot be part of a real path — reject it by name instead of letting
|
||||||
|
# 1C answer with its opaque "Неверные или отсутствующие параметры соединения".
|
||||||
|
param([string]$Value, [string]$ParamName)
|
||||||
|
if (-not $Value) { return $Value }
|
||||||
|
$v = $Value.Trim()
|
||||||
|
if ($v.Length -ge 2 -and $v[0] -eq $v[-1] -and ($v[0] -eq '"' -or $v[0] -eq "'")) {
|
||||||
|
$v = $v.Substring(1, $v.Length - 2).Trim()
|
||||||
|
}
|
||||||
|
if ($v.Length -gt 3 -and ($v[-1] -eq '\' -or $v[-1] -eq '/')) { $v = $v.Substring(0, $v.Length - 1) }
|
||||||
|
if ($v.Contains('"')) {
|
||||||
|
Write-Host "Error: $ParamName contains a quote character: $Value" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
return $v
|
||||||
|
}
|
||||||
|
|
||||||
|
$V8Path = ConvertTo-CleanPath $V8Path '-V8Path'
|
||||||
|
$InfoBasePath = ConvertTo-CleanPath $InfoBasePath '-InfoBasePath'
|
||||||
|
$InputFile = ConvertTo-CleanPath $InputFile '-InputFile'
|
||||||
|
|
||||||
|
function Assert-InfoBaseExists {
|
||||||
|
# These skills work on a ready infobase. Saying so up front beats the platform's
|
||||||
|
# "Неверные или отсутствующие параметры соединения" after a launch.
|
||||||
|
param([string]$Path)
|
||||||
|
if (-not $Path) { return }
|
||||||
|
if (-not (Test-Path (Join-Path $Path "1Cv8.1CD"))) {
|
||||||
|
Write-Host "Error: information base not found at $Path (no 1Cv8.1CD)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert-InfoBaseExists $InfoBasePath
|
||||||
|
|
||||||
# --- Resolve V8Path ---
|
# --- Resolve V8Path ---
|
||||||
function Find-ProjectV8Path {
|
function Find-ProjectV8Path {
|
||||||
$dir = (Get-Location).Path
|
$dir = (Get-Location).Path
|
||||||
@@ -144,35 +314,82 @@ if (-not (Test-Path $V8Path)) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
|
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
|
||||||
function Invoke-IbcmdProcess {
|
function ConvertFrom-PlatformBytes {
|
||||||
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
|
# ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
|
||||||
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
|
# OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes — guessing
|
||||||
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
|
# one of them outright mangles Cyrillic.
|
||||||
param([string]$Exe, [string[]]$IbArgs)
|
param([byte[]]$Bytes)
|
||||||
|
if (-not $Bytes -or $Bytes.Length -eq 0) { return '' }
|
||||||
|
try {
|
||||||
|
$strict = New-Object System.Text.UTF8Encoding($false, $true)
|
||||||
|
return $strict.GetString($Bytes)
|
||||||
|
} catch {
|
||||||
|
return [System.Text.Encoding]::GetEncoding(866).GetString($Bytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-PlatformProcess {
|
||||||
|
# Run the platform non-interactively and capture its console output. A closed stdin pipe
|
||||||
|
# (EOF) makes an auth prompt fast-fail instead of hanging; capturing keeps the child's
|
||||||
|
# text out of our stream until we print it labelled (and out of the wrong encoding).
|
||||||
|
# Returns @{ Output; ExitCode }.
|
||||||
|
#
|
||||||
|
# Quoting differs by engine, so the caller says which it built:
|
||||||
|
# ibcmd — tokens are bare (--db-path=C:\a b), the whole token gets quoted here;
|
||||||
|
# 1cv8 — -PreQuoted: the caller already put quotes inside the token (File="C:\a b"),
|
||||||
|
# which is where 1C's own parser expects them; quoting again breaks the value.
|
||||||
|
param([string]$Exe, [string[]]$ProcArgs, [switch]$PreQuoted)
|
||||||
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
||||||
$psi.FileName = $Exe
|
$psi.FileName = $Exe
|
||||||
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
|
$psi.Arguments = if ($PreQuoted) {
|
||||||
|
$ProcArgs -join ' '
|
||||||
|
} else {
|
||||||
|
($ProcArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
|
||||||
|
}
|
||||||
$psi.UseShellExecute = $false
|
$psi.UseShellExecute = $false
|
||||||
$psi.CreateNoWindow = $true
|
$psi.CreateNoWindow = $true
|
||||||
$psi.RedirectStandardInput = $true
|
$psi.RedirectStandardInput = $true
|
||||||
$psi.RedirectStandardOutput = $true
|
$psi.RedirectStandardOutput = $true
|
||||||
$psi.RedirectStandardError = $true
|
$psi.RedirectStandardError = $true
|
||||||
try {
|
|
||||||
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
|
|
||||||
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
|
|
||||||
} catch {}
|
|
||||||
$p = [System.Diagnostics.Process]::Start($psi)
|
$p = [System.Diagnostics.Process]::Start($psi)
|
||||||
$p.StandardInput.Close()
|
$p.StandardInput.Close()
|
||||||
$out = $p.StandardOutput.ReadToEnd()
|
# stderr is drained in parallel: reading the streams one after another deadlocks
|
||||||
$err = $p.StandardError.ReadToEnd()
|
# as soon as the other one fills its pipe buffer.
|
||||||
|
$errMs = New-Object System.IO.MemoryStream
|
||||||
|
$errTask = $p.StandardError.BaseStream.CopyToAsync($errMs)
|
||||||
|
$outMs = New-Object System.IO.MemoryStream
|
||||||
|
$p.StandardOutput.BaseStream.CopyTo($outMs)
|
||||||
|
$errTask.Wait()
|
||||||
$p.WaitForExit()
|
$p.WaitForExit()
|
||||||
|
$out = ConvertFrom-PlatformBytes $outMs.ToArray()
|
||||||
|
$err = ConvertFrom-PlatformBytes $errMs.ToArray()
|
||||||
if ($err) { $out += $err }
|
if ($err) { $out += $err }
|
||||||
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
|
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Write-PlatformOutput {
|
||||||
|
# Print what the platform wrote to the console as its own labelled block. Silence stays
|
||||||
|
# silent: in batch mode 1cv8 reports through /Out and prints nothing here.
|
||||||
|
param([string]$Text)
|
||||||
|
if (-not $Text) { return }
|
||||||
|
$t = $Text.TrimEnd()
|
||||||
|
if (-not $t) { return }
|
||||||
|
$limit = 65536
|
||||||
|
if ($t.Length -gt $limit) {
|
||||||
|
$t = "[... обрезано, показаны последние $limit символов ...]`r`n" + $t.Substring($t.Length - $limit)
|
||||||
|
}
|
||||||
|
Write-Host "--- Вывод платформы ---"
|
||||||
|
Write-Host $t
|
||||||
|
Write-Host "--- End ---"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
||||||
|
|
||||||
|
# --- Resolve additional arguments for the selected engine ---
|
||||||
|
$argHints = @{ '/F' = '-InfoBasePath'; '/S' = '-InfoBaseServer + -InfoBaseRef'; '/N' = '-UserName'; '/P' = '-Password'; '--db-path' = '-InfoBasePath'; '--user' = '-UserName'; '--password' = '-Password' }
|
||||||
|
$extraArgs = @(Resolve-ExtraArgs $engine $AdditionalV8Arguments $AdditionalIbcmdArguments $argHints)
|
||||||
|
|
||||||
# --- Validate connection ---
|
# --- Validate connection ---
|
||||||
if ($engine -eq "ibcmd") {
|
if ($engine -eq "ibcmd") {
|
||||||
if (-not $InfoBasePath) {
|
if (-not $InfoBasePath) {
|
||||||
@@ -207,8 +424,9 @@ try {
|
|||||||
if ($UserName) { $arguments += "--user=$UserName" }
|
if ($UserName) { $arguments += "--user=$UserName" }
|
||||||
if ($Password) { $arguments += "--password=$Password" }
|
if ($Password) { $arguments += "--password=$Password" }
|
||||||
$arguments += "--data=$tempDir"
|
$arguments += "--data=$tempDir"
|
||||||
Write-Host "Running: ibcmd $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))"
|
$arguments += $extraArgs
|
||||||
$__ib = Invoke-IbcmdProcess $V8Path $arguments
|
Write-Host "Running: ibcmd $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
|
||||||
|
$__ib = Invoke-PlatformProcess $V8Path $arguments
|
||||||
$output = $__ib.Output
|
$output = $__ib.Output
|
||||||
$exitCode = $__ib.ExitCode
|
$exitCode = $__ib.ExitCode
|
||||||
if ($exitCode -eq 0) {
|
if ($exitCode -eq 0) {
|
||||||
@@ -216,7 +434,7 @@ try {
|
|||||||
} else {
|
} else {
|
||||||
Write-Host "Error loading configuration (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
|
Write-Host "Error loading configuration (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
|
||||||
}
|
}
|
||||||
if ($output) { Write-Host ($output | Out-String) }
|
Write-PlatformOutput $output
|
||||||
exit $exitCode
|
exit $exitCode
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -246,11 +464,12 @@ try {
|
|||||||
$outFile = Join-Path $tempDir "load_cf_log.txt"
|
$outFile = Join-Path $tempDir "load_cf_log.txt"
|
||||||
$arguments += "/Out", "`"$outFile`""
|
$arguments += "/Out", "`"$outFile`""
|
||||||
$arguments += "/DisableStartupDialogs"
|
$arguments += "/DisableStartupDialogs"
|
||||||
|
$arguments += $extraArgs
|
||||||
|
|
||||||
# --- Execute ---
|
# --- Execute ---
|
||||||
Write-Host "Running: 1cv8.exe $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))"
|
Write-Host "Running: 1cv8.exe $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
|
||||||
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
|
$__v8 = Invoke-PlatformProcess $V8Path $arguments -PreQuoted
|
||||||
$exitCode = $process.ExitCode
|
$exitCode = $__v8.ExitCode
|
||||||
|
|
||||||
# --- Result ---
|
# --- Result ---
|
||||||
if ($exitCode -eq 0) {
|
if ($exitCode -eq 0) {
|
||||||
@@ -267,6 +486,7 @@ try {
|
|||||||
Write-Host "--- End ---"
|
Write-Host "--- End ---"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Write-PlatformOutput $__v8.Output
|
||||||
|
|
||||||
exit $exitCode
|
exit $exitCode
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# db-load-cf v1.10 — Load 1C configuration from CF file
|
# db-load-cf v1.14 — Load 1C configuration from CF file
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -14,6 +14,28 @@ import subprocess
|
|||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
|
# Регистронезависимый ввод — паритет с 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)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def _find_project_v8path():
|
def _find_project_v8path():
|
||||||
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
||||||
@@ -36,6 +58,163 @@ def _find_project_v8path():
|
|||||||
d = parent
|
d = parent
|
||||||
|
|
||||||
|
|
||||||
|
# --- Additional platform arguments ---
|
||||||
|
V8_OWNED_KEYS = [
|
||||||
|
"DESIGNER", "ENTERPRISE", "CREATEINFOBASE", "CONFIG",
|
||||||
|
"/F", "/S", "/N", "/P", "/Out", "/DisableStartupDialogs",
|
||||||
|
"/UseTemplate", "/AddToList", "/Execute", "/C", "/URL", "/UC",
|
||||||
|
"/DumpIB", "/RestoreIB", "/DumpCfg", "/LoadCfg",
|
||||||
|
"/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg",
|
||||||
|
"/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles",
|
||||||
|
]
|
||||||
|
IBCMD_OWNED_KEYS = [
|
||||||
|
"--db-path", "--data", "--out", "--file", "--load", "--restore",
|
||||||
|
"--import", "--export", "--apply", "--force", "--create-database",
|
||||||
|
"--user", "--password",
|
||||||
|
]
|
||||||
|
V8_SECRET_KEYS = ["/P", "/UC", "/WSP", "/AWSP"]
|
||||||
|
IBCMD_SECRET_KEYS = ["--password", "--token", "--db-pwd"]
|
||||||
|
|
||||||
|
|
||||||
|
def arg_key_match(token, key):
|
||||||
|
"""Token matches a key when it equals it, or starts with it and the next character
|
||||||
|
is not a letter — catches glued /N"user" and --password=x, while keeping
|
||||||
|
/ClearCache distinct from /C."""
|
||||||
|
if len(token) < len(key):
|
||||||
|
return False
|
||||||
|
if token[: len(key)].lower() != key.lower():
|
||||||
|
return False
|
||||||
|
if len(token) == len(key):
|
||||||
|
return True
|
||||||
|
return not token[len(key)].isalpha()
|
||||||
|
|
||||||
|
|
||||||
|
def project_extra_args(name):
|
||||||
|
"""v8args / ibcmdargs from .v8-project.json — same upward walk as v8path."""
|
||||||
|
d = os.getcwd()
|
||||||
|
while True:
|
||||||
|
pf = os.path.join(d, ".v8-project.json")
|
||||||
|
if os.path.isfile(pf):
|
||||||
|
try:
|
||||||
|
with open(pf, encoding="utf-8-sig") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
v = data.get(name)
|
||||||
|
if v:
|
||||||
|
return [str(x) for x in v]
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return []
|
||||||
|
parent = os.path.dirname(d)
|
||||||
|
if parent == d:
|
||||||
|
return []
|
||||||
|
d = parent
|
||||||
|
|
||||||
|
|
||||||
|
def assert_extra_args(extra, engine, hints):
|
||||||
|
"""The platform accepts only one batch operation, and a duplicate connection or
|
||||||
|
output key fails with an opaque 1C error — reject what the skill owns itself."""
|
||||||
|
param = "-AdditionalIbcmdArguments" if engine == "ibcmd" else "-AdditionalV8Arguments"
|
||||||
|
owned = IBCMD_OWNED_KEYS if engine == "ibcmd" else V8_OWNED_KEYS
|
||||||
|
for tok in extra:
|
||||||
|
if engine == "ibcmd" and not tok.startswith("-"):
|
||||||
|
print(
|
||||||
|
f"Error: '{tok}' is a positional token — pass values as --key=value "
|
||||||
|
f"({param} cannot extend the ibcmd command)",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
for k in owned:
|
||||||
|
if arg_key_match(tok, k):
|
||||||
|
hint = f" (use {hints[k]})" if hints and k in hints else ""
|
||||||
|
print(
|
||||||
|
f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def format_args_for_display(arglist, engine):
|
||||||
|
"""Redact values of secret-prone keys in glued, =-joined and separate forms.
|
||||||
|
Matching here is a plain prefix (no letter rule): over-masking costs nothing,
|
||||||
|
a leaked password does."""
|
||||||
|
keys = IBCMD_SECRET_KEYS if engine == "ibcmd" else V8_SECRET_KEYS
|
||||||
|
res = []
|
||||||
|
mask_next = False
|
||||||
|
for tok in arglist:
|
||||||
|
if mask_next:
|
||||||
|
res.append("***")
|
||||||
|
mask_next = False
|
||||||
|
continue
|
||||||
|
hit = None
|
||||||
|
for k in keys:
|
||||||
|
if tok[: len(k)].lower() == k.lower():
|
||||||
|
hit = k
|
||||||
|
break
|
||||||
|
if hit is None:
|
||||||
|
res.append(tok)
|
||||||
|
elif len(tok) == len(hit):
|
||||||
|
res.append(tok)
|
||||||
|
mask_next = True
|
||||||
|
elif tok[len(hit)] == "=":
|
||||||
|
res.append(hit + "=***")
|
||||||
|
else:
|
||||||
|
res.append(hit + "***")
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
def extract_extra_args(argv, known_opts):
|
||||||
|
"""argparse refuses values that start with '-' (every ibcmd key does), so pull the two
|
||||||
|
escape-hatch lists out of argv by hand: after the flag, take everything up to the next
|
||||||
|
declared skill option. Returns (remaining_argv, v8_extra, ibcmd_extra)."""
|
||||||
|
rest, v8, ibcmd = [], [], []
|
||||||
|
i = 0
|
||||||
|
while i < len(argv):
|
||||||
|
low = argv[i].lower()
|
||||||
|
if low in ("-additionalv8arguments", "-additionalibcmdarguments"):
|
||||||
|
target = v8 if low == "-additionalv8arguments" else ibcmd
|
||||||
|
i += 1
|
||||||
|
while i < len(argv) and argv[i].lower() not in known_opts:
|
||||||
|
target.append(argv[i])
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
rest.append(argv[i])
|
||||||
|
i += 1
|
||||||
|
return rest, v8, ibcmd
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints):
|
||||||
|
"""Pick the argument list for the selected engine and validate it. An explicitly
|
||||||
|
passed parameter for the other engine is an error; the same keys coming from
|
||||||
|
.v8-project.json simply do not apply — a project may describe both engines.
|
||||||
|
|
||||||
|
Comma-separated elements are split apart: PowerShell's -File cannot bind an array,
|
||||||
|
so that form is the documented one and both ports must accept it. A value containing
|
||||||
|
a comma is not supported."""
|
||||||
|
v8_extra = [p for tok in v8_extra for p in str(tok).split(",") if p]
|
||||||
|
ibcmd_extra = [p for tok in ibcmd_extra for p in str(tok).split(",") if p]
|
||||||
|
if engine == "ibcmd" and v8_extra:
|
||||||
|
print(
|
||||||
|
"Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd "
|
||||||
|
"(use -AdditionalIbcmdArguments)",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
if engine != "ibcmd" and ibcmd_extra:
|
||||||
|
print(
|
||||||
|
"Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 "
|
||||||
|
"(use -AdditionalV8Arguments)",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
if engine == "ibcmd":
|
||||||
|
extra = project_extra_args("ibcmdargs") + list(ibcmd_extra)
|
||||||
|
else:
|
||||||
|
extra = project_extra_args("v8args") + list(v8_extra)
|
||||||
|
if extra:
|
||||||
|
assert_extra_args(extra, engine, hints)
|
||||||
|
return extra
|
||||||
|
|
||||||
|
|
||||||
def _version_dir(p):
|
def _version_dir(p):
|
||||||
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
|
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
|
||||||
parent = os.path.dirname(p)
|
parent = os.path.dirname(p)
|
||||||
@@ -86,6 +265,85 @@ IBCMD_NOUSER_HINT = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def decode_platform_bytes(data):
|
||||||
|
"""ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
|
||||||
|
OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes — the locale
|
||||||
|
code page (what text=True uses) mangles both."""
|
||||||
|
if not data:
|
||||||
|
return ""
|
||||||
|
try:
|
||||||
|
return data.decode("utf-8")
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
return data.decode("cp866", errors="replace")
|
||||||
|
|
||||||
|
|
||||||
|
def assert_infobase_exists(path):
|
||||||
|
"""These skills work on a ready infobase. Saying so up front beats the platform's
|
||||||
|
"Неверные или отсутствующие параметры соединения" after a launch."""
|
||||||
|
if not path:
|
||||||
|
return
|
||||||
|
if not os.path.isfile(os.path.join(path, "1Cv8.1CD")):
|
||||||
|
print(f"Error: information base not found at {path} (no 1Cv8.1CD)", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def clean_path(value, param=""):
|
||||||
|
"""Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
|
||||||
|
surrounding quotes that survived shell parsing, a trailing separator. A quote left
|
||||||
|
inside afterwards cannot be part of a real path — reject it by name instead of letting
|
||||||
|
1C answer with its opaque "Неверные или отсутствующие параметры соединения"."""
|
||||||
|
if not value:
|
||||||
|
return value
|
||||||
|
v = value.strip()
|
||||||
|
if len(v) >= 2 and v[0] == v[-1] and v[0] in "\"'":
|
||||||
|
v = v[1:-1].strip()
|
||||||
|
if len(v) > 3 and v[-1] in "\\/":
|
||||||
|
v = v[:-1]
|
||||||
|
if '"' in v:
|
||||||
|
print(f"Error: {param or 'path'} contains a quote character: {value}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
return v
|
||||||
|
|
||||||
|
|
||||||
|
def quote_if_needed(token):
|
||||||
|
"""Extra arguments come from the caller unquoted; the 1cv8 command line is joined
|
||||||
|
verbatim, so a token with a space needs quotes of its own."""
|
||||||
|
if token and (" " in token or "\t" in token) and '"' not in token:
|
||||||
|
return f'"{token}"'
|
||||||
|
return token
|
||||||
|
|
||||||
|
|
||||||
|
def run_v8(v8path, arguments):
|
||||||
|
"""Run 1cv8 in batch mode and capture its console output.
|
||||||
|
|
||||||
|
The arguments carry their own quotes inside the value (File="C:\\a b") — that is where
|
||||||
|
1C's parser expects them, on Windows and on *nix alike. Windows list2cmdline would
|
||||||
|
escape those quotes, so there the command line is handed over ready-made.
|
||||||
|
"""
|
||||||
|
if os.name == "nt":
|
||||||
|
cmd = '"' + v8path + '" ' + " ".join(arguments)
|
||||||
|
else:
|
||||||
|
cmd = [v8path] + arguments
|
||||||
|
r = subprocess.run(cmd, input=b"", capture_output=True)
|
||||||
|
r.stdout = decode_platform_bytes(r.stdout)
|
||||||
|
r.stderr = decode_platform_bytes(r.stderr)
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
|
def print_platform_output(result):
|
||||||
|
"""Print what the platform wrote to the console as its own labelled block. Silence stays
|
||||||
|
silent: in batch mode 1cv8 reports through /Out and prints nothing here."""
|
||||||
|
text = ((result.stdout or "") + (result.stderr or "")).rstrip()
|
||||||
|
if not text:
|
||||||
|
return
|
||||||
|
limit = 65536
|
||||||
|
if len(text) > limit:
|
||||||
|
text = f"[... обрезано, показаны последние {limit} символов ...]\n" + text[-limit:]
|
||||||
|
print("--- Вывод платформы ---")
|
||||||
|
print(text)
|
||||||
|
print("--- End ---")
|
||||||
|
|
||||||
|
|
||||||
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
||||||
"""Run an ibcmd command non-interactively.
|
"""Run an ibcmd command non-interactively.
|
||||||
|
|
||||||
@@ -96,7 +354,10 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
|||||||
if warn_no_user and os.name == "nt" and not has_username:
|
if warn_no_user and os.name == "nt" and not has_username:
|
||||||
sys.stderr.write(IBCMD_NOUSER_HINT)
|
sys.stderr.write(IBCMD_NOUSER_HINT)
|
||||||
sys.stderr.flush()
|
sys.stderr.flush()
|
||||||
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
|
r = subprocess.run(cmd, input=b"", capture_output=True)
|
||||||
|
r.stdout = decode_platform_bytes(r.stdout)
|
||||||
|
r.stderr = decode_platform_bytes(r.stderr)
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
def describe_exit(code):
|
def describe_exit(code):
|
||||||
@@ -148,11 +409,34 @@ def main():
|
|||||||
parser.add_argument("-InputFile", required=True)
|
parser.add_argument("-InputFile", required=True)
|
||||||
parser.add_argument("-Extension", default="")
|
parser.add_argument("-Extension", default="")
|
||||||
parser.add_argument("-AllExtensions", action="store_true")
|
parser.add_argument("-AllExtensions", action="store_true")
|
||||||
args = parser.parse_args()
|
parser.add_argument("-AdditionalV8Arguments", nargs="*", default=[],
|
||||||
|
help="Extra 1cv8 arguments, e.g. /UseHwLicenses+")
|
||||||
|
parser.add_argument("-AdditionalIbcmdArguments", nargs="*", default=[],
|
||||||
|
help="Extra ibcmd arguments in --key=value form")
|
||||||
|
known_opts = {s.lower() for a in parser._actions for s in a.option_strings}
|
||||||
|
argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts)
|
||||||
|
args = ci_parse_args(parser, argv)
|
||||||
|
|
||||||
|
args.V8Path = clean_path(args.V8Path, "-V8Path")
|
||||||
|
args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath")
|
||||||
|
assert_infobase_exists(args.InfoBasePath)
|
||||||
|
args.InputFile = clean_path(args.InputFile, "-InputFile")
|
||||||
|
|
||||||
v8path = resolve_v8path(args.V8Path)
|
v8path = resolve_v8path(args.V8Path)
|
||||||
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
|
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
|
||||||
|
|
||||||
|
# --- Resolve additional arguments for the selected engine ---
|
||||||
|
arg_hints = {
|
||||||
|
"/F": "-InfoBasePath",
|
||||||
|
"/S": "-InfoBaseServer + -InfoBaseRef",
|
||||||
|
"/N": "-UserName",
|
||||||
|
"/P": "-Password",
|
||||||
|
"--db-path": "-InfoBasePath",
|
||||||
|
"--user": "-UserName",
|
||||||
|
"--password": "-Password",
|
||||||
|
}
|
||||||
|
extra_args = resolve_extra_args(engine, v8_extra, ibcmd_extra, arg_hints)
|
||||||
|
|
||||||
# --- Validate connection ---
|
# --- Validate connection ---
|
||||||
if engine == "ibcmd":
|
if engine == "ibcmd":
|
||||||
if not args.InfoBasePath:
|
if not args.InfoBasePath:
|
||||||
@@ -183,16 +467,13 @@ def main():
|
|||||||
if args.Password:
|
if args.Password:
|
||||||
arguments.append(f"--password={args.Password}")
|
arguments.append(f"--password={args.Password}")
|
||||||
arguments.append(f"--data={ib_data}")
|
arguments.append(f"--data={ib_data}")
|
||||||
print(f"Running: ibcmd {_redact(' '.join(arguments), args.Password, args.UserName)}")
|
arguments.extend(extra_args)
|
||||||
|
print(f"Running: ibcmd {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
|
||||||
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
|
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
|
||||||
if result.returncode == 0:
|
if result.returncode == 0:
|
||||||
print(f"Configuration loaded successfully from: {args.InputFile}")
|
print(f"Configuration loaded successfully from: {args.InputFile}")
|
||||||
else:
|
else:
|
||||||
print(f"Error loading configuration (code: {result.returncode}){describe_exit(result.returncode)}", file=sys.stderr)
|
print(f"Error loading configuration (code: {result.returncode}){describe_exit(result.returncode)}", file=sys.stderr)
|
||||||
if result.stdout:
|
|
||||||
print(result.stdout)
|
|
||||||
if result.stderr:
|
|
||||||
print(result.stderr, file=sys.stderr)
|
|
||||||
sys.exit(result.returncode)
|
sys.exit(result.returncode)
|
||||||
|
|
||||||
# --- Temp dir ---
|
# --- Temp dir ---
|
||||||
@@ -204,35 +485,32 @@ def main():
|
|||||||
arguments = ["DESIGNER"]
|
arguments = ["DESIGNER"]
|
||||||
|
|
||||||
if args.InfoBaseServer and args.InfoBaseRef:
|
if args.InfoBaseServer and args.InfoBaseRef:
|
||||||
arguments.extend(["/S", f"{args.InfoBaseServer}/{args.InfoBaseRef}"])
|
arguments.extend(["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"'])
|
||||||
else:
|
else:
|
||||||
arguments.extend(["/F", args.InfoBasePath])
|
arguments.extend(["/F", f'"{args.InfoBasePath}"'])
|
||||||
|
|
||||||
if args.UserName:
|
if args.UserName:
|
||||||
arguments.append(f"/N{args.UserName}")
|
arguments.append(f'/N"{args.UserName}"')
|
||||||
if args.Password:
|
if args.Password:
|
||||||
arguments.append(f"/P{args.Password}")
|
arguments.append(f'/P"{args.Password}"')
|
||||||
|
|
||||||
arguments.extend(["/LoadCfg", args.InputFile])
|
arguments.extend(["/LoadCfg", f'"{args.InputFile}"'])
|
||||||
|
|
||||||
# --- Extensions ---
|
# --- Extensions ---
|
||||||
if args.Extension:
|
if args.Extension:
|
||||||
arguments.extend(["-Extension", args.Extension])
|
arguments.extend(["-Extension", f'"{args.Extension}"'])
|
||||||
elif args.AllExtensions:
|
elif args.AllExtensions:
|
||||||
arguments.append("-AllExtensions")
|
arguments.append("-AllExtensions")
|
||||||
|
|
||||||
# --- Output ---
|
# --- Output ---
|
||||||
out_file = os.path.join(temp_dir, "load_cf_log.txt")
|
out_file = os.path.join(temp_dir, "load_cf_log.txt")
|
||||||
arguments.extend(["/Out", out_file])
|
arguments.extend(["/Out", f'"{out_file}"'])
|
||||||
arguments.append("/DisableStartupDialogs")
|
arguments.append("/DisableStartupDialogs")
|
||||||
|
arguments.extend(quote_if_needed(a) for a in extra_args)
|
||||||
|
|
||||||
# --- Execute ---
|
# --- Execute ---
|
||||||
print(f"Running: 1cv8.exe {_redact(' '.join(arguments), args.Password, args.UserName)}")
|
print(f"Running: 1cv8.exe {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
|
||||||
result = subprocess.run(
|
result = run_v8(v8path, arguments)
|
||||||
[v8path] + arguments,
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
)
|
|
||||||
exit_code = result.returncode
|
exit_code = result.returncode
|
||||||
|
|
||||||
# --- Result ---
|
# --- Result ---
|
||||||
@@ -252,6 +530,7 @@ def main():
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
print_platform_output(result)
|
||||||
sys.exit(exit_code)
|
sys.exit(exit_code)
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
|
|||||||
@@ -68,6 +68,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-dt.ps1" <п
|
|||||||
| `-InputFile <путь>` | да | Путь к DT-файлу |
|
| `-InputFile <путь>` | да | Путь к DT-файлу |
|
||||||
| `-JobsCount <N>` | нет | Число фоновых заданий загрузки (0 = по числу процессоров) |
|
| `-JobsCount <N>` | нет | Число фоновых заданий загрузки (0 = по числу процессоров) |
|
||||||
| `-UnlockCode <код>` | нет | Код разблокировки (`/UC`), если заблокировано начало сеансов |
|
| `-UnlockCode <код>` | нет | Код разблокировки (`/UC`), если заблокировано начало сеансов |
|
||||||
|
| `-AdditionalV8Arguments <список>` | нет | Доп. аргументы запуска `1cv8.exe` через запятую, напр. `/UseHwLicenses+` |
|
||||||
|
| `-AdditionalIbcmdArguments <список>` | нет | Доп. аргументы `ibcmd` через запятую, в форме `--ключ=значение` |
|
||||||
|
|
||||||
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
|
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# db-load-dt v1.9 — Load 1C information base from DT file
|
# db-load-dt v1.13 — Load 1C information base from DT file
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||||
<#
|
<#
|
||||||
@@ -36,6 +36,12 @@
|
|||||||
.PARAMETER UnlockCode
|
.PARAMETER UnlockCode
|
||||||
Код разблокировки базы (/UC) — если заблокировано начало сеансов
|
Код разблокировки базы (/UC) — если заблокировано начало сеансов
|
||||||
|
|
||||||
|
.PARAMETER AdditionalV8Arguments
|
||||||
|
Дополнительные аргументы запуска 1cv8.exe (например /UseHwLicenses+)
|
||||||
|
|
||||||
|
.PARAMETER AdditionalIbcmdArguments
|
||||||
|
Дополнительные аргументы запуска ibcmd (форма --ключ=значение)
|
||||||
|
|
||||||
.EXAMPLE
|
.EXAMPLE
|
||||||
.\db-load-dt.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "backup.dt"
|
.\db-load-dt.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "backup.dt"
|
||||||
#>
|
#>
|
||||||
@@ -67,7 +73,13 @@ param(
|
|||||||
[int]$JobsCount = 0,
|
[int]$JobsCount = 0,
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
[Parameter(Mandatory=$false)]
|
||||||
[string]$UnlockCode
|
[string]$UnlockCode,
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[string[]]$AdditionalV8Arguments = @(),
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[string[]]$AdditionalIbcmdArguments = @()
|
||||||
)
|
)
|
||||||
|
|
||||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
@@ -97,6 +109,164 @@ function Get-ExitAnnotation {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- Additional platform arguments ---
|
||||||
|
$script:V8OwnedKeys = @(
|
||||||
|
'DESIGNER', 'ENTERPRISE', 'CREATEINFOBASE', 'CONFIG',
|
||||||
|
'/F', '/S', '/N', '/P', '/Out', '/DisableStartupDialogs',
|
||||||
|
'/UseTemplate', '/AddToList', '/Execute', '/C', '/URL', '/UC',
|
||||||
|
'/DumpIB', '/RestoreIB', '/DumpCfg', '/LoadCfg',
|
||||||
|
'/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg',
|
||||||
|
'/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles'
|
||||||
|
)
|
||||||
|
$script:IbcmdOwnedKeys = @(
|
||||||
|
'--db-path', '--data', '--out', '--file', '--load', '--restore',
|
||||||
|
'--import', '--export', '--apply', '--force', '--create-database',
|
||||||
|
'--user', '--password'
|
||||||
|
)
|
||||||
|
$script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP')
|
||||||
|
$script:IbcmdSecretKeys = @('--password', '--token', '--db-pwd')
|
||||||
|
|
||||||
|
function Test-ArgKeyMatch {
|
||||||
|
# A token matches a key when it equals the key, or starts with it and the next
|
||||||
|
# character is not a letter — catches glued /N"user" and --password=x, while
|
||||||
|
# keeping /ClearCache distinct from /C.
|
||||||
|
param([string]$Token, [string]$Key)
|
||||||
|
if ($Token.Length -lt $Key.Length) { return $false }
|
||||||
|
if (-not $Token.Substring(0, $Key.Length).Equals($Key, [System.StringComparison]::OrdinalIgnoreCase)) { return $false }
|
||||||
|
if ($Token.Length -eq $Key.Length) { return $true }
|
||||||
|
return -not [char]::IsLetter($Token[$Key.Length])
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-ProjectExtraArgs {
|
||||||
|
# v8args / ibcmdargs from .v8-project.json — same upward walk as v8path.
|
||||||
|
param([string]$Name)
|
||||||
|
$dir = (Get-Location).Path
|
||||||
|
while ($dir) {
|
||||||
|
$pf = Join-Path $dir ".v8-project.json"
|
||||||
|
if (Test-Path $pf) {
|
||||||
|
try {
|
||||||
|
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||||
|
if ($j.$Name) { return @($j.$Name | ForEach-Object { [string]$_ }) }
|
||||||
|
} catch {}
|
||||||
|
return @()
|
||||||
|
}
|
||||||
|
$parent = Split-Path $dir -Parent
|
||||||
|
if (-not $parent -or $parent -eq $dir) { break }
|
||||||
|
$dir = $parent
|
||||||
|
}
|
||||||
|
return @()
|
||||||
|
}
|
||||||
|
|
||||||
|
function Assert-ExtraArgs {
|
||||||
|
# The platform accepts only one batch operation, and a duplicate connection or
|
||||||
|
# output key fails with an opaque 1C error — reject what the skill owns itself.
|
||||||
|
param([string[]]$ExtraArgs, [string]$Engine, [hashtable]$Hints)
|
||||||
|
$paramName = if ($Engine -eq 'ibcmd') { '-AdditionalIbcmdArguments' } else { '-AdditionalV8Arguments' }
|
||||||
|
$owned = if ($Engine -eq 'ibcmd') { $script:IbcmdOwnedKeys } else { $script:V8OwnedKeys }
|
||||||
|
foreach ($tok in $ExtraArgs) {
|
||||||
|
if ($Engine -eq 'ibcmd' -and $tok -notmatch '^-') {
|
||||||
|
Write-Host "Error: '$tok' is a positional token — pass values as --key=value ($paramName cannot extend the ibcmd command)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
foreach ($k in $owned) {
|
||||||
|
if (Test-ArgKeyMatch $tok $k) {
|
||||||
|
$hint = ''
|
||||||
|
if ($Hints -and $Hints.ContainsKey($k)) { $hint = " (use $($Hints[$k]))" }
|
||||||
|
Write-Host "Error: $k is controlled by the skill and cannot be passed via $paramName$hint" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Resolve-ExtraArgs {
|
||||||
|
# Pick the argument list for the selected engine and validate it. An explicitly passed
|
||||||
|
# parameter for the other engine is an error; the same keys coming from .v8-project.json
|
||||||
|
# simply do not apply — a project may describe both engines.
|
||||||
|
param([string]$Engine, [string[]]$V8Extra, [string[]]$IbcmdExtra, [hashtable]$Hints)
|
||||||
|
# powershell.exe -File — how skills are invoked — cannot bind an array parameter:
|
||||||
|
# space-separated values spill into positional ones, a comma-joined list arrives as a
|
||||||
|
# single token. So accept the repo's list convention (comma-separated) and split here;
|
||||||
|
# a native array call keeps working. A value containing a comma is not supported.
|
||||||
|
$V8Extra = @($V8Extra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
|
||||||
|
$IbcmdExtra = @($IbcmdExtra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
|
||||||
|
if ($Engine -eq 'ibcmd' -and $V8Extra.Count -gt 0) {
|
||||||
|
Write-Host "Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd (use -AdditionalIbcmdArguments)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
if ($Engine -ne 'ibcmd' -and $IbcmdExtra.Count -gt 0) {
|
||||||
|
Write-Host "Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 (use -AdditionalV8Arguments)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
if ($Engine -eq 'ibcmd') {
|
||||||
|
$extra = @(Get-ProjectExtraArgs 'ibcmdargs') + @($IbcmdExtra)
|
||||||
|
} else {
|
||||||
|
$extra = @(Get-ProjectExtraArgs 'v8args') + @($V8Extra)
|
||||||
|
}
|
||||||
|
if ($extra.Count -gt 0) { Assert-ExtraArgs $extra $Engine $Hints }
|
||||||
|
# Plain return, no comma trick: the caller re-collects with @(...), and ,@() there
|
||||||
|
# would nest the array — the tokens would then be glued into one argument.
|
||||||
|
return $extra
|
||||||
|
}
|
||||||
|
|
||||||
|
function Format-ArgsForDisplay {
|
||||||
|
# Redact values of secret-prone keys in glued, =-joined and separate forms.
|
||||||
|
# Matching here is a plain prefix (no letter rule): over-masking costs nothing,
|
||||||
|
# a leaked password does.
|
||||||
|
param([string[]]$ArgList, [string]$Engine)
|
||||||
|
$keys = if ($Engine -eq 'ibcmd') { $script:IbcmdSecretKeys } else { $script:V8SecretKeys }
|
||||||
|
$res = @()
|
||||||
|
$maskNext = $false
|
||||||
|
foreach ($tok in $ArgList) {
|
||||||
|
if ($maskNext) { $res += '***'; $maskNext = $false; continue }
|
||||||
|
$hit = $null
|
||||||
|
foreach ($k in $keys) {
|
||||||
|
if ($tok.Length -ge $k.Length -and $tok.Substring(0, $k.Length).Equals($k, [System.StringComparison]::OrdinalIgnoreCase)) { $hit = $k; break }
|
||||||
|
}
|
||||||
|
if (-not $hit) { $res += $tok; continue }
|
||||||
|
if ($tok.Length -eq $hit.Length) { $res += $tok; $maskNext = $true }
|
||||||
|
elseif ($tok[$hit.Length] -eq '=') { $res += ($hit + '=***') }
|
||||||
|
else { $res += ($hit + '***') }
|
||||||
|
}
|
||||||
|
return ,$res
|
||||||
|
}
|
||||||
|
|
||||||
|
function ConvertTo-CleanPath {
|
||||||
|
# Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
|
||||||
|
# surrounding quotes that survived shell parsing, a trailing separator. A quote left
|
||||||
|
# inside afterwards cannot be part of a real path — reject it by name instead of letting
|
||||||
|
# 1C answer with its opaque "Неверные или отсутствующие параметры соединения".
|
||||||
|
param([string]$Value, [string]$ParamName)
|
||||||
|
if (-not $Value) { return $Value }
|
||||||
|
$v = $Value.Trim()
|
||||||
|
if ($v.Length -ge 2 -and $v[0] -eq $v[-1] -and ($v[0] -eq '"' -or $v[0] -eq "'")) {
|
||||||
|
$v = $v.Substring(1, $v.Length - 2).Trim()
|
||||||
|
}
|
||||||
|
if ($v.Length -gt 3 -and ($v[-1] -eq '\' -or $v[-1] -eq '/')) { $v = $v.Substring(0, $v.Length - 1) }
|
||||||
|
if ($v.Contains('"')) {
|
||||||
|
Write-Host "Error: $ParamName contains a quote character: $Value" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
return $v
|
||||||
|
}
|
||||||
|
|
||||||
|
$V8Path = ConvertTo-CleanPath $V8Path '-V8Path'
|
||||||
|
$InfoBasePath = ConvertTo-CleanPath $InfoBasePath '-InfoBasePath'
|
||||||
|
$InputFile = ConvertTo-CleanPath $InputFile '-InputFile'
|
||||||
|
|
||||||
|
function Assert-InfoBaseExists {
|
||||||
|
# These skills work on a ready infobase. Saying so up front beats the platform's
|
||||||
|
# "Неверные или отсутствующие параметры соединения" after a launch.
|
||||||
|
param([string]$Path)
|
||||||
|
if (-not $Path) { return }
|
||||||
|
if (-not (Test-Path (Join-Path $Path "1Cv8.1CD"))) {
|
||||||
|
Write-Host "Error: information base not found at $Path (no 1Cv8.1CD)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert-InfoBaseExists $InfoBasePath
|
||||||
|
|
||||||
# --- Resolve V8Path ---
|
# --- Resolve V8Path ---
|
||||||
function Find-ProjectV8Path {
|
function Find-ProjectV8Path {
|
||||||
$dir = (Get-Location).Path
|
$dir = (Get-Location).Path
|
||||||
@@ -141,35 +311,82 @@ if (-not (Test-Path $V8Path)) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
|
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
|
||||||
function Invoke-IbcmdProcess {
|
function ConvertFrom-PlatformBytes {
|
||||||
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
|
# ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
|
||||||
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
|
# OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes — guessing
|
||||||
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
|
# one of them outright mangles Cyrillic.
|
||||||
param([string]$Exe, [string[]]$IbArgs)
|
param([byte[]]$Bytes)
|
||||||
|
if (-not $Bytes -or $Bytes.Length -eq 0) { return '' }
|
||||||
|
try {
|
||||||
|
$strict = New-Object System.Text.UTF8Encoding($false, $true)
|
||||||
|
return $strict.GetString($Bytes)
|
||||||
|
} catch {
|
||||||
|
return [System.Text.Encoding]::GetEncoding(866).GetString($Bytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-PlatformProcess {
|
||||||
|
# Run the platform non-interactively and capture its console output. A closed stdin pipe
|
||||||
|
# (EOF) makes an auth prompt fast-fail instead of hanging; capturing keeps the child's
|
||||||
|
# text out of our stream until we print it labelled (and out of the wrong encoding).
|
||||||
|
# Returns @{ Output; ExitCode }.
|
||||||
|
#
|
||||||
|
# Quoting differs by engine, so the caller says which it built:
|
||||||
|
# ibcmd — tokens are bare (--db-path=C:\a b), the whole token gets quoted here;
|
||||||
|
# 1cv8 — -PreQuoted: the caller already put quotes inside the token (File="C:\a b"),
|
||||||
|
# which is where 1C's own parser expects them; quoting again breaks the value.
|
||||||
|
param([string]$Exe, [string[]]$ProcArgs, [switch]$PreQuoted)
|
||||||
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
||||||
$psi.FileName = $Exe
|
$psi.FileName = $Exe
|
||||||
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
|
$psi.Arguments = if ($PreQuoted) {
|
||||||
|
$ProcArgs -join ' '
|
||||||
|
} else {
|
||||||
|
($ProcArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
|
||||||
|
}
|
||||||
$psi.UseShellExecute = $false
|
$psi.UseShellExecute = $false
|
||||||
$psi.CreateNoWindow = $true
|
$psi.CreateNoWindow = $true
|
||||||
$psi.RedirectStandardInput = $true
|
$psi.RedirectStandardInput = $true
|
||||||
$psi.RedirectStandardOutput = $true
|
$psi.RedirectStandardOutput = $true
|
||||||
$psi.RedirectStandardError = $true
|
$psi.RedirectStandardError = $true
|
||||||
try {
|
|
||||||
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
|
|
||||||
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
|
|
||||||
} catch {}
|
|
||||||
$p = [System.Diagnostics.Process]::Start($psi)
|
$p = [System.Diagnostics.Process]::Start($psi)
|
||||||
$p.StandardInput.Close()
|
$p.StandardInput.Close()
|
||||||
$out = $p.StandardOutput.ReadToEnd()
|
# stderr is drained in parallel: reading the streams one after another deadlocks
|
||||||
$err = $p.StandardError.ReadToEnd()
|
# as soon as the other one fills its pipe buffer.
|
||||||
|
$errMs = New-Object System.IO.MemoryStream
|
||||||
|
$errTask = $p.StandardError.BaseStream.CopyToAsync($errMs)
|
||||||
|
$outMs = New-Object System.IO.MemoryStream
|
||||||
|
$p.StandardOutput.BaseStream.CopyTo($outMs)
|
||||||
|
$errTask.Wait()
|
||||||
$p.WaitForExit()
|
$p.WaitForExit()
|
||||||
|
$out = ConvertFrom-PlatformBytes $outMs.ToArray()
|
||||||
|
$err = ConvertFrom-PlatformBytes $errMs.ToArray()
|
||||||
if ($err) { $out += $err }
|
if ($err) { $out += $err }
|
||||||
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
|
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Write-PlatformOutput {
|
||||||
|
# Print what the platform wrote to the console as its own labelled block. Silence stays
|
||||||
|
# silent: in batch mode 1cv8 reports through /Out and prints nothing here.
|
||||||
|
param([string]$Text)
|
||||||
|
if (-not $Text) { return }
|
||||||
|
$t = $Text.TrimEnd()
|
||||||
|
if (-not $t) { return }
|
||||||
|
$limit = 65536
|
||||||
|
if ($t.Length -gt $limit) {
|
||||||
|
$t = "[... обрезано, показаны последние $limit символов ...]`r`n" + $t.Substring($t.Length - $limit)
|
||||||
|
}
|
||||||
|
Write-Host "--- Вывод платформы ---"
|
||||||
|
Write-Host $t
|
||||||
|
Write-Host "--- End ---"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
||||||
|
|
||||||
|
# --- Resolve additional arguments for the selected engine ---
|
||||||
|
$argHints = @{ '/F' = '-InfoBasePath'; '/S' = '-InfoBaseServer + -InfoBaseRef'; '/N' = '-UserName'; '/P' = '-Password'; '--db-path' = '-InfoBasePath'; '--user' = '-UserName'; '--password' = '-Password' }
|
||||||
|
$extraArgs = @(Resolve-ExtraArgs $engine $AdditionalV8Arguments $AdditionalIbcmdArguments $argHints)
|
||||||
|
|
||||||
# --- Validate connection ---
|
# --- Validate connection ---
|
||||||
if ($engine -eq "ibcmd") {
|
if ($engine -eq "ibcmd") {
|
||||||
if (-not $InfoBasePath) {
|
if (-not $InfoBasePath) {
|
||||||
@@ -201,8 +418,10 @@ try {
|
|||||||
$arguments += "$InputFile"
|
$arguments += "$InputFile"
|
||||||
|
|
||||||
$arguments += "--data=$tempDir"
|
$arguments += "--data=$tempDir"
|
||||||
Write-Host "Running: ibcmd $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))"
|
|
||||||
$__ib = Invoke-IbcmdProcess $V8Path $arguments
|
$arguments += $extraArgs
|
||||||
|
Write-Host "Running: ibcmd $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
|
||||||
|
$__ib = Invoke-PlatformProcess $V8Path $arguments
|
||||||
$output = $__ib.Output
|
$output = $__ib.Output
|
||||||
$exitCode = $__ib.ExitCode
|
$exitCode = $__ib.ExitCode
|
||||||
if ($exitCode -eq 0) {
|
if ($exitCode -eq 0) {
|
||||||
@@ -210,7 +429,7 @@ try {
|
|||||||
} else {
|
} else {
|
||||||
Write-Host "Error restoring information base (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
|
Write-Host "Error restoring information base (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
|
||||||
}
|
}
|
||||||
if ($output) { Write-Host ($output | Out-String) }
|
Write-PlatformOutput $output
|
||||||
exit $exitCode
|
exit $exitCode
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,11 +454,12 @@ try {
|
|||||||
$outFile = Join-Path $tempDir "load_dt_log.txt"
|
$outFile = Join-Path $tempDir "load_dt_log.txt"
|
||||||
$arguments += "/Out", "`"$outFile`""
|
$arguments += "/Out", "`"$outFile`""
|
||||||
$arguments += "/DisableStartupDialogs"
|
$arguments += "/DisableStartupDialogs"
|
||||||
|
$arguments += $extraArgs
|
||||||
|
|
||||||
# --- Execute ---
|
# --- Execute ---
|
||||||
Write-Host "Running: 1cv8.exe $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))"
|
Write-Host "Running: 1cv8.exe $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
|
||||||
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
|
$__v8 = Invoke-PlatformProcess $V8Path $arguments -PreQuoted
|
||||||
$exitCode = $process.ExitCode
|
$exitCode = $__v8.ExitCode
|
||||||
|
|
||||||
# --- Result ---
|
# --- Result ---
|
||||||
if ($exitCode -eq 0) {
|
if ($exitCode -eq 0) {
|
||||||
@@ -256,6 +476,7 @@ try {
|
|||||||
Write-Host "--- End ---"
|
Write-Host "--- End ---"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Write-PlatformOutput $__v8.Output
|
||||||
|
|
||||||
exit $exitCode
|
exit $exitCode
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# db-load-dt v1.9 — Load 1C information base from DT file
|
# db-load-dt v1.13 — Load 1C information base from DT file
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -14,6 +14,28 @@ import subprocess
|
|||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
|
# Регистронезависимый ввод — паритет с 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)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def _find_project_v8path():
|
def _find_project_v8path():
|
||||||
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
||||||
@@ -36,6 +58,163 @@ def _find_project_v8path():
|
|||||||
d = parent
|
d = parent
|
||||||
|
|
||||||
|
|
||||||
|
# --- Additional platform arguments ---
|
||||||
|
V8_OWNED_KEYS = [
|
||||||
|
"DESIGNER", "ENTERPRISE", "CREATEINFOBASE", "CONFIG",
|
||||||
|
"/F", "/S", "/N", "/P", "/Out", "/DisableStartupDialogs",
|
||||||
|
"/UseTemplate", "/AddToList", "/Execute", "/C", "/URL", "/UC",
|
||||||
|
"/DumpIB", "/RestoreIB", "/DumpCfg", "/LoadCfg",
|
||||||
|
"/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg",
|
||||||
|
"/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles",
|
||||||
|
]
|
||||||
|
IBCMD_OWNED_KEYS = [
|
||||||
|
"--db-path", "--data", "--out", "--file", "--load", "--restore",
|
||||||
|
"--import", "--export", "--apply", "--force", "--create-database",
|
||||||
|
"--user", "--password",
|
||||||
|
]
|
||||||
|
V8_SECRET_KEYS = ["/P", "/UC", "/WSP", "/AWSP"]
|
||||||
|
IBCMD_SECRET_KEYS = ["--password", "--token", "--db-pwd"]
|
||||||
|
|
||||||
|
|
||||||
|
def arg_key_match(token, key):
|
||||||
|
"""Token matches a key when it equals it, or starts with it and the next character
|
||||||
|
is not a letter — catches glued /N"user" and --password=x, while keeping
|
||||||
|
/ClearCache distinct from /C."""
|
||||||
|
if len(token) < len(key):
|
||||||
|
return False
|
||||||
|
if token[: len(key)].lower() != key.lower():
|
||||||
|
return False
|
||||||
|
if len(token) == len(key):
|
||||||
|
return True
|
||||||
|
return not token[len(key)].isalpha()
|
||||||
|
|
||||||
|
|
||||||
|
def project_extra_args(name):
|
||||||
|
"""v8args / ibcmdargs from .v8-project.json — same upward walk as v8path."""
|
||||||
|
d = os.getcwd()
|
||||||
|
while True:
|
||||||
|
pf = os.path.join(d, ".v8-project.json")
|
||||||
|
if os.path.isfile(pf):
|
||||||
|
try:
|
||||||
|
with open(pf, encoding="utf-8-sig") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
v = data.get(name)
|
||||||
|
if v:
|
||||||
|
return [str(x) for x in v]
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return []
|
||||||
|
parent = os.path.dirname(d)
|
||||||
|
if parent == d:
|
||||||
|
return []
|
||||||
|
d = parent
|
||||||
|
|
||||||
|
|
||||||
|
def assert_extra_args(extra, engine, hints):
|
||||||
|
"""The platform accepts only one batch operation, and a duplicate connection or
|
||||||
|
output key fails with an opaque 1C error — reject what the skill owns itself."""
|
||||||
|
param = "-AdditionalIbcmdArguments" if engine == "ibcmd" else "-AdditionalV8Arguments"
|
||||||
|
owned = IBCMD_OWNED_KEYS if engine == "ibcmd" else V8_OWNED_KEYS
|
||||||
|
for tok in extra:
|
||||||
|
if engine == "ibcmd" and not tok.startswith("-"):
|
||||||
|
print(
|
||||||
|
f"Error: '{tok}' is a positional token — pass values as --key=value "
|
||||||
|
f"({param} cannot extend the ibcmd command)",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
for k in owned:
|
||||||
|
if arg_key_match(tok, k):
|
||||||
|
hint = f" (use {hints[k]})" if hints and k in hints else ""
|
||||||
|
print(
|
||||||
|
f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def format_args_for_display(arglist, engine):
|
||||||
|
"""Redact values of secret-prone keys in glued, =-joined and separate forms.
|
||||||
|
Matching here is a plain prefix (no letter rule): over-masking costs nothing,
|
||||||
|
a leaked password does."""
|
||||||
|
keys = IBCMD_SECRET_KEYS if engine == "ibcmd" else V8_SECRET_KEYS
|
||||||
|
res = []
|
||||||
|
mask_next = False
|
||||||
|
for tok in arglist:
|
||||||
|
if mask_next:
|
||||||
|
res.append("***")
|
||||||
|
mask_next = False
|
||||||
|
continue
|
||||||
|
hit = None
|
||||||
|
for k in keys:
|
||||||
|
if tok[: len(k)].lower() == k.lower():
|
||||||
|
hit = k
|
||||||
|
break
|
||||||
|
if hit is None:
|
||||||
|
res.append(tok)
|
||||||
|
elif len(tok) == len(hit):
|
||||||
|
res.append(tok)
|
||||||
|
mask_next = True
|
||||||
|
elif tok[len(hit)] == "=":
|
||||||
|
res.append(hit + "=***")
|
||||||
|
else:
|
||||||
|
res.append(hit + "***")
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
def extract_extra_args(argv, known_opts):
|
||||||
|
"""argparse refuses values that start with '-' (every ibcmd key does), so pull the two
|
||||||
|
escape-hatch lists out of argv by hand: after the flag, take everything up to the next
|
||||||
|
declared skill option. Returns (remaining_argv, v8_extra, ibcmd_extra)."""
|
||||||
|
rest, v8, ibcmd = [], [], []
|
||||||
|
i = 0
|
||||||
|
while i < len(argv):
|
||||||
|
low = argv[i].lower()
|
||||||
|
if low in ("-additionalv8arguments", "-additionalibcmdarguments"):
|
||||||
|
target = v8 if low == "-additionalv8arguments" else ibcmd
|
||||||
|
i += 1
|
||||||
|
while i < len(argv) and argv[i].lower() not in known_opts:
|
||||||
|
target.append(argv[i])
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
rest.append(argv[i])
|
||||||
|
i += 1
|
||||||
|
return rest, v8, ibcmd
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints):
|
||||||
|
"""Pick the argument list for the selected engine and validate it. An explicitly
|
||||||
|
passed parameter for the other engine is an error; the same keys coming from
|
||||||
|
.v8-project.json simply do not apply — a project may describe both engines.
|
||||||
|
|
||||||
|
Comma-separated elements are split apart: PowerShell's -File cannot bind an array,
|
||||||
|
so that form is the documented one and both ports must accept it. A value containing
|
||||||
|
a comma is not supported."""
|
||||||
|
v8_extra = [p for tok in v8_extra for p in str(tok).split(",") if p]
|
||||||
|
ibcmd_extra = [p for tok in ibcmd_extra for p in str(tok).split(",") if p]
|
||||||
|
if engine == "ibcmd" and v8_extra:
|
||||||
|
print(
|
||||||
|
"Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd "
|
||||||
|
"(use -AdditionalIbcmdArguments)",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
if engine != "ibcmd" and ibcmd_extra:
|
||||||
|
print(
|
||||||
|
"Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 "
|
||||||
|
"(use -AdditionalV8Arguments)",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
if engine == "ibcmd":
|
||||||
|
extra = project_extra_args("ibcmdargs") + list(ibcmd_extra)
|
||||||
|
else:
|
||||||
|
extra = project_extra_args("v8args") + list(v8_extra)
|
||||||
|
if extra:
|
||||||
|
assert_extra_args(extra, engine, hints)
|
||||||
|
return extra
|
||||||
|
|
||||||
|
|
||||||
def _version_dir(p):
|
def _version_dir(p):
|
||||||
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
|
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
|
||||||
parent = os.path.dirname(p)
|
parent = os.path.dirname(p)
|
||||||
@@ -86,6 +265,85 @@ IBCMD_NOUSER_HINT = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def decode_platform_bytes(data):
|
||||||
|
"""ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
|
||||||
|
OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes — the locale
|
||||||
|
code page (what text=True uses) mangles both."""
|
||||||
|
if not data:
|
||||||
|
return ""
|
||||||
|
try:
|
||||||
|
return data.decode("utf-8")
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
return data.decode("cp866", errors="replace")
|
||||||
|
|
||||||
|
|
||||||
|
def assert_infobase_exists(path):
|
||||||
|
"""These skills work on a ready infobase. Saying so up front beats the platform's
|
||||||
|
"Неверные или отсутствующие параметры соединения" after a launch."""
|
||||||
|
if not path:
|
||||||
|
return
|
||||||
|
if not os.path.isfile(os.path.join(path, "1Cv8.1CD")):
|
||||||
|
print(f"Error: information base not found at {path} (no 1Cv8.1CD)", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def clean_path(value, param=""):
|
||||||
|
"""Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
|
||||||
|
surrounding quotes that survived shell parsing, a trailing separator. A quote left
|
||||||
|
inside afterwards cannot be part of a real path — reject it by name instead of letting
|
||||||
|
1C answer with its opaque "Неверные или отсутствующие параметры соединения"."""
|
||||||
|
if not value:
|
||||||
|
return value
|
||||||
|
v = value.strip()
|
||||||
|
if len(v) >= 2 and v[0] == v[-1] and v[0] in "\"'":
|
||||||
|
v = v[1:-1].strip()
|
||||||
|
if len(v) > 3 and v[-1] in "\\/":
|
||||||
|
v = v[:-1]
|
||||||
|
if '"' in v:
|
||||||
|
print(f"Error: {param or 'path'} contains a quote character: {value}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
return v
|
||||||
|
|
||||||
|
|
||||||
|
def quote_if_needed(token):
|
||||||
|
"""Extra arguments come from the caller unquoted; the 1cv8 command line is joined
|
||||||
|
verbatim, so a token with a space needs quotes of its own."""
|
||||||
|
if token and (" " in token or "\t" in token) and '"' not in token:
|
||||||
|
return f'"{token}"'
|
||||||
|
return token
|
||||||
|
|
||||||
|
|
||||||
|
def run_v8(v8path, arguments):
|
||||||
|
"""Run 1cv8 in batch mode and capture its console output.
|
||||||
|
|
||||||
|
The arguments carry their own quotes inside the value (File="C:\\a b") — that is where
|
||||||
|
1C's parser expects them, on Windows and on *nix alike. Windows list2cmdline would
|
||||||
|
escape those quotes, so there the command line is handed over ready-made.
|
||||||
|
"""
|
||||||
|
if os.name == "nt":
|
||||||
|
cmd = '"' + v8path + '" ' + " ".join(arguments)
|
||||||
|
else:
|
||||||
|
cmd = [v8path] + arguments
|
||||||
|
r = subprocess.run(cmd, input=b"", capture_output=True)
|
||||||
|
r.stdout = decode_platform_bytes(r.stdout)
|
||||||
|
r.stderr = decode_platform_bytes(r.stderr)
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
|
def print_platform_output(result):
|
||||||
|
"""Print what the platform wrote to the console as its own labelled block. Silence stays
|
||||||
|
silent: in batch mode 1cv8 reports through /Out and prints nothing here."""
|
||||||
|
text = ((result.stdout or "") + (result.stderr or "")).rstrip()
|
||||||
|
if not text:
|
||||||
|
return
|
||||||
|
limit = 65536
|
||||||
|
if len(text) > limit:
|
||||||
|
text = f"[... обрезано, показаны последние {limit} символов ...]\n" + text[-limit:]
|
||||||
|
print("--- Вывод платформы ---")
|
||||||
|
print(text)
|
||||||
|
print("--- End ---")
|
||||||
|
|
||||||
|
|
||||||
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
||||||
"""Run an ibcmd command non-interactively.
|
"""Run an ibcmd command non-interactively.
|
||||||
|
|
||||||
@@ -96,7 +354,10 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
|||||||
if warn_no_user and os.name == "nt" and not has_username:
|
if warn_no_user and os.name == "nt" and not has_username:
|
||||||
sys.stderr.write(IBCMD_NOUSER_HINT)
|
sys.stderr.write(IBCMD_NOUSER_HINT)
|
||||||
sys.stderr.flush()
|
sys.stderr.flush()
|
||||||
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
|
r = subprocess.run(cmd, input=b"", capture_output=True)
|
||||||
|
r.stdout = decode_platform_bytes(r.stdout)
|
||||||
|
r.stderr = decode_platform_bytes(r.stderr)
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
def describe_exit(code):
|
def describe_exit(code):
|
||||||
@@ -148,11 +409,34 @@ def main():
|
|||||||
parser.add_argument("-InputFile", required=True)
|
parser.add_argument("-InputFile", required=True)
|
||||||
parser.add_argument("-JobsCount", type=int, default=0)
|
parser.add_argument("-JobsCount", type=int, default=0)
|
||||||
parser.add_argument("-UnlockCode", default="")
|
parser.add_argument("-UnlockCode", default="")
|
||||||
args = parser.parse_args()
|
parser.add_argument("-AdditionalV8Arguments", nargs="*", default=[],
|
||||||
|
help="Extra 1cv8 arguments, e.g. /UseHwLicenses+")
|
||||||
|
parser.add_argument("-AdditionalIbcmdArguments", nargs="*", default=[],
|
||||||
|
help="Extra ibcmd arguments in --key=value form")
|
||||||
|
known_opts = {s.lower() for a in parser._actions for s in a.option_strings}
|
||||||
|
argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts)
|
||||||
|
args = ci_parse_args(parser, argv)
|
||||||
|
|
||||||
|
args.V8Path = clean_path(args.V8Path, "-V8Path")
|
||||||
|
args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath")
|
||||||
|
assert_infobase_exists(args.InfoBasePath)
|
||||||
|
args.InputFile = clean_path(args.InputFile, "-InputFile")
|
||||||
|
|
||||||
v8path = resolve_v8path(args.V8Path)
|
v8path = resolve_v8path(args.V8Path)
|
||||||
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
|
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
|
||||||
|
|
||||||
|
# --- Resolve additional arguments for the selected engine ---
|
||||||
|
arg_hints = {
|
||||||
|
"/F": "-InfoBasePath",
|
||||||
|
"/S": "-InfoBaseServer + -InfoBaseRef",
|
||||||
|
"/N": "-UserName",
|
||||||
|
"/P": "-Password",
|
||||||
|
"--db-path": "-InfoBasePath",
|
||||||
|
"--user": "-UserName",
|
||||||
|
"--password": "-Password",
|
||||||
|
}
|
||||||
|
extra_args = resolve_extra_args(engine, v8_extra, ibcmd_extra, arg_hints)
|
||||||
|
|
||||||
# --- Validate connection ---
|
# --- Validate connection ---
|
||||||
if engine == "ibcmd":
|
if engine == "ibcmd":
|
||||||
if not args.InfoBasePath:
|
if not args.InfoBasePath:
|
||||||
@@ -180,16 +464,13 @@ def main():
|
|||||||
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
|
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
|
||||||
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
|
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
|
||||||
arguments.append(f"--data={ib_data}")
|
arguments.append(f"--data={ib_data}")
|
||||||
print(f"Running: ibcmd {_redact(' '.join(arguments), args.Password, args.UserName)}")
|
arguments.extend(extra_args)
|
||||||
|
print(f"Running: ibcmd {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
|
||||||
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
|
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
|
||||||
if result.returncode == 0:
|
if result.returncode == 0:
|
||||||
print(f"Information base restored successfully from: {args.InputFile}")
|
print(f"Information base restored successfully from: {args.InputFile}")
|
||||||
else:
|
else:
|
||||||
print(f"Error restoring information base (code: {result.returncode}){describe_exit(result.returncode)}", file=sys.stderr)
|
print(f"Error restoring information base (code: {result.returncode}){describe_exit(result.returncode)}", file=sys.stderr)
|
||||||
if result.stdout:
|
|
||||||
print(result.stdout)
|
|
||||||
if result.stderr:
|
|
||||||
print(result.stderr, file=sys.stderr)
|
|
||||||
sys.exit(result.returncode)
|
sys.exit(result.returncode)
|
||||||
|
|
||||||
# --- Temp dir ---
|
# --- Temp dir ---
|
||||||
@@ -201,33 +482,30 @@ def main():
|
|||||||
arguments = ["DESIGNER"]
|
arguments = ["DESIGNER"]
|
||||||
|
|
||||||
if args.InfoBaseServer and args.InfoBaseRef:
|
if args.InfoBaseServer and args.InfoBaseRef:
|
||||||
arguments.extend(["/S", f"{args.InfoBaseServer}/{args.InfoBaseRef}"])
|
arguments.extend(["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"'])
|
||||||
else:
|
else:
|
||||||
arguments.extend(["/F", args.InfoBasePath])
|
arguments.extend(["/F", f'"{args.InfoBasePath}"'])
|
||||||
|
|
||||||
if args.UserName:
|
if args.UserName:
|
||||||
arguments.append(f"/N{args.UserName}")
|
arguments.append(f'/N"{args.UserName}"')
|
||||||
if args.Password:
|
if args.Password:
|
||||||
arguments.append(f"/P{args.Password}")
|
arguments.append(f'/P"{args.Password}"')
|
||||||
if args.UnlockCode:
|
if args.UnlockCode:
|
||||||
arguments.append(f"/UC{args.UnlockCode}")
|
arguments.append(f'/UC"{args.UnlockCode}"')
|
||||||
|
|
||||||
arguments.extend(["/RestoreIB", args.InputFile])
|
arguments.extend(["/RestoreIB", f'"{args.InputFile}"'])
|
||||||
if args.JobsCount > 0:
|
if args.JobsCount > 0:
|
||||||
arguments.extend(["-JobsCount", str(args.JobsCount)])
|
arguments.extend(["-JobsCount", str(args.JobsCount)])
|
||||||
|
|
||||||
# --- Output ---
|
# --- Output ---
|
||||||
out_file = os.path.join(temp_dir, "load_dt_log.txt")
|
out_file = os.path.join(temp_dir, "load_dt_log.txt")
|
||||||
arguments.extend(["/Out", out_file])
|
arguments.extend(["/Out", f'"{out_file}"'])
|
||||||
arguments.append("/DisableStartupDialogs")
|
arguments.append("/DisableStartupDialogs")
|
||||||
|
arguments.extend(quote_if_needed(a) for a in extra_args)
|
||||||
|
|
||||||
# --- Execute ---
|
# --- Execute ---
|
||||||
print(f"Running: 1cv8.exe {_redact(' '.join(arguments), args.Password, args.UserName)}")
|
print(f"Running: 1cv8.exe {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
|
||||||
result = subprocess.run(
|
result = run_v8(v8path, arguments)
|
||||||
[v8path] + arguments,
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
)
|
|
||||||
exit_code = result.returncode
|
exit_code = result.returncode
|
||||||
|
|
||||||
# --- Result ---
|
# --- Result ---
|
||||||
@@ -247,6 +525,7 @@ def main():
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
print_platform_output(result)
|
||||||
sys.exit(exit_code)
|
sys.exit(exit_code)
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
|
|||||||
@@ -59,6 +59,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-git.ps1" <
|
|||||||
| `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` |
|
| `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` |
|
||||||
| `-DryRun` | нет | Только показать что будет загружено (без загрузки) |
|
| `-DryRun` | нет | Только показать что будет загружено (без загрузки) |
|
||||||
| `-UpdateDB` | нет | После загрузки сразу обновить конфигурацию БД (`/UpdateDBCfg`) |
|
| `-UpdateDB` | нет | После загрузки сразу обновить конфигурацию БД (`/UpdateDBCfg`) |
|
||||||
|
| `-AdditionalV8Arguments <список>` | нет | Доп. аргументы запуска `1cv8.exe` через запятую, напр. `/UseHwLicenses+` |
|
||||||
|
| `-AdditionalIbcmdArguments <список>` | нет | Доп. аргументы `ibcmd` через запятую, в форме `--ключ=значение` |
|
||||||
|
|
||||||
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
|
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# db-load-git v1.15 — Load Git changes into 1C database
|
# db-load-git v1.19 — Load Git changes into 1C database
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||||
<#
|
<#
|
||||||
@@ -48,6 +48,12 @@
|
|||||||
.PARAMETER DryRun
|
.PARAMETER DryRun
|
||||||
Только показать что будет загружено (без загрузки)
|
Только показать что будет загружено (без загрузки)
|
||||||
|
|
||||||
|
.PARAMETER AdditionalV8Arguments
|
||||||
|
Дополнительные аргументы запуска 1cv8.exe (например /UseHwLicenses+)
|
||||||
|
|
||||||
|
.PARAMETER AdditionalIbcmdArguments
|
||||||
|
Дополнительные аргументы запуска ibcmd (форма --ключ=значение)
|
||||||
|
|
||||||
.EXAMPLE
|
.EXAMPLE
|
||||||
.\db-load-git.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Source All
|
.\db-load-git.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Source All
|
||||||
|
|
||||||
@@ -102,7 +108,13 @@ param(
|
|||||||
[switch]$DryRun,
|
[switch]$DryRun,
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
[Parameter(Mandatory=$false)]
|
||||||
[switch]$UpdateDB
|
[switch]$UpdateDB,
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[string[]]$AdditionalV8Arguments = @(),
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[string[]]$AdditionalIbcmdArguments = @()
|
||||||
)
|
)
|
||||||
|
|
||||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
@@ -115,6 +127,128 @@ function Protect-Secrets {
|
|||||||
return $Text
|
return $Text
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- Additional platform arguments ---
|
||||||
|
$script:V8OwnedKeys = @(
|
||||||
|
'DESIGNER', 'ENTERPRISE', 'CREATEINFOBASE', 'CONFIG',
|
||||||
|
'/F', '/S', '/N', '/P', '/Out', '/DisableStartupDialogs',
|
||||||
|
'/UseTemplate', '/AddToList', '/Execute', '/C', '/URL', '/UC',
|
||||||
|
'/DumpIB', '/RestoreIB', '/DumpCfg', '/LoadCfg',
|
||||||
|
'/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg',
|
||||||
|
'/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles'
|
||||||
|
)
|
||||||
|
$script:IbcmdOwnedKeys = @(
|
||||||
|
'--db-path', '--data', '--out', '--file', '--load', '--restore',
|
||||||
|
'--import', '--export', '--apply', '--force', '--create-database',
|
||||||
|
'--user', '--password'
|
||||||
|
)
|
||||||
|
$script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP')
|
||||||
|
$script:IbcmdSecretKeys = @('--password', '--token', '--db-pwd')
|
||||||
|
|
||||||
|
function Test-ArgKeyMatch {
|
||||||
|
# A token matches a key when it equals the key, or starts with it and the next
|
||||||
|
# character is not a letter — catches glued /N"user" and --password=x, while
|
||||||
|
# keeping /ClearCache distinct from /C.
|
||||||
|
param([string]$Token, [string]$Key)
|
||||||
|
if ($Token.Length -lt $Key.Length) { return $false }
|
||||||
|
if (-not $Token.Substring(0, $Key.Length).Equals($Key, [System.StringComparison]::OrdinalIgnoreCase)) { return $false }
|
||||||
|
if ($Token.Length -eq $Key.Length) { return $true }
|
||||||
|
return -not [char]::IsLetter($Token[$Key.Length])
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-ProjectExtraArgs {
|
||||||
|
# v8args / ibcmdargs from .v8-project.json — same upward walk as v8path.
|
||||||
|
param([string]$Name)
|
||||||
|
$dir = (Get-Location).Path
|
||||||
|
while ($dir) {
|
||||||
|
$pf = Join-Path $dir ".v8-project.json"
|
||||||
|
if (Test-Path $pf) {
|
||||||
|
try {
|
||||||
|
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||||
|
if ($j.$Name) { return @($j.$Name | ForEach-Object { [string]$_ }) }
|
||||||
|
} catch {}
|
||||||
|
return @()
|
||||||
|
}
|
||||||
|
$parent = Split-Path $dir -Parent
|
||||||
|
if (-not $parent -or $parent -eq $dir) { break }
|
||||||
|
$dir = $parent
|
||||||
|
}
|
||||||
|
return @()
|
||||||
|
}
|
||||||
|
|
||||||
|
function Assert-ExtraArgs {
|
||||||
|
# The platform accepts only one batch operation, and a duplicate connection or
|
||||||
|
# output key fails with an opaque 1C error — reject what the skill owns itself.
|
||||||
|
param([string[]]$ExtraArgs, [string]$Engine, [hashtable]$Hints)
|
||||||
|
$paramName = if ($Engine -eq 'ibcmd') { '-AdditionalIbcmdArguments' } else { '-AdditionalV8Arguments' }
|
||||||
|
$owned = if ($Engine -eq 'ibcmd') { $script:IbcmdOwnedKeys } else { $script:V8OwnedKeys }
|
||||||
|
foreach ($tok in $ExtraArgs) {
|
||||||
|
if ($Engine -eq 'ibcmd' -and $tok -notmatch '^-') {
|
||||||
|
Write-Host "Error: '$tok' is a positional token — pass values as --key=value ($paramName cannot extend the ibcmd command)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
foreach ($k in $owned) {
|
||||||
|
if (Test-ArgKeyMatch $tok $k) {
|
||||||
|
$hint = ''
|
||||||
|
if ($Hints -and $Hints.ContainsKey($k)) { $hint = " (use $($Hints[$k]))" }
|
||||||
|
Write-Host "Error: $k is controlled by the skill and cannot be passed via $paramName$hint" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Resolve-ExtraArgs {
|
||||||
|
# Pick the argument list for the selected engine and validate it. An explicitly passed
|
||||||
|
# parameter for the other engine is an error; the same keys coming from .v8-project.json
|
||||||
|
# simply do not apply — a project may describe both engines.
|
||||||
|
param([string]$Engine, [string[]]$V8Extra, [string[]]$IbcmdExtra, [hashtable]$Hints)
|
||||||
|
# powershell.exe -File — how skills are invoked — cannot bind an array parameter:
|
||||||
|
# space-separated values spill into positional ones, a comma-joined list arrives as a
|
||||||
|
# single token. So accept the repo's list convention (comma-separated) and split here;
|
||||||
|
# a native array call keeps working. A value containing a comma is not supported.
|
||||||
|
$V8Extra = @($V8Extra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
|
||||||
|
$IbcmdExtra = @($IbcmdExtra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
|
||||||
|
if ($Engine -eq 'ibcmd' -and $V8Extra.Count -gt 0) {
|
||||||
|
Write-Host "Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd (use -AdditionalIbcmdArguments)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
if ($Engine -ne 'ibcmd' -and $IbcmdExtra.Count -gt 0) {
|
||||||
|
Write-Host "Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 (use -AdditionalV8Arguments)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
if ($Engine -eq 'ibcmd') {
|
||||||
|
$extra = @(Get-ProjectExtraArgs 'ibcmdargs') + @($IbcmdExtra)
|
||||||
|
} else {
|
||||||
|
$extra = @(Get-ProjectExtraArgs 'v8args') + @($V8Extra)
|
||||||
|
}
|
||||||
|
if ($extra.Count -gt 0) { Assert-ExtraArgs $extra $Engine $Hints }
|
||||||
|
# Plain return, no comma trick: the caller re-collects with @(...), and ,@() there
|
||||||
|
# would nest the array — the tokens would then be glued into one argument.
|
||||||
|
return $extra
|
||||||
|
}
|
||||||
|
|
||||||
|
function Format-ArgsForDisplay {
|
||||||
|
# Redact values of secret-prone keys in glued, =-joined and separate forms.
|
||||||
|
# Matching here is a plain prefix (no letter rule): over-masking costs nothing,
|
||||||
|
# a leaked password does.
|
||||||
|
param([string[]]$ArgList, [string]$Engine)
|
||||||
|
$keys = if ($Engine -eq 'ibcmd') { $script:IbcmdSecretKeys } else { $script:V8SecretKeys }
|
||||||
|
$res = @()
|
||||||
|
$maskNext = $false
|
||||||
|
foreach ($tok in $ArgList) {
|
||||||
|
if ($maskNext) { $res += '***'; $maskNext = $false; continue }
|
||||||
|
$hit = $null
|
||||||
|
foreach ($k in $keys) {
|
||||||
|
if ($tok.Length -ge $k.Length -and $tok.Substring(0, $k.Length).Equals($k, [System.StringComparison]::OrdinalIgnoreCase)) { $hit = $k; break }
|
||||||
|
}
|
||||||
|
if (-not $hit) { $res += $tok; continue }
|
||||||
|
if ($tok.Length -eq $hit.Length) { $res += $tok; $maskNext = $true }
|
||||||
|
elseif ($tok[$hit.Length] -eq '=') { $res += ($hit + '=***') }
|
||||||
|
else { $res += ($hit + '***') }
|
||||||
|
}
|
||||||
|
return ,$res
|
||||||
|
}
|
||||||
|
|
||||||
function Get-ExitAnnotation {
|
function Get-ExitAnnotation {
|
||||||
# Annotate an abnormal process exit code so a crash isn't reported as a bare number.
|
# Annotate an abnormal process exit code so a crash isn't reported as a bare number.
|
||||||
# A batch DESIGNER that crashes (e.g. missing license) may leave the infobase locked or
|
# A batch DESIGNER that crashes (e.g. missing license) may leave the infobase locked or
|
||||||
@@ -191,32 +325,75 @@ if (-not $DryRun) {
|
|||||||
# --- Detect engine + validate connection (skip if DryRun) ---
|
# --- Detect engine + validate connection (skip if DryRun) ---
|
||||||
$engine = "1cv8"
|
$engine = "1cv8"
|
||||||
if (-not $DryRun) {
|
if (-not $DryRun) {
|
||||||
function Invoke-IbcmdProcess {
|
function ConvertFrom-PlatformBytes {
|
||||||
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
|
# ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
|
||||||
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
|
# OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes — guessing
|
||||||
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
|
# one of them outright mangles Cyrillic.
|
||||||
param([string]$Exe, [string[]]$IbArgs)
|
param([byte[]]$Bytes)
|
||||||
|
if (-not $Bytes -or $Bytes.Length -eq 0) { return '' }
|
||||||
|
try {
|
||||||
|
$strict = New-Object System.Text.UTF8Encoding($false, $true)
|
||||||
|
return $strict.GetString($Bytes)
|
||||||
|
} catch {
|
||||||
|
return [System.Text.Encoding]::GetEncoding(866).GetString($Bytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-PlatformProcess {
|
||||||
|
# Run the platform non-interactively and capture its console output. A closed stdin pipe
|
||||||
|
# (EOF) makes an auth prompt fast-fail instead of hanging; capturing keeps the child's
|
||||||
|
# text out of our stream until we print it labelled (and out of the wrong encoding).
|
||||||
|
# Returns @{ Output; ExitCode }.
|
||||||
|
#
|
||||||
|
# Quoting differs by engine, so the caller says which it built:
|
||||||
|
# ibcmd — tokens are bare (--db-path=C:\a b), the whole token gets quoted here;
|
||||||
|
# 1cv8 — -PreQuoted: the caller already put quotes inside the token (File="C:\a b"),
|
||||||
|
# which is where 1C's own parser expects them; quoting again breaks the value.
|
||||||
|
param([string]$Exe, [string[]]$ProcArgs, [switch]$PreQuoted)
|
||||||
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
||||||
$psi.FileName = $Exe
|
$psi.FileName = $Exe
|
||||||
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
|
$psi.Arguments = if ($PreQuoted) {
|
||||||
|
$ProcArgs -join ' '
|
||||||
|
} else {
|
||||||
|
($ProcArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
|
||||||
|
}
|
||||||
$psi.UseShellExecute = $false
|
$psi.UseShellExecute = $false
|
||||||
$psi.CreateNoWindow = $true
|
$psi.CreateNoWindow = $true
|
||||||
$psi.RedirectStandardInput = $true
|
$psi.RedirectStandardInput = $true
|
||||||
$psi.RedirectStandardOutput = $true
|
$psi.RedirectStandardOutput = $true
|
||||||
$psi.RedirectStandardError = $true
|
$psi.RedirectStandardError = $true
|
||||||
try {
|
|
||||||
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
|
|
||||||
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
|
|
||||||
} catch {}
|
|
||||||
$p = [System.Diagnostics.Process]::Start($psi)
|
$p = [System.Diagnostics.Process]::Start($psi)
|
||||||
$p.StandardInput.Close()
|
$p.StandardInput.Close()
|
||||||
$out = $p.StandardOutput.ReadToEnd()
|
# stderr is drained in parallel: reading the streams one after another deadlocks
|
||||||
$err = $p.StandardError.ReadToEnd()
|
# as soon as the other one fills its pipe buffer.
|
||||||
|
$errMs = New-Object System.IO.MemoryStream
|
||||||
|
$errTask = $p.StandardError.BaseStream.CopyToAsync($errMs)
|
||||||
|
$outMs = New-Object System.IO.MemoryStream
|
||||||
|
$p.StandardOutput.BaseStream.CopyTo($outMs)
|
||||||
|
$errTask.Wait()
|
||||||
$p.WaitForExit()
|
$p.WaitForExit()
|
||||||
|
$out = ConvertFrom-PlatformBytes $outMs.ToArray()
|
||||||
|
$err = ConvertFrom-PlatformBytes $errMs.ToArray()
|
||||||
if ($err) { $out += $err }
|
if ($err) { $out += $err }
|
||||||
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
|
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Write-PlatformOutput {
|
||||||
|
# Print what the platform wrote to the console as its own labelled block. Silence stays
|
||||||
|
# silent: in batch mode 1cv8 reports through /Out and prints nothing here.
|
||||||
|
param([string]$Text)
|
||||||
|
if (-not $Text) { return }
|
||||||
|
$t = $Text.TrimEnd()
|
||||||
|
if (-not $t) { return }
|
||||||
|
$limit = 65536
|
||||||
|
if ($t.Length -gt $limit) {
|
||||||
|
$t = "[... обрезано, показаны последние $limit символов ...]`r`n" + $t.Substring($t.Length - $limit)
|
||||||
|
}
|
||||||
|
Write-Host "--- Вывод платформы ---"
|
||||||
|
Write-Host $t
|
||||||
|
Write-Host "--- End ---"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
||||||
if ($engine -eq "ibcmd") {
|
if ($engine -eq "ibcmd") {
|
||||||
@@ -230,6 +407,10 @@ function Invoke-IbcmdProcess {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- Resolve additional arguments for the selected engine ---
|
||||||
|
$argHints = @{ '/F' = '-InfoBasePath'; '/S' = '-InfoBaseServer + -InfoBaseRef'; '/N' = '-UserName'; '/P' = '-Password'; '--db-path' = '-InfoBasePath'; '--user' = '-UserName'; '--password' = '-Password' }
|
||||||
|
$extraArgs = @(Resolve-ExtraArgs $engine $AdditionalV8Arguments $AdditionalIbcmdArguments $argHints)
|
||||||
|
|
||||||
# --- Validate config dir ---
|
# --- Validate config dir ---
|
||||||
if (-not (Test-Path $ConfigDir)) {
|
if (-not (Test-Path $ConfigDir)) {
|
||||||
Write-Host "Error: config directory not found: $ConfigDir" -ForegroundColor Red
|
Write-Host "Error: config directory not found: $ConfigDir" -ForegroundColor Red
|
||||||
@@ -396,24 +577,26 @@ try {
|
|||||||
if ($UserName) { $arguments += "--user=$UserName" }
|
if ($UserName) { $arguments += "--user=$UserName" }
|
||||||
if ($Password) { $arguments += "--password=$Password" }
|
if ($Password) { $arguments += "--password=$Password" }
|
||||||
$arguments += "--data=$tempDir"
|
$arguments += "--data=$tempDir"
|
||||||
Write-Host "Running: ibcmd $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))"
|
$arguments += $extraArgs
|
||||||
$__ib = Invoke-IbcmdProcess $V8Path $arguments
|
Write-Host "Running: ibcmd $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
|
||||||
|
$__ib = Invoke-PlatformProcess $V8Path $arguments
|
||||||
$output = $__ib.Output
|
$output = $__ib.Output
|
||||||
$exitCode = $__ib.ExitCode
|
$exitCode = $__ib.ExitCode
|
||||||
if ($exitCode -ne 0) {
|
if ($exitCode -ne 0) {
|
||||||
Write-Host "Error loading changes (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
|
Write-Host "Error loading changes (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
|
||||||
if ($output) { Write-Host ($output | Out-String) }
|
Write-PlatformOutput $output
|
||||||
exit $exitCode
|
exit $exitCode
|
||||||
}
|
}
|
||||||
Write-Host "Changes loaded successfully ($($configFiles.Count) files)" -ForegroundColor Green
|
Write-Host "Changes loaded successfully ($($configFiles.Count) files)" -ForegroundColor Green
|
||||||
if ($output) { Write-Host ($output | Out-String) }
|
Write-PlatformOutput $output
|
||||||
if ($UpdateDB) {
|
if ($UpdateDB) {
|
||||||
$applyArgs = @("infobase", "config", "apply", "--db-path=$InfoBasePath", "--force")
|
$applyArgs = @("infobase", "config", "apply", "--db-path=$InfoBasePath", "--force")
|
||||||
if ($UserName) { $applyArgs += "--user=$UserName" }
|
if ($UserName) { $applyArgs += "--user=$UserName" }
|
||||||
if ($Password) { $applyArgs += "--password=$Password" }
|
if ($Password) { $applyArgs += "--password=$Password" }
|
||||||
$applyArgs += "--data=$tempDir"
|
$applyArgs += "--data=$tempDir"
|
||||||
Write-Host "Running: ibcmd $(Protect-Secrets ($applyArgs -join ' ') @($Password, $UserName))"
|
$applyArgs += $extraArgs
|
||||||
$__ib = Invoke-IbcmdProcess $V8Path $applyArgs
|
Write-Host "Running: ibcmd $(Protect-Secrets ((Format-ArgsForDisplay $applyArgs $engine) -join ' ') @($Password, $UserName))"
|
||||||
|
$__ib = Invoke-PlatformProcess $V8Path $applyArgs
|
||||||
$applyOut = $__ib.Output
|
$applyOut = $__ib.Output
|
||||||
$exitCode = $__ib.ExitCode
|
$exitCode = $__ib.ExitCode
|
||||||
if ($exitCode -eq 0) {
|
if ($exitCode -eq 0) {
|
||||||
@@ -421,7 +604,7 @@ try {
|
|||||||
} else {
|
} else {
|
||||||
Write-Host "Error updating database configuration (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
|
Write-Host "Error updating database configuration (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
|
||||||
}
|
}
|
||||||
if ($applyOut) { Write-Host ($applyOut | Out-String) }
|
Write-PlatformOutput $applyOut
|
||||||
}
|
}
|
||||||
exit $exitCode
|
exit $exitCode
|
||||||
}
|
}
|
||||||
@@ -466,14 +649,15 @@ try {
|
|||||||
$outFile = Join-Path $tempDir "load_log.txt"
|
$outFile = Join-Path $tempDir "load_log.txt"
|
||||||
$arguments += "/Out", "`"$outFile`""
|
$arguments += "/Out", "`"$outFile`""
|
||||||
$arguments += "/DisableStartupDialogs"
|
$arguments += "/DisableStartupDialogs"
|
||||||
|
$arguments += $extraArgs
|
||||||
|
|
||||||
# --- Execute ---
|
# --- Execute ---
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
Write-Host "Executing partial configuration load..."
|
Write-Host "Executing partial configuration load..."
|
||||||
Write-Host "Running: 1cv8.exe $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))"
|
Write-Host "Running: 1cv8.exe $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
|
||||||
|
|
||||||
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
|
$__v8 = Invoke-PlatformProcess $V8Path $arguments -PreQuoted
|
||||||
$exitCode = $process.ExitCode
|
$exitCode = $__v8.ExitCode
|
||||||
|
|
||||||
# --- Result ---
|
# --- Result ---
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
@@ -491,6 +675,7 @@ try {
|
|||||||
Write-Host "--- End ---"
|
Write-Host "--- End ---"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Write-PlatformOutput $__v8.Output
|
||||||
|
|
||||||
exit $exitCode
|
exit $exitCode
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# db-load-git v1.15 — Load Git changes into 1C database
|
# db-load-git v1.19 — Load Git changes into 1C database
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -14,6 +14,28 @@ import subprocess
|
|||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
|
# Регистронезависимый ввод — паритет с 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)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def _find_project_v8path():
|
def _find_project_v8path():
|
||||||
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
||||||
@@ -36,6 +58,163 @@ def _find_project_v8path():
|
|||||||
d = parent
|
d = parent
|
||||||
|
|
||||||
|
|
||||||
|
# --- Additional platform arguments ---
|
||||||
|
V8_OWNED_KEYS = [
|
||||||
|
"DESIGNER", "ENTERPRISE", "CREATEINFOBASE", "CONFIG",
|
||||||
|
"/F", "/S", "/N", "/P", "/Out", "/DisableStartupDialogs",
|
||||||
|
"/UseTemplate", "/AddToList", "/Execute", "/C", "/URL", "/UC",
|
||||||
|
"/DumpIB", "/RestoreIB", "/DumpCfg", "/LoadCfg",
|
||||||
|
"/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg",
|
||||||
|
"/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles",
|
||||||
|
]
|
||||||
|
IBCMD_OWNED_KEYS = [
|
||||||
|
"--db-path", "--data", "--out", "--file", "--load", "--restore",
|
||||||
|
"--import", "--export", "--apply", "--force", "--create-database",
|
||||||
|
"--user", "--password",
|
||||||
|
]
|
||||||
|
V8_SECRET_KEYS = ["/P", "/UC", "/WSP", "/AWSP"]
|
||||||
|
IBCMD_SECRET_KEYS = ["--password", "--token", "--db-pwd"]
|
||||||
|
|
||||||
|
|
||||||
|
def arg_key_match(token, key):
|
||||||
|
"""Token matches a key when it equals it, or starts with it and the next character
|
||||||
|
is not a letter — catches glued /N"user" and --password=x, while keeping
|
||||||
|
/ClearCache distinct from /C."""
|
||||||
|
if len(token) < len(key):
|
||||||
|
return False
|
||||||
|
if token[: len(key)].lower() != key.lower():
|
||||||
|
return False
|
||||||
|
if len(token) == len(key):
|
||||||
|
return True
|
||||||
|
return not token[len(key)].isalpha()
|
||||||
|
|
||||||
|
|
||||||
|
def project_extra_args(name):
|
||||||
|
"""v8args / ibcmdargs from .v8-project.json — same upward walk as v8path."""
|
||||||
|
d = os.getcwd()
|
||||||
|
while True:
|
||||||
|
pf = os.path.join(d, ".v8-project.json")
|
||||||
|
if os.path.isfile(pf):
|
||||||
|
try:
|
||||||
|
with open(pf, encoding="utf-8-sig") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
v = data.get(name)
|
||||||
|
if v:
|
||||||
|
return [str(x) for x in v]
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return []
|
||||||
|
parent = os.path.dirname(d)
|
||||||
|
if parent == d:
|
||||||
|
return []
|
||||||
|
d = parent
|
||||||
|
|
||||||
|
|
||||||
|
def assert_extra_args(extra, engine, hints):
|
||||||
|
"""The platform accepts only one batch operation, and a duplicate connection or
|
||||||
|
output key fails with an opaque 1C error — reject what the skill owns itself."""
|
||||||
|
param = "-AdditionalIbcmdArguments" if engine == "ibcmd" else "-AdditionalV8Arguments"
|
||||||
|
owned = IBCMD_OWNED_KEYS if engine == "ibcmd" else V8_OWNED_KEYS
|
||||||
|
for tok in extra:
|
||||||
|
if engine == "ibcmd" and not tok.startswith("-"):
|
||||||
|
print(
|
||||||
|
f"Error: '{tok}' is a positional token — pass values as --key=value "
|
||||||
|
f"({param} cannot extend the ibcmd command)",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
for k in owned:
|
||||||
|
if arg_key_match(tok, k):
|
||||||
|
hint = f" (use {hints[k]})" if hints and k in hints else ""
|
||||||
|
print(
|
||||||
|
f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def format_args_for_display(arglist, engine):
|
||||||
|
"""Redact values of secret-prone keys in glued, =-joined and separate forms.
|
||||||
|
Matching here is a plain prefix (no letter rule): over-masking costs nothing,
|
||||||
|
a leaked password does."""
|
||||||
|
keys = IBCMD_SECRET_KEYS if engine == "ibcmd" else V8_SECRET_KEYS
|
||||||
|
res = []
|
||||||
|
mask_next = False
|
||||||
|
for tok in arglist:
|
||||||
|
if mask_next:
|
||||||
|
res.append("***")
|
||||||
|
mask_next = False
|
||||||
|
continue
|
||||||
|
hit = None
|
||||||
|
for k in keys:
|
||||||
|
if tok[: len(k)].lower() == k.lower():
|
||||||
|
hit = k
|
||||||
|
break
|
||||||
|
if hit is None:
|
||||||
|
res.append(tok)
|
||||||
|
elif len(tok) == len(hit):
|
||||||
|
res.append(tok)
|
||||||
|
mask_next = True
|
||||||
|
elif tok[len(hit)] == "=":
|
||||||
|
res.append(hit + "=***")
|
||||||
|
else:
|
||||||
|
res.append(hit + "***")
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
def extract_extra_args(argv, known_opts):
|
||||||
|
"""argparse refuses values that start with '-' (every ibcmd key does), so pull the two
|
||||||
|
escape-hatch lists out of argv by hand: after the flag, take everything up to the next
|
||||||
|
declared skill option. Returns (remaining_argv, v8_extra, ibcmd_extra)."""
|
||||||
|
rest, v8, ibcmd = [], [], []
|
||||||
|
i = 0
|
||||||
|
while i < len(argv):
|
||||||
|
low = argv[i].lower()
|
||||||
|
if low in ("-additionalv8arguments", "-additionalibcmdarguments"):
|
||||||
|
target = v8 if low == "-additionalv8arguments" else ibcmd
|
||||||
|
i += 1
|
||||||
|
while i < len(argv) and argv[i].lower() not in known_opts:
|
||||||
|
target.append(argv[i])
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
rest.append(argv[i])
|
||||||
|
i += 1
|
||||||
|
return rest, v8, ibcmd
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints):
|
||||||
|
"""Pick the argument list for the selected engine and validate it. An explicitly
|
||||||
|
passed parameter for the other engine is an error; the same keys coming from
|
||||||
|
.v8-project.json simply do not apply — a project may describe both engines.
|
||||||
|
|
||||||
|
Comma-separated elements are split apart: PowerShell's -File cannot bind an array,
|
||||||
|
so that form is the documented one and both ports must accept it. A value containing
|
||||||
|
a comma is not supported."""
|
||||||
|
v8_extra = [p for tok in v8_extra for p in str(tok).split(",") if p]
|
||||||
|
ibcmd_extra = [p for tok in ibcmd_extra for p in str(tok).split(",") if p]
|
||||||
|
if engine == "ibcmd" and v8_extra:
|
||||||
|
print(
|
||||||
|
"Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd "
|
||||||
|
"(use -AdditionalIbcmdArguments)",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
if engine != "ibcmd" and ibcmd_extra:
|
||||||
|
print(
|
||||||
|
"Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 "
|
||||||
|
"(use -AdditionalV8Arguments)",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
if engine == "ibcmd":
|
||||||
|
extra = project_extra_args("ibcmdargs") + list(ibcmd_extra)
|
||||||
|
else:
|
||||||
|
extra = project_extra_args("v8args") + list(v8_extra)
|
||||||
|
if extra:
|
||||||
|
assert_extra_args(extra, engine, hints)
|
||||||
|
return extra
|
||||||
|
|
||||||
|
|
||||||
def _version_dir(p):
|
def _version_dir(p):
|
||||||
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
|
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
|
||||||
parent = os.path.dirname(p)
|
parent = os.path.dirname(p)
|
||||||
@@ -86,6 +265,85 @@ IBCMD_NOUSER_HINT = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def decode_platform_bytes(data):
|
||||||
|
"""ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
|
||||||
|
OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes — the locale
|
||||||
|
code page (what text=True uses) mangles both."""
|
||||||
|
if not data:
|
||||||
|
return ""
|
||||||
|
try:
|
||||||
|
return data.decode("utf-8")
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
return data.decode("cp866", errors="replace")
|
||||||
|
|
||||||
|
|
||||||
|
def assert_infobase_exists(path):
|
||||||
|
"""These skills work on a ready infobase. Saying so up front beats the platform's
|
||||||
|
"Неверные или отсутствующие параметры соединения" after a launch."""
|
||||||
|
if not path:
|
||||||
|
return
|
||||||
|
if not os.path.isfile(os.path.join(path, "1Cv8.1CD")):
|
||||||
|
print(f"Error: information base not found at {path} (no 1Cv8.1CD)", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def clean_path(value, param=""):
|
||||||
|
"""Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
|
||||||
|
surrounding quotes that survived shell parsing, a trailing separator. A quote left
|
||||||
|
inside afterwards cannot be part of a real path — reject it by name instead of letting
|
||||||
|
1C answer with its opaque "Неверные или отсутствующие параметры соединения"."""
|
||||||
|
if not value:
|
||||||
|
return value
|
||||||
|
v = value.strip()
|
||||||
|
if len(v) >= 2 and v[0] == v[-1] and v[0] in "\"'":
|
||||||
|
v = v[1:-1].strip()
|
||||||
|
if len(v) > 3 and v[-1] in "\\/":
|
||||||
|
v = v[:-1]
|
||||||
|
if '"' in v:
|
||||||
|
print(f"Error: {param or 'path'} contains a quote character: {value}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
return v
|
||||||
|
|
||||||
|
|
||||||
|
def quote_if_needed(token):
|
||||||
|
"""Extra arguments come from the caller unquoted; the 1cv8 command line is joined
|
||||||
|
verbatim, so a token with a space needs quotes of its own."""
|
||||||
|
if token and (" " in token or "\t" in token) and '"' not in token:
|
||||||
|
return f'"{token}"'
|
||||||
|
return token
|
||||||
|
|
||||||
|
|
||||||
|
def run_v8(v8path, arguments):
|
||||||
|
"""Run 1cv8 in batch mode and capture its console output.
|
||||||
|
|
||||||
|
The arguments carry their own quotes inside the value (File="C:\\a b") — that is where
|
||||||
|
1C's parser expects them, on Windows and on *nix alike. Windows list2cmdline would
|
||||||
|
escape those quotes, so there the command line is handed over ready-made.
|
||||||
|
"""
|
||||||
|
if os.name == "nt":
|
||||||
|
cmd = '"' + v8path + '" ' + " ".join(arguments)
|
||||||
|
else:
|
||||||
|
cmd = [v8path] + arguments
|
||||||
|
r = subprocess.run(cmd, input=b"", capture_output=True)
|
||||||
|
r.stdout = decode_platform_bytes(r.stdout)
|
||||||
|
r.stderr = decode_platform_bytes(r.stderr)
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
|
def print_platform_output(result):
|
||||||
|
"""Print what the platform wrote to the console as its own labelled block. Silence stays
|
||||||
|
silent: in batch mode 1cv8 reports through /Out and prints nothing here."""
|
||||||
|
text = ((result.stdout or "") + (result.stderr or "")).rstrip()
|
||||||
|
if not text:
|
||||||
|
return
|
||||||
|
limit = 65536
|
||||||
|
if len(text) > limit:
|
||||||
|
text = f"[... обрезано, показаны последние {limit} символов ...]\n" + text[-limit:]
|
||||||
|
print("--- Вывод платформы ---")
|
||||||
|
print(text)
|
||||||
|
print("--- End ---")
|
||||||
|
|
||||||
|
|
||||||
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
||||||
"""Run an ibcmd command non-interactively.
|
"""Run an ibcmd command non-interactively.
|
||||||
|
|
||||||
@@ -96,7 +354,10 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
|||||||
if warn_no_user and os.name == "nt" and not has_username:
|
if warn_no_user and os.name == "nt" and not has_username:
|
||||||
sys.stderr.write(IBCMD_NOUSER_HINT)
|
sys.stderr.write(IBCMD_NOUSER_HINT)
|
||||||
sys.stderr.flush()
|
sys.stderr.flush()
|
||||||
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
|
r = subprocess.run(cmd, input=b"", capture_output=True)
|
||||||
|
r.stdout = decode_platform_bytes(r.stdout)
|
||||||
|
r.stderr = decode_platform_bytes(r.stderr)
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
def get_object_xml_from_subfile(relative_path):
|
def get_object_xml_from_subfile(relative_path):
|
||||||
@@ -185,7 +446,18 @@ def main():
|
|||||||
)
|
)
|
||||||
parser.add_argument("-DryRun", action="store_true", help="Only show what would be loaded (no actual load)")
|
parser.add_argument("-DryRun", action="store_true", help="Only show what would be loaded (no actual load)")
|
||||||
parser.add_argument("-UpdateDB", action="store_true", help="Also update database configuration after load")
|
parser.add_argument("-UpdateDB", action="store_true", help="Also update database configuration after load")
|
||||||
args = parser.parse_args()
|
parser.add_argument("-AdditionalV8Arguments", nargs="*", default=[],
|
||||||
|
help="Extra 1cv8 arguments, e.g. /UseHwLicenses+")
|
||||||
|
parser.add_argument("-AdditionalIbcmdArguments", nargs="*", default=[],
|
||||||
|
help="Extra ibcmd arguments in --key=value form")
|
||||||
|
known_opts = {s.lower() for a in parser._actions for s in a.option_strings}
|
||||||
|
argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts)
|
||||||
|
args = ci_parse_args(parser, argv)
|
||||||
|
|
||||||
|
args.V8Path = clean_path(args.V8Path, "-V8Path")
|
||||||
|
args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath")
|
||||||
|
assert_infobase_exists(args.InfoBasePath)
|
||||||
|
args.ConfigDir = clean_path(args.ConfigDir, "-ConfigDir")
|
||||||
|
|
||||||
# --- Resolve V8Path (skip if DryRun) ---
|
# --- Resolve V8Path (skip if DryRun) ---
|
||||||
v8path = None
|
v8path = None
|
||||||
@@ -204,6 +476,18 @@ def main():
|
|||||||
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
# --- Resolve additional arguments for the selected engine ---
|
||||||
|
arg_hints = {
|
||||||
|
"/F": "-InfoBasePath",
|
||||||
|
"/S": "-InfoBaseServer + -InfoBaseRef",
|
||||||
|
"/N": "-UserName",
|
||||||
|
"/P": "-Password",
|
||||||
|
"--db-path": "-InfoBasePath",
|
||||||
|
"--user": "-UserName",
|
||||||
|
"--password": "-Password",
|
||||||
|
}
|
||||||
|
extra_args = resolve_extra_args(engine, v8_extra, ibcmd_extra, arg_hints)
|
||||||
|
|
||||||
# --- Validate config dir ---
|
# --- Validate config dir ---
|
||||||
if not os.path.exists(args.ConfigDir):
|
if not os.path.exists(args.ConfigDir):
|
||||||
print(f"Error: config directory not found: {args.ConfigDir}", file=sys.stderr)
|
print(f"Error: config directory not found: {args.ConfigDir}", file=sys.stderr)
|
||||||
@@ -340,18 +624,13 @@ def main():
|
|||||||
if args.Password:
|
if args.Password:
|
||||||
arguments.append(f"--password={args.Password}")
|
arguments.append(f"--password={args.Password}")
|
||||||
arguments.append(f"--data={ib_data}")
|
arguments.append(f"--data={ib_data}")
|
||||||
print(f"Running: ibcmd {_redact(' '.join(arguments), args.Password, args.UserName)}")
|
arguments.extend(extra_args)
|
||||||
|
print(f"Running: ibcmd {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
|
||||||
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
|
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
|
||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
print(f"Error loading changes (code: {result.returncode}){describe_exit(result.returncode)}", file=sys.stderr)
|
print(f"Error loading changes (code: {result.returncode}){describe_exit(result.returncode)}", file=sys.stderr)
|
||||||
if result.stdout:
|
|
||||||
print(result.stdout)
|
|
||||||
if result.stderr:
|
|
||||||
print(result.stderr, file=sys.stderr)
|
|
||||||
sys.exit(result.returncode)
|
sys.exit(result.returncode)
|
||||||
print(f"Changes loaded successfully ({len(config_files)} files)")
|
print(f"Changes loaded successfully ({len(config_files)} files)")
|
||||||
if result.stdout:
|
|
||||||
print(result.stdout)
|
|
||||||
exit_code = 0
|
exit_code = 0
|
||||||
if args.UpdateDB:
|
if args.UpdateDB:
|
||||||
apply_args = ["infobase", "config", "apply", f"--db-path={args.InfoBasePath}", "--force"]
|
apply_args = ["infobase", "config", "apply", f"--db-path={args.InfoBasePath}", "--force"]
|
||||||
@@ -360,17 +639,15 @@ def main():
|
|||||||
if args.Password:
|
if args.Password:
|
||||||
apply_args.append(f"--password={args.Password}")
|
apply_args.append(f"--password={args.Password}")
|
||||||
apply_args.append(f"--data={ib_data}")
|
apply_args.append(f"--data={ib_data}")
|
||||||
print(f"Running: ibcmd {_redact(' '.join(apply_args), args.Password, args.UserName)}")
|
apply_args.extend(extra_args)
|
||||||
|
print(f"Running: ibcmd {_redact(' '.join(format_args_for_display(apply_args, engine)), args.Password, args.UserName)}")
|
||||||
ar = run_ibcmd([v8path] + apply_args, bool(args.UserName))
|
ar = run_ibcmd([v8path] + apply_args, bool(args.UserName))
|
||||||
exit_code = ar.returncode
|
exit_code = ar.returncode
|
||||||
if exit_code == 0:
|
if exit_code == 0:
|
||||||
print("Database configuration updated successfully")
|
print("Database configuration updated successfully")
|
||||||
else:
|
else:
|
||||||
print(f"Error updating database configuration (code: {exit_code}){describe_exit(exit_code)}", file=sys.stderr)
|
print(f"Error updating database configuration (code: {exit_code}){describe_exit(exit_code)}", file=sys.stderr)
|
||||||
if ar.stdout:
|
print_platform_output(ar)
|
||||||
print(ar.stdout)
|
|
||||||
if ar.stderr:
|
|
||||||
print(ar.stderr, file=sys.stderr)
|
|
||||||
sys.exit(exit_code)
|
sys.exit(exit_code)
|
||||||
|
|
||||||
# --- Write list file (UTF-8 with BOM) ---
|
# --- Write list file (UTF-8 with BOM) ---
|
||||||
@@ -382,24 +659,24 @@ def main():
|
|||||||
arguments = ["DESIGNER"]
|
arguments = ["DESIGNER"]
|
||||||
|
|
||||||
if args.InfoBaseServer and args.InfoBaseRef:
|
if args.InfoBaseServer and args.InfoBaseRef:
|
||||||
arguments += ["/S", f"{args.InfoBaseServer}/{args.InfoBaseRef}"]
|
arguments += ["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"']
|
||||||
else:
|
else:
|
||||||
arguments += ["/F", args.InfoBasePath]
|
arguments += ["/F", f'"{args.InfoBasePath}"']
|
||||||
|
|
||||||
if args.UserName:
|
if args.UserName:
|
||||||
arguments.append(f"/N{args.UserName}")
|
arguments.append(f'/N"{args.UserName}"')
|
||||||
if args.Password:
|
if args.Password:
|
||||||
arguments.append(f"/P{args.Password}")
|
arguments.append(f'/P"{args.Password}"')
|
||||||
|
|
||||||
arguments += ["/LoadConfigFromFiles", args.ConfigDir]
|
arguments += ["/LoadConfigFromFiles", f'"{args.ConfigDir}"']
|
||||||
arguments += ["-listFile", list_file]
|
arguments += ["-listFile", f'"{list_file}"']
|
||||||
arguments += ["-Format", args.Format]
|
arguments += ["-Format", args.Format]
|
||||||
arguments.append("-partial")
|
arguments.append("-partial")
|
||||||
arguments.append("-updateConfigDumpInfo")
|
arguments.append("-updateConfigDumpInfo")
|
||||||
|
|
||||||
# --- Extensions ---
|
# --- Extensions ---
|
||||||
if args.Extension:
|
if args.Extension:
|
||||||
arguments += ["-Extension", args.Extension]
|
arguments += ["-Extension", f'"{args.Extension}"']
|
||||||
elif args.AllExtensions:
|
elif args.AllExtensions:
|
||||||
arguments.append("-AllExtensions")
|
arguments.append("-AllExtensions")
|
||||||
|
|
||||||
@@ -409,19 +686,16 @@ def main():
|
|||||||
|
|
||||||
# --- Output ---
|
# --- Output ---
|
||||||
out_file = os.path.join(temp_dir, "load_log.txt")
|
out_file = os.path.join(temp_dir, "load_log.txt")
|
||||||
arguments += ["/Out", out_file]
|
arguments += ["/Out", f'"{out_file}"']
|
||||||
arguments.append("/DisableStartupDialogs")
|
arguments.append("/DisableStartupDialogs")
|
||||||
|
arguments.extend(quote_if_needed(a) for a in extra_args)
|
||||||
|
|
||||||
# --- Execute ---
|
# --- Execute ---
|
||||||
print("")
|
print("")
|
||||||
print("Executing partial configuration load...")
|
print("Executing partial configuration load...")
|
||||||
print(f"Running: 1cv8.exe {_redact(' '.join(arguments), args.Password, args.UserName)}")
|
print(f"Running: 1cv8.exe {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
|
||||||
|
|
||||||
result = subprocess.run(
|
result = run_v8(v8path, arguments)
|
||||||
[v8path] + arguments,
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
)
|
|
||||||
exit_code = result.returncode
|
exit_code = result.returncode
|
||||||
|
|
||||||
# --- Result ---
|
# --- Result ---
|
||||||
@@ -442,6 +716,7 @@ def main():
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
print_platform_output(result)
|
||||||
sys.exit(exit_code)
|
sys.exit(exit_code)
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
|
|||||||
@@ -59,6 +59,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.ps1" <
|
|||||||
| `-AllExtensions` | нет | Загрузить все расширения |
|
| `-AllExtensions` | нет | Загрузить все расширения |
|
||||||
| `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` |
|
| `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` |
|
||||||
| `-UpdateDB` | нет | После загрузки сразу обновить конфигурацию БД (`/UpdateDBCfg`) |
|
| `-UpdateDB` | нет | После загрузки сразу обновить конфигурацию БД (`/UpdateDBCfg`) |
|
||||||
|
| `-AdditionalV8Arguments <список>` | нет | Доп. аргументы запуска `1cv8.exe` через запятую, напр. `/UseHwLicenses+` |
|
||||||
|
| `-AdditionalIbcmdArguments <список>` | нет | Доп. аргументы `ibcmd` через запятую, в форме `--ключ=значение` |
|
||||||
|
|
||||||
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
|
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# db-load-xml v1.16 — Load 1C configuration from XML files
|
# db-load-xml v1.20 — Load 1C configuration from XML files
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||||
<#
|
<#
|
||||||
@@ -48,6 +48,12 @@
|
|||||||
.PARAMETER Format
|
.PARAMETER Format
|
||||||
Формат файлов: Hierarchical или Plain (по умолчанию Hierarchical)
|
Формат файлов: Hierarchical или Plain (по умолчанию Hierarchical)
|
||||||
|
|
||||||
|
.PARAMETER AdditionalV8Arguments
|
||||||
|
Дополнительные аргументы запуска 1cv8.exe (например /UseHwLicenses+)
|
||||||
|
|
||||||
|
.PARAMETER AdditionalIbcmdArguments
|
||||||
|
Дополнительные аргументы запуска ibcmd (форма --ключ=значение)
|
||||||
|
|
||||||
.EXAMPLE
|
.EXAMPLE
|
||||||
.\db-load-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Mode Full
|
.\db-load-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Mode Full
|
||||||
|
|
||||||
@@ -102,7 +108,13 @@ param(
|
|||||||
[switch]$UpdateDB,
|
[switch]$UpdateDB,
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
[Parameter(Mandatory=$false)]
|
||||||
[switch]$StrictLog
|
[switch]$StrictLog,
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[string[]]$AdditionalV8Arguments = @(),
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[string[]]$AdditionalIbcmdArguments = @()
|
||||||
)
|
)
|
||||||
|
|
||||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
@@ -132,6 +144,165 @@ function Get-ExitAnnotation {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- Additional platform arguments ---
|
||||||
|
$script:V8OwnedKeys = @(
|
||||||
|
'DESIGNER', 'ENTERPRISE', 'CREATEINFOBASE', 'CONFIG',
|
||||||
|
'/F', '/S', '/N', '/P', '/Out', '/DisableStartupDialogs',
|
||||||
|
'/UseTemplate', '/AddToList', '/Execute', '/C', '/URL', '/UC',
|
||||||
|
'/DumpIB', '/RestoreIB', '/DumpCfg', '/LoadCfg',
|
||||||
|
'/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg',
|
||||||
|
'/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles'
|
||||||
|
)
|
||||||
|
$script:IbcmdOwnedKeys = @(
|
||||||
|
'--db-path', '--data', '--out', '--file', '--load', '--restore',
|
||||||
|
'--import', '--export', '--apply', '--force', '--create-database',
|
||||||
|
'--user', '--password'
|
||||||
|
)
|
||||||
|
$script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP')
|
||||||
|
$script:IbcmdSecretKeys = @('--password', '--token', '--db-pwd')
|
||||||
|
|
||||||
|
function Test-ArgKeyMatch {
|
||||||
|
# A token matches a key when it equals the key, or starts with it and the next
|
||||||
|
# character is not a letter — catches glued /N"user" and --password=x, while
|
||||||
|
# keeping /ClearCache distinct from /C.
|
||||||
|
param([string]$Token, [string]$Key)
|
||||||
|
if ($Token.Length -lt $Key.Length) { return $false }
|
||||||
|
if (-not $Token.Substring(0, $Key.Length).Equals($Key, [System.StringComparison]::OrdinalIgnoreCase)) { return $false }
|
||||||
|
if ($Token.Length -eq $Key.Length) { return $true }
|
||||||
|
return -not [char]::IsLetter($Token[$Key.Length])
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-ProjectExtraArgs {
|
||||||
|
# v8args / ibcmdargs from .v8-project.json — same upward walk as v8path.
|
||||||
|
param([string]$Name)
|
||||||
|
$dir = (Get-Location).Path
|
||||||
|
while ($dir) {
|
||||||
|
$pf = Join-Path $dir ".v8-project.json"
|
||||||
|
if (Test-Path $pf) {
|
||||||
|
try {
|
||||||
|
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||||
|
if ($j.$Name) { return @($j.$Name | ForEach-Object { [string]$_ }) }
|
||||||
|
} catch {}
|
||||||
|
return @()
|
||||||
|
}
|
||||||
|
$parent = Split-Path $dir -Parent
|
||||||
|
if (-not $parent -or $parent -eq $dir) { break }
|
||||||
|
$dir = $parent
|
||||||
|
}
|
||||||
|
return @()
|
||||||
|
}
|
||||||
|
|
||||||
|
function Assert-ExtraArgs {
|
||||||
|
# The platform accepts only one batch operation, and a duplicate connection or
|
||||||
|
# output key fails with an opaque 1C error — reject what the skill owns itself.
|
||||||
|
param([string[]]$ExtraArgs, [string]$Engine, [hashtable]$Hints)
|
||||||
|
$paramName = if ($Engine -eq 'ibcmd') { '-AdditionalIbcmdArguments' } else { '-AdditionalV8Arguments' }
|
||||||
|
$owned = if ($Engine -eq 'ibcmd') { $script:IbcmdOwnedKeys } else { $script:V8OwnedKeys }
|
||||||
|
foreach ($tok in $ExtraArgs) {
|
||||||
|
if ($Engine -eq 'ibcmd' -and $tok -notmatch '^-') {
|
||||||
|
Write-Host "Error: '$tok' is a positional token — pass values as --key=value ($paramName cannot extend the ibcmd command)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
foreach ($k in $owned) {
|
||||||
|
if (Test-ArgKeyMatch $tok $k) {
|
||||||
|
$hint = ''
|
||||||
|
if ($Hints -and $Hints.ContainsKey($k)) { $hint = " (use $($Hints[$k]))" }
|
||||||
|
Write-Host "Error: $k is controlled by the skill and cannot be passed via $paramName$hint" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Resolve-ExtraArgs {
|
||||||
|
# Pick the argument list for the selected engine and validate it. An explicitly passed
|
||||||
|
# parameter for the other engine is an error; the same keys coming from .v8-project.json
|
||||||
|
# simply do not apply — a project may describe both engines.
|
||||||
|
param([string]$Engine, [string[]]$V8Extra, [string[]]$IbcmdExtra, [hashtable]$Hints)
|
||||||
|
# powershell.exe -File — how skills are invoked — cannot bind an array parameter:
|
||||||
|
# space-separated values spill into positional ones, a comma-joined list arrives as a
|
||||||
|
# single token. So accept the repo's list convention (comma-separated) and split here;
|
||||||
|
# a native array call keeps working. A value containing a comma is not supported.
|
||||||
|
$V8Extra = @($V8Extra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
|
||||||
|
$IbcmdExtra = @($IbcmdExtra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
|
||||||
|
if ($Engine -eq 'ibcmd' -and $V8Extra.Count -gt 0) {
|
||||||
|
Write-Host "Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd (use -AdditionalIbcmdArguments)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
if ($Engine -ne 'ibcmd' -and $IbcmdExtra.Count -gt 0) {
|
||||||
|
Write-Host "Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 (use -AdditionalV8Arguments)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
if ($Engine -eq 'ibcmd') {
|
||||||
|
$extra = @(Get-ProjectExtraArgs 'ibcmdargs') + @($IbcmdExtra)
|
||||||
|
} else {
|
||||||
|
$extra = @(Get-ProjectExtraArgs 'v8args') + @($V8Extra)
|
||||||
|
}
|
||||||
|
if ($extra.Count -gt 0) { Assert-ExtraArgs $extra $Engine $Hints }
|
||||||
|
# Plain return, no comma trick: the caller re-collects with @(...), and ,@() there
|
||||||
|
# would nest the array — the tokens would then be glued into one argument.
|
||||||
|
return $extra
|
||||||
|
}
|
||||||
|
|
||||||
|
function Format-ArgsForDisplay {
|
||||||
|
# Redact values of secret-prone keys in glued, =-joined and separate forms.
|
||||||
|
# Matching here is a plain prefix (no letter rule): over-masking costs nothing,
|
||||||
|
# a leaked password does.
|
||||||
|
param([string[]]$ArgList, [string]$Engine)
|
||||||
|
$keys = if ($Engine -eq 'ibcmd') { $script:IbcmdSecretKeys } else { $script:V8SecretKeys }
|
||||||
|
$res = @()
|
||||||
|
$maskNext = $false
|
||||||
|
foreach ($tok in $ArgList) {
|
||||||
|
if ($maskNext) { $res += '***'; $maskNext = $false; continue }
|
||||||
|
$hit = $null
|
||||||
|
foreach ($k in $keys) {
|
||||||
|
if ($tok.Length -ge $k.Length -and $tok.Substring(0, $k.Length).Equals($k, [System.StringComparison]::OrdinalIgnoreCase)) { $hit = $k; break }
|
||||||
|
}
|
||||||
|
if (-not $hit) { $res += $tok; continue }
|
||||||
|
if ($tok.Length -eq $hit.Length) { $res += $tok; $maskNext = $true }
|
||||||
|
elseif ($tok[$hit.Length] -eq '=') { $res += ($hit + '=***') }
|
||||||
|
else { $res += ($hit + '***') }
|
||||||
|
}
|
||||||
|
return ,$res
|
||||||
|
}
|
||||||
|
|
||||||
|
function ConvertTo-CleanPath {
|
||||||
|
# Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
|
||||||
|
# surrounding quotes that survived shell parsing, a trailing separator. A quote left
|
||||||
|
# inside afterwards cannot be part of a real path — reject it by name instead of letting
|
||||||
|
# 1C answer with its opaque "Неверные или отсутствующие параметры соединения".
|
||||||
|
param([string]$Value, [string]$ParamName)
|
||||||
|
if (-not $Value) { return $Value }
|
||||||
|
$v = $Value.Trim()
|
||||||
|
if ($v.Length -ge 2 -and $v[0] -eq $v[-1] -and ($v[0] -eq '"' -or $v[0] -eq "'")) {
|
||||||
|
$v = $v.Substring(1, $v.Length - 2).Trim()
|
||||||
|
}
|
||||||
|
if ($v.Length -gt 3 -and ($v[-1] -eq '\' -or $v[-1] -eq '/')) { $v = $v.Substring(0, $v.Length - 1) }
|
||||||
|
if ($v.Contains('"')) {
|
||||||
|
Write-Host "Error: $ParamName contains a quote character: $Value" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
return $v
|
||||||
|
}
|
||||||
|
|
||||||
|
$V8Path = ConvertTo-CleanPath $V8Path '-V8Path'
|
||||||
|
$InfoBasePath = ConvertTo-CleanPath $InfoBasePath '-InfoBasePath'
|
||||||
|
$ConfigDir = ConvertTo-CleanPath $ConfigDir '-ConfigDir'
|
||||||
|
$ListFile = ConvertTo-CleanPath $ListFile '-ListFile'
|
||||||
|
|
||||||
|
function Assert-InfoBaseExists {
|
||||||
|
# These skills work on a ready infobase. Saying so up front beats the platform's
|
||||||
|
# "Неверные или отсутствующие параметры соединения" after a launch.
|
||||||
|
param([string]$Path)
|
||||||
|
if (-not $Path) { return }
|
||||||
|
if (-not (Test-Path (Join-Path $Path "1Cv8.1CD"))) {
|
||||||
|
Write-Host "Error: information base not found at $Path (no 1Cv8.1CD)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert-InfoBaseExists $InfoBasePath
|
||||||
|
|
||||||
# --- Resolve V8Path ---
|
# --- Resolve V8Path ---
|
||||||
function Find-ProjectV8Path {
|
function Find-ProjectV8Path {
|
||||||
$dir = (Get-Location).Path
|
$dir = (Get-Location).Path
|
||||||
@@ -176,35 +347,82 @@ if (-not (Test-Path $V8Path)) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
|
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
|
||||||
function Invoke-IbcmdProcess {
|
function ConvertFrom-PlatformBytes {
|
||||||
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
|
# ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
|
||||||
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
|
# OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes — guessing
|
||||||
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
|
# one of them outright mangles Cyrillic.
|
||||||
param([string]$Exe, [string[]]$IbArgs)
|
param([byte[]]$Bytes)
|
||||||
|
if (-not $Bytes -or $Bytes.Length -eq 0) { return '' }
|
||||||
|
try {
|
||||||
|
$strict = New-Object System.Text.UTF8Encoding($false, $true)
|
||||||
|
return $strict.GetString($Bytes)
|
||||||
|
} catch {
|
||||||
|
return [System.Text.Encoding]::GetEncoding(866).GetString($Bytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-PlatformProcess {
|
||||||
|
# Run the platform non-interactively and capture its console output. A closed stdin pipe
|
||||||
|
# (EOF) makes an auth prompt fast-fail instead of hanging; capturing keeps the child's
|
||||||
|
# text out of our stream until we print it labelled (and out of the wrong encoding).
|
||||||
|
# Returns @{ Output; ExitCode }.
|
||||||
|
#
|
||||||
|
# Quoting differs by engine, so the caller says which it built:
|
||||||
|
# ibcmd — tokens are bare (--db-path=C:\a b), the whole token gets quoted here;
|
||||||
|
# 1cv8 — -PreQuoted: the caller already put quotes inside the token (File="C:\a b"),
|
||||||
|
# which is where 1C's own parser expects them; quoting again breaks the value.
|
||||||
|
param([string]$Exe, [string[]]$ProcArgs, [switch]$PreQuoted)
|
||||||
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
||||||
$psi.FileName = $Exe
|
$psi.FileName = $Exe
|
||||||
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
|
$psi.Arguments = if ($PreQuoted) {
|
||||||
|
$ProcArgs -join ' '
|
||||||
|
} else {
|
||||||
|
($ProcArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
|
||||||
|
}
|
||||||
$psi.UseShellExecute = $false
|
$psi.UseShellExecute = $false
|
||||||
$psi.CreateNoWindow = $true
|
$psi.CreateNoWindow = $true
|
||||||
$psi.RedirectStandardInput = $true
|
$psi.RedirectStandardInput = $true
|
||||||
$psi.RedirectStandardOutput = $true
|
$psi.RedirectStandardOutput = $true
|
||||||
$psi.RedirectStandardError = $true
|
$psi.RedirectStandardError = $true
|
||||||
try {
|
|
||||||
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
|
|
||||||
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
|
|
||||||
} catch {}
|
|
||||||
$p = [System.Diagnostics.Process]::Start($psi)
|
$p = [System.Diagnostics.Process]::Start($psi)
|
||||||
$p.StandardInput.Close()
|
$p.StandardInput.Close()
|
||||||
$out = $p.StandardOutput.ReadToEnd()
|
# stderr is drained in parallel: reading the streams one after another deadlocks
|
||||||
$err = $p.StandardError.ReadToEnd()
|
# as soon as the other one fills its pipe buffer.
|
||||||
|
$errMs = New-Object System.IO.MemoryStream
|
||||||
|
$errTask = $p.StandardError.BaseStream.CopyToAsync($errMs)
|
||||||
|
$outMs = New-Object System.IO.MemoryStream
|
||||||
|
$p.StandardOutput.BaseStream.CopyTo($outMs)
|
||||||
|
$errTask.Wait()
|
||||||
$p.WaitForExit()
|
$p.WaitForExit()
|
||||||
|
$out = ConvertFrom-PlatformBytes $outMs.ToArray()
|
||||||
|
$err = ConvertFrom-PlatformBytes $errMs.ToArray()
|
||||||
if ($err) { $out += $err }
|
if ($err) { $out += $err }
|
||||||
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
|
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Write-PlatformOutput {
|
||||||
|
# Print what the platform wrote to the console as its own labelled block. Silence stays
|
||||||
|
# silent: in batch mode 1cv8 reports through /Out and prints nothing here.
|
||||||
|
param([string]$Text)
|
||||||
|
if (-not $Text) { return }
|
||||||
|
$t = $Text.TrimEnd()
|
||||||
|
if (-not $t) { return }
|
||||||
|
$limit = 65536
|
||||||
|
if ($t.Length -gt $limit) {
|
||||||
|
$t = "[... обрезано, показаны последние $limit символов ...]`r`n" + $t.Substring($t.Length - $limit)
|
||||||
|
}
|
||||||
|
Write-Host "--- Вывод платформы ---"
|
||||||
|
Write-Host $t
|
||||||
|
Write-Host "--- End ---"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
||||||
|
|
||||||
|
# --- Resolve additional arguments for the selected engine ---
|
||||||
|
$argHints = @{ '/F' = '-InfoBasePath'; '/S' = '-InfoBaseServer + -InfoBaseRef'; '/N' = '-UserName'; '/P' = '-Password'; '--db-path' = '-InfoBasePath'; '--user' = '-UserName'; '--password' = '-Password' }
|
||||||
|
$extraArgs = @(Resolve-ExtraArgs $engine $AdditionalV8Arguments $AdditionalIbcmdArguments $argHints)
|
||||||
|
|
||||||
# --- Validate connection ---
|
# --- Validate connection ---
|
||||||
if ($engine -eq "ibcmd") {
|
if ($engine -eq "ibcmd") {
|
||||||
if (-not $InfoBasePath) {
|
if (-not $InfoBasePath) {
|
||||||
@@ -268,25 +486,27 @@ try {
|
|||||||
if ($UserName) { $arguments += "--user=$UserName" }
|
if ($UserName) { $arguments += "--user=$UserName" }
|
||||||
if ($Password) { $arguments += "--password=$Password" }
|
if ($Password) { $arguments += "--password=$Password" }
|
||||||
$arguments += "--data=$tempDir"
|
$arguments += "--data=$tempDir"
|
||||||
Write-Host "Running: ibcmd $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))"
|
$arguments += $extraArgs
|
||||||
$__ib = Invoke-IbcmdProcess $V8Path $arguments
|
Write-Host "Running: ibcmd $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
|
||||||
|
$__ib = Invoke-PlatformProcess $V8Path $arguments
|
||||||
$output = $__ib.Output
|
$output = $__ib.Output
|
||||||
$exitCode = $__ib.ExitCode
|
$exitCode = $__ib.ExitCode
|
||||||
if ($exitCode -ne 0) {
|
if ($exitCode -ne 0) {
|
||||||
Write-Host "Error loading configuration from files (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
|
Write-Host "Error loading configuration from files (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
|
||||||
if ($output) { Write-Host ($output | Out-String) }
|
Write-PlatformOutput $output
|
||||||
exit $exitCode
|
exit $exitCode
|
||||||
}
|
}
|
||||||
Write-Host "Configuration loaded successfully from: $ConfigDir" -ForegroundColor Green
|
Write-Host "Configuration loaded successfully from: $ConfigDir" -ForegroundColor Green
|
||||||
if ($output) { Write-Host ($output | Out-String) }
|
Write-PlatformOutput $output
|
||||||
|
|
||||||
if ($UpdateDB) {
|
if ($UpdateDB) {
|
||||||
$applyArgs = @("infobase", "config", "apply", "--db-path=$InfoBasePath", "--force")
|
$applyArgs = @("infobase", "config", "apply", "--db-path=$InfoBasePath", "--force")
|
||||||
if ($UserName) { $applyArgs += "--user=$UserName" }
|
if ($UserName) { $applyArgs += "--user=$UserName" }
|
||||||
if ($Password) { $applyArgs += "--password=$Password" }
|
if ($Password) { $applyArgs += "--password=$Password" }
|
||||||
$applyArgs += "--data=$tempDir"
|
$applyArgs += "--data=$tempDir"
|
||||||
Write-Host "Running: ibcmd $(Protect-Secrets ($applyArgs -join ' ') @($Password, $UserName))"
|
$applyArgs += $extraArgs
|
||||||
$__ib = Invoke-IbcmdProcess $V8Path $applyArgs
|
Write-Host "Running: ibcmd $(Protect-Secrets ((Format-ArgsForDisplay $applyArgs $engine) -join ' ') @($Password, $UserName))"
|
||||||
|
$__ib = Invoke-PlatformProcess $V8Path $applyArgs
|
||||||
$applyOut = $__ib.Output
|
$applyOut = $__ib.Output
|
||||||
$exitCode = $__ib.ExitCode
|
$exitCode = $__ib.ExitCode
|
||||||
if ($exitCode -eq 0) {
|
if ($exitCode -eq 0) {
|
||||||
@@ -294,7 +514,7 @@ try {
|
|||||||
} else {
|
} else {
|
||||||
Write-Host "Error updating database configuration (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
|
Write-Host "Error updating database configuration (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
|
||||||
}
|
}
|
||||||
if ($applyOut) { Write-Host ($applyOut | Out-String) }
|
Write-PlatformOutput $applyOut
|
||||||
}
|
}
|
||||||
exit $exitCode
|
exit $exitCode
|
||||||
}
|
}
|
||||||
@@ -373,11 +593,12 @@ try {
|
|||||||
$outFile = Join-Path $tempDir "load_log.txt"
|
$outFile = Join-Path $tempDir "load_log.txt"
|
||||||
$arguments += "/Out", "`"$outFile`""
|
$arguments += "/Out", "`"$outFile`""
|
||||||
$arguments += "/DisableStartupDialogs"
|
$arguments += "/DisableStartupDialogs"
|
||||||
|
$arguments += $extraArgs
|
||||||
|
|
||||||
# --- Execute ---
|
# --- Execute ---
|
||||||
Write-Host "Running: 1cv8.exe $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))"
|
Write-Host "Running: 1cv8.exe $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
|
||||||
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
|
$__v8 = Invoke-PlatformProcess $V8Path $arguments -PreQuoted
|
||||||
$exitCode = $process.ExitCode
|
$exitCode = $__v8.ExitCode
|
||||||
|
|
||||||
# --- Read log ---
|
# --- Read log ---
|
||||||
$logContent = $null
|
$logContent = $null
|
||||||
@@ -424,6 +645,7 @@ try {
|
|||||||
Write-Host $logContent
|
Write-Host $logContent
|
||||||
Write-Host "--- End ---"
|
Write-Host "--- End ---"
|
||||||
}
|
}
|
||||||
|
Write-PlatformOutput $__v8.Output
|
||||||
|
|
||||||
if ($silentFailures.Count -gt 0) {
|
if ($silentFailures.Count -gt 0) {
|
||||||
$msg = "[warning] log contains $($silentFailures.Count) rejection(s) — platform loaded config but dropped properties/refs"
|
$msg = "[warning] log contains $($silentFailures.Count) rejection(s) — platform loaded config but dropped properties/refs"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# db-load-xml v1.16 — Load 1C configuration from XML files
|
# db-load-xml v1.20 — Load 1C configuration from XML files
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -14,6 +14,28 @@ import subprocess
|
|||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
|
# Регистронезависимый ввод — паритет с 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)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def _find_project_v8path():
|
def _find_project_v8path():
|
||||||
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
||||||
@@ -36,6 +58,163 @@ def _find_project_v8path():
|
|||||||
d = parent
|
d = parent
|
||||||
|
|
||||||
|
|
||||||
|
# --- Additional platform arguments ---
|
||||||
|
V8_OWNED_KEYS = [
|
||||||
|
"DESIGNER", "ENTERPRISE", "CREATEINFOBASE", "CONFIG",
|
||||||
|
"/F", "/S", "/N", "/P", "/Out", "/DisableStartupDialogs",
|
||||||
|
"/UseTemplate", "/AddToList", "/Execute", "/C", "/URL", "/UC",
|
||||||
|
"/DumpIB", "/RestoreIB", "/DumpCfg", "/LoadCfg",
|
||||||
|
"/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg",
|
||||||
|
"/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles",
|
||||||
|
]
|
||||||
|
IBCMD_OWNED_KEYS = [
|
||||||
|
"--db-path", "--data", "--out", "--file", "--load", "--restore",
|
||||||
|
"--import", "--export", "--apply", "--force", "--create-database",
|
||||||
|
"--user", "--password",
|
||||||
|
]
|
||||||
|
V8_SECRET_KEYS = ["/P", "/UC", "/WSP", "/AWSP"]
|
||||||
|
IBCMD_SECRET_KEYS = ["--password", "--token", "--db-pwd"]
|
||||||
|
|
||||||
|
|
||||||
|
def arg_key_match(token, key):
|
||||||
|
"""Token matches a key when it equals it, or starts with it and the next character
|
||||||
|
is not a letter — catches glued /N"user" and --password=x, while keeping
|
||||||
|
/ClearCache distinct from /C."""
|
||||||
|
if len(token) < len(key):
|
||||||
|
return False
|
||||||
|
if token[: len(key)].lower() != key.lower():
|
||||||
|
return False
|
||||||
|
if len(token) == len(key):
|
||||||
|
return True
|
||||||
|
return not token[len(key)].isalpha()
|
||||||
|
|
||||||
|
|
||||||
|
def project_extra_args(name):
|
||||||
|
"""v8args / ibcmdargs from .v8-project.json — same upward walk as v8path."""
|
||||||
|
d = os.getcwd()
|
||||||
|
while True:
|
||||||
|
pf = os.path.join(d, ".v8-project.json")
|
||||||
|
if os.path.isfile(pf):
|
||||||
|
try:
|
||||||
|
with open(pf, encoding="utf-8-sig") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
v = data.get(name)
|
||||||
|
if v:
|
||||||
|
return [str(x) for x in v]
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return []
|
||||||
|
parent = os.path.dirname(d)
|
||||||
|
if parent == d:
|
||||||
|
return []
|
||||||
|
d = parent
|
||||||
|
|
||||||
|
|
||||||
|
def assert_extra_args(extra, engine, hints):
|
||||||
|
"""The platform accepts only one batch operation, and a duplicate connection or
|
||||||
|
output key fails with an opaque 1C error — reject what the skill owns itself."""
|
||||||
|
param = "-AdditionalIbcmdArguments" if engine == "ibcmd" else "-AdditionalV8Arguments"
|
||||||
|
owned = IBCMD_OWNED_KEYS if engine == "ibcmd" else V8_OWNED_KEYS
|
||||||
|
for tok in extra:
|
||||||
|
if engine == "ibcmd" and not tok.startswith("-"):
|
||||||
|
print(
|
||||||
|
f"Error: '{tok}' is a positional token — pass values as --key=value "
|
||||||
|
f"({param} cannot extend the ibcmd command)",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
for k in owned:
|
||||||
|
if arg_key_match(tok, k):
|
||||||
|
hint = f" (use {hints[k]})" if hints and k in hints else ""
|
||||||
|
print(
|
||||||
|
f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def format_args_for_display(arglist, engine):
|
||||||
|
"""Redact values of secret-prone keys in glued, =-joined and separate forms.
|
||||||
|
Matching here is a plain prefix (no letter rule): over-masking costs nothing,
|
||||||
|
a leaked password does."""
|
||||||
|
keys = IBCMD_SECRET_KEYS if engine == "ibcmd" else V8_SECRET_KEYS
|
||||||
|
res = []
|
||||||
|
mask_next = False
|
||||||
|
for tok in arglist:
|
||||||
|
if mask_next:
|
||||||
|
res.append("***")
|
||||||
|
mask_next = False
|
||||||
|
continue
|
||||||
|
hit = None
|
||||||
|
for k in keys:
|
||||||
|
if tok[: len(k)].lower() == k.lower():
|
||||||
|
hit = k
|
||||||
|
break
|
||||||
|
if hit is None:
|
||||||
|
res.append(tok)
|
||||||
|
elif len(tok) == len(hit):
|
||||||
|
res.append(tok)
|
||||||
|
mask_next = True
|
||||||
|
elif tok[len(hit)] == "=":
|
||||||
|
res.append(hit + "=***")
|
||||||
|
else:
|
||||||
|
res.append(hit + "***")
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
def extract_extra_args(argv, known_opts):
|
||||||
|
"""argparse refuses values that start with '-' (every ibcmd key does), so pull the two
|
||||||
|
escape-hatch lists out of argv by hand: after the flag, take everything up to the next
|
||||||
|
declared skill option. Returns (remaining_argv, v8_extra, ibcmd_extra)."""
|
||||||
|
rest, v8, ibcmd = [], [], []
|
||||||
|
i = 0
|
||||||
|
while i < len(argv):
|
||||||
|
low = argv[i].lower()
|
||||||
|
if low in ("-additionalv8arguments", "-additionalibcmdarguments"):
|
||||||
|
target = v8 if low == "-additionalv8arguments" else ibcmd
|
||||||
|
i += 1
|
||||||
|
while i < len(argv) and argv[i].lower() not in known_opts:
|
||||||
|
target.append(argv[i])
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
rest.append(argv[i])
|
||||||
|
i += 1
|
||||||
|
return rest, v8, ibcmd
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints):
|
||||||
|
"""Pick the argument list for the selected engine and validate it. An explicitly
|
||||||
|
passed parameter for the other engine is an error; the same keys coming from
|
||||||
|
.v8-project.json simply do not apply — a project may describe both engines.
|
||||||
|
|
||||||
|
Comma-separated elements are split apart: PowerShell's -File cannot bind an array,
|
||||||
|
so that form is the documented one and both ports must accept it. A value containing
|
||||||
|
a comma is not supported."""
|
||||||
|
v8_extra = [p for tok in v8_extra for p in str(tok).split(",") if p]
|
||||||
|
ibcmd_extra = [p for tok in ibcmd_extra for p in str(tok).split(",") if p]
|
||||||
|
if engine == "ibcmd" and v8_extra:
|
||||||
|
print(
|
||||||
|
"Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd "
|
||||||
|
"(use -AdditionalIbcmdArguments)",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
if engine != "ibcmd" and ibcmd_extra:
|
||||||
|
print(
|
||||||
|
"Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 "
|
||||||
|
"(use -AdditionalV8Arguments)",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
if engine == "ibcmd":
|
||||||
|
extra = project_extra_args("ibcmdargs") + list(ibcmd_extra)
|
||||||
|
else:
|
||||||
|
extra = project_extra_args("v8args") + list(v8_extra)
|
||||||
|
if extra:
|
||||||
|
assert_extra_args(extra, engine, hints)
|
||||||
|
return extra
|
||||||
|
|
||||||
|
|
||||||
def _version_dir(p):
|
def _version_dir(p):
|
||||||
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
|
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
|
||||||
parent = os.path.dirname(p)
|
parent = os.path.dirname(p)
|
||||||
@@ -86,6 +265,85 @@ IBCMD_NOUSER_HINT = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def decode_platform_bytes(data):
|
||||||
|
"""ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
|
||||||
|
OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes — the locale
|
||||||
|
code page (what text=True uses) mangles both."""
|
||||||
|
if not data:
|
||||||
|
return ""
|
||||||
|
try:
|
||||||
|
return data.decode("utf-8")
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
return data.decode("cp866", errors="replace")
|
||||||
|
|
||||||
|
|
||||||
|
def assert_infobase_exists(path):
|
||||||
|
"""These skills work on a ready infobase. Saying so up front beats the platform's
|
||||||
|
"Неверные или отсутствующие параметры соединения" after a launch."""
|
||||||
|
if not path:
|
||||||
|
return
|
||||||
|
if not os.path.isfile(os.path.join(path, "1Cv8.1CD")):
|
||||||
|
print(f"Error: information base not found at {path} (no 1Cv8.1CD)", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def clean_path(value, param=""):
|
||||||
|
"""Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
|
||||||
|
surrounding quotes that survived shell parsing, a trailing separator. A quote left
|
||||||
|
inside afterwards cannot be part of a real path — reject it by name instead of letting
|
||||||
|
1C answer with its opaque "Неверные или отсутствующие параметры соединения"."""
|
||||||
|
if not value:
|
||||||
|
return value
|
||||||
|
v = value.strip()
|
||||||
|
if len(v) >= 2 and v[0] == v[-1] and v[0] in "\"'":
|
||||||
|
v = v[1:-1].strip()
|
||||||
|
if len(v) > 3 and v[-1] in "\\/":
|
||||||
|
v = v[:-1]
|
||||||
|
if '"' in v:
|
||||||
|
print(f"Error: {param or 'path'} contains a quote character: {value}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
return v
|
||||||
|
|
||||||
|
|
||||||
|
def quote_if_needed(token):
|
||||||
|
"""Extra arguments come from the caller unquoted; the 1cv8 command line is joined
|
||||||
|
verbatim, so a token with a space needs quotes of its own."""
|
||||||
|
if token and (" " in token or "\t" in token) and '"' not in token:
|
||||||
|
return f'"{token}"'
|
||||||
|
return token
|
||||||
|
|
||||||
|
|
||||||
|
def run_v8(v8path, arguments):
|
||||||
|
"""Run 1cv8 in batch mode and capture its console output.
|
||||||
|
|
||||||
|
The arguments carry their own quotes inside the value (File="C:\\a b") — that is where
|
||||||
|
1C's parser expects them, on Windows and on *nix alike. Windows list2cmdline would
|
||||||
|
escape those quotes, so there the command line is handed over ready-made.
|
||||||
|
"""
|
||||||
|
if os.name == "nt":
|
||||||
|
cmd = '"' + v8path + '" ' + " ".join(arguments)
|
||||||
|
else:
|
||||||
|
cmd = [v8path] + arguments
|
||||||
|
r = subprocess.run(cmd, input=b"", capture_output=True)
|
||||||
|
r.stdout = decode_platform_bytes(r.stdout)
|
||||||
|
r.stderr = decode_platform_bytes(r.stderr)
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
|
def print_platform_output(result):
|
||||||
|
"""Print what the platform wrote to the console as its own labelled block. Silence stays
|
||||||
|
silent: in batch mode 1cv8 reports through /Out and prints nothing here."""
|
||||||
|
text = ((result.stdout or "") + (result.stderr or "")).rstrip()
|
||||||
|
if not text:
|
||||||
|
return
|
||||||
|
limit = 65536
|
||||||
|
if len(text) > limit:
|
||||||
|
text = f"[... обрезано, показаны последние {limit} символов ...]\n" + text[-limit:]
|
||||||
|
print("--- Вывод платформы ---")
|
||||||
|
print(text)
|
||||||
|
print("--- End ---")
|
||||||
|
|
||||||
|
|
||||||
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
||||||
"""Run an ibcmd command non-interactively.
|
"""Run an ibcmd command non-interactively.
|
||||||
|
|
||||||
@@ -96,7 +354,10 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
|||||||
if warn_no_user and os.name == "nt" and not has_username:
|
if warn_no_user and os.name == "nt" and not has_username:
|
||||||
sys.stderr.write(IBCMD_NOUSER_HINT)
|
sys.stderr.write(IBCMD_NOUSER_HINT)
|
||||||
sys.stderr.flush()
|
sys.stderr.flush()
|
||||||
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
|
r = subprocess.run(cmd, input=b"", capture_output=True)
|
||||||
|
r.stdout = decode_platform_bytes(r.stdout)
|
||||||
|
r.stderr = decode_platform_bytes(r.stderr)
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
def describe_exit(code):
|
def describe_exit(code):
|
||||||
@@ -168,13 +429,37 @@ def main():
|
|||||||
action="store_true",
|
action="store_true",
|
||||||
help="Treat silent rejection warnings in the log as errors (elevate exit code to 1)",
|
help="Treat silent rejection warnings in the log as errors (elevate exit code to 1)",
|
||||||
)
|
)
|
||||||
args = parser.parse_args()
|
parser.add_argument("-AdditionalV8Arguments", nargs="*", default=[],
|
||||||
|
help="Extra 1cv8 arguments, e.g. /UseHwLicenses+")
|
||||||
|
parser.add_argument("-AdditionalIbcmdArguments", nargs="*", default=[],
|
||||||
|
help="Extra ibcmd arguments in --key=value form")
|
||||||
|
known_opts = {s.lower() for a in parser._actions for s in a.option_strings}
|
||||||
|
argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts)
|
||||||
|
args = ci_parse_args(parser, argv)
|
||||||
|
|
||||||
|
args.V8Path = clean_path(args.V8Path, "-V8Path")
|
||||||
|
args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath")
|
||||||
|
assert_infobase_exists(args.InfoBasePath)
|
||||||
|
args.ConfigDir = clean_path(args.ConfigDir, "-ConfigDir")
|
||||||
|
args.ListFile = clean_path(args.ListFile, "-ListFile")
|
||||||
|
|
||||||
# --- Resolve V8Path ---
|
# --- Resolve V8Path ---
|
||||||
v8path = resolve_v8path(args.V8Path)
|
v8path = resolve_v8path(args.V8Path)
|
||||||
|
|
||||||
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
|
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
|
||||||
|
|
||||||
|
# --- Resolve additional arguments for the selected engine ---
|
||||||
|
arg_hints = {
|
||||||
|
"/F": "-InfoBasePath",
|
||||||
|
"/S": "-InfoBaseServer + -InfoBaseRef",
|
||||||
|
"/N": "-UserName",
|
||||||
|
"/P": "-Password",
|
||||||
|
"--db-path": "-InfoBasePath",
|
||||||
|
"--user": "-UserName",
|
||||||
|
"--password": "-Password",
|
||||||
|
}
|
||||||
|
extra_args = resolve_extra_args(engine, v8_extra, ibcmd_extra, arg_hints)
|
||||||
|
|
||||||
# --- Validate connection ---
|
# --- Validate connection ---
|
||||||
if engine == "ibcmd":
|
if engine == "ibcmd":
|
||||||
if not args.InfoBasePath:
|
if not args.InfoBasePath:
|
||||||
@@ -232,18 +517,13 @@ def main():
|
|||||||
if args.Password:
|
if args.Password:
|
||||||
arguments.append(f"--password={args.Password}")
|
arguments.append(f"--password={args.Password}")
|
||||||
arguments.append(f"--data={ib_data}")
|
arguments.append(f"--data={ib_data}")
|
||||||
print(f"Running: ibcmd {_redact(' '.join(arguments), args.Password, args.UserName)}")
|
arguments.extend(extra_args)
|
||||||
|
print(f"Running: ibcmd {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
|
||||||
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
|
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
|
||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
print(f"Error loading configuration from files (code: {result.returncode}){describe_exit(result.returncode)}", file=sys.stderr)
|
print(f"Error loading configuration from files (code: {result.returncode}){describe_exit(result.returncode)}", file=sys.stderr)
|
||||||
if result.stdout:
|
|
||||||
print(result.stdout)
|
|
||||||
if result.stderr:
|
|
||||||
print(result.stderr, file=sys.stderr)
|
|
||||||
sys.exit(result.returncode)
|
sys.exit(result.returncode)
|
||||||
print(f"Configuration loaded successfully from: {args.ConfigDir}")
|
print(f"Configuration loaded successfully from: {args.ConfigDir}")
|
||||||
if result.stdout:
|
|
||||||
print(result.stdout)
|
|
||||||
exit_code = 0
|
exit_code = 0
|
||||||
if args.UpdateDB:
|
if args.UpdateDB:
|
||||||
apply_args = ["infobase", "config", "apply", f"--db-path={args.InfoBasePath}", "--force"]
|
apply_args = ["infobase", "config", "apply", f"--db-path={args.InfoBasePath}", "--force"]
|
||||||
@@ -252,17 +532,15 @@ def main():
|
|||||||
if args.Password:
|
if args.Password:
|
||||||
apply_args.append(f"--password={args.Password}")
|
apply_args.append(f"--password={args.Password}")
|
||||||
apply_args.append(f"--data={ib_data}")
|
apply_args.append(f"--data={ib_data}")
|
||||||
print(f"Running: ibcmd {_redact(' '.join(apply_args), args.Password, args.UserName)}")
|
apply_args.extend(extra_args)
|
||||||
|
print(f"Running: ibcmd {_redact(' '.join(format_args_for_display(apply_args, engine)), args.Password, args.UserName)}")
|
||||||
ar = run_ibcmd([v8path] + apply_args, bool(args.UserName))
|
ar = run_ibcmd([v8path] + apply_args, bool(args.UserName))
|
||||||
exit_code = ar.returncode
|
exit_code = ar.returncode
|
||||||
if exit_code == 0:
|
if exit_code == 0:
|
||||||
print("Database configuration updated successfully")
|
print("Database configuration updated successfully")
|
||||||
else:
|
else:
|
||||||
print(f"Error updating database configuration (code: {exit_code}){describe_exit(exit_code)}", file=sys.stderr)
|
print(f"Error updating database configuration (code: {exit_code}){describe_exit(exit_code)}", file=sys.stderr)
|
||||||
if ar.stdout:
|
print_platform_output(ar)
|
||||||
print(ar.stdout)
|
|
||||||
if ar.stderr:
|
|
||||||
print(ar.stderr, file=sys.stderr)
|
|
||||||
sys.exit(exit_code)
|
sys.exit(exit_code)
|
||||||
|
|
||||||
# --- Temp dir ---
|
# --- Temp dir ---
|
||||||
@@ -274,16 +552,16 @@ def main():
|
|||||||
arguments = ["DESIGNER"]
|
arguments = ["DESIGNER"]
|
||||||
|
|
||||||
if args.InfoBaseServer and args.InfoBaseRef:
|
if args.InfoBaseServer and args.InfoBaseRef:
|
||||||
arguments += ["/S", f"{args.InfoBaseServer}/{args.InfoBaseRef}"]
|
arguments += ["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"']
|
||||||
else:
|
else:
|
||||||
arguments += ["/F", args.InfoBasePath]
|
arguments += ["/F", f'"{args.InfoBasePath}"']
|
||||||
|
|
||||||
if args.UserName:
|
if args.UserName:
|
||||||
arguments.append(f"/N{args.UserName}")
|
arguments.append(f'/N"{args.UserName}"')
|
||||||
if args.Password:
|
if args.Password:
|
||||||
arguments.append(f"/P{args.Password}")
|
arguments.append(f'/P"{args.Password}"')
|
||||||
|
|
||||||
arguments += ["/LoadConfigFromFiles", args.ConfigDir]
|
arguments += ["/LoadConfigFromFiles", f'"{args.ConfigDir}"']
|
||||||
|
|
||||||
if args.Mode == "Full":
|
if args.Mode == "Full":
|
||||||
print("Executing full configuration load...")
|
print("Executing full configuration load...")
|
||||||
@@ -319,7 +597,7 @@ def main():
|
|||||||
for fl in file_list:
|
for fl in file_list:
|
||||||
print(f" {fl}")
|
print(f" {fl}")
|
||||||
|
|
||||||
arguments += ["-listFile", generated_list_file]
|
arguments += ["-listFile", f'"{generated_list_file}"']
|
||||||
arguments.append("-partial")
|
arguments.append("-partial")
|
||||||
arguments.append("-updateConfigDumpInfo")
|
arguments.append("-updateConfigDumpInfo")
|
||||||
|
|
||||||
@@ -327,7 +605,7 @@ def main():
|
|||||||
|
|
||||||
# --- Extensions ---
|
# --- Extensions ---
|
||||||
if args.Extension:
|
if args.Extension:
|
||||||
arguments += ["-Extension", args.Extension]
|
arguments += ["-Extension", f'"{args.Extension}"']
|
||||||
elif args.AllExtensions:
|
elif args.AllExtensions:
|
||||||
arguments.append("-AllExtensions")
|
arguments.append("-AllExtensions")
|
||||||
|
|
||||||
@@ -337,16 +615,13 @@ def main():
|
|||||||
|
|
||||||
# --- Output ---
|
# --- Output ---
|
||||||
out_file = os.path.join(temp_dir, "load_log.txt")
|
out_file = os.path.join(temp_dir, "load_log.txt")
|
||||||
arguments += ["/Out", out_file]
|
arguments += ["/Out", f'"{out_file}"']
|
||||||
arguments.append("/DisableStartupDialogs")
|
arguments.append("/DisableStartupDialogs")
|
||||||
|
arguments.extend(quote_if_needed(a) for a in extra_args)
|
||||||
|
|
||||||
# --- Execute ---
|
# --- Execute ---
|
||||||
print(f"Running: 1cv8.exe {_redact(' '.join(arguments), args.Password, args.UserName)}")
|
print(f"Running: 1cv8.exe {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
|
||||||
result = subprocess.run(
|
result = run_v8(v8path, arguments)
|
||||||
[v8path] + arguments,
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
)
|
|
||||||
exit_code = result.returncode
|
exit_code = result.returncode
|
||||||
|
|
||||||
# --- Read log ---
|
# --- Read log ---
|
||||||
@@ -392,6 +667,7 @@ def main():
|
|||||||
print(log_content)
|
print(log_content)
|
||||||
print("--- End ---")
|
print("--- End ---")
|
||||||
|
|
||||||
|
print_platform_output(result)
|
||||||
if silent_failures:
|
if silent_failures:
|
||||||
suffix = "" if args.StrictLog else " (pass -StrictLog to treat as error)"
|
suffix = "" if args.StrictLog else " (pass -StrictLog to treat as error)"
|
||||||
print(
|
print(
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-run.ps1" <пар
|
|||||||
| `-Execute <файл.epf>` | нет | Запуск внешней обработки сразу после старта |
|
| `-Execute <файл.epf>` | нет | Запуск внешней обработки сразу после старта |
|
||||||
| `-CParam <строка>` | нет | Параметр запуска (/C) |
|
| `-CParam <строка>` | нет | Параметр запуска (/C) |
|
||||||
| `-URL <ссылка>` | нет | Навигационная ссылка (формат `e1cib/...`) |
|
| `-URL <ссылка>` | нет | Навигационная ссылка (формат `e1cib/...`) |
|
||||||
|
| `-AdditionalV8Arguments <список>` | нет | Доп. аргументы запуска `1cv8.exe` через запятую, напр. `/UseHwLicenses+` |
|
||||||
|
|
||||||
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
|
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# db-run v1.4 — Launch 1C:Enterprise
|
# db-run v1.8 — Launch 1C:Enterprise
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||||
<#
|
<#
|
||||||
@@ -36,6 +36,12 @@
|
|||||||
.PARAMETER URL
|
.PARAMETER URL
|
||||||
Навигационная ссылка (e1cib/...)
|
Навигационная ссылка (e1cib/...)
|
||||||
|
|
||||||
|
.PARAMETER AdditionalV8Arguments
|
||||||
|
Дополнительные аргументы запуска 1cv8.exe (например /UseHwLicenses+)
|
||||||
|
|
||||||
|
.PARAMETER AdditionalIbcmdArguments
|
||||||
|
Дополнительные аргументы запуска ibcmd (форма --ключ=значение)
|
||||||
|
|
||||||
.EXAMPLE
|
.EXAMPLE
|
||||||
.\db-run.ps1 -InfoBasePath "C:\Bases\MyDB"
|
.\db-run.ps1 -InfoBasePath "C:\Bases\MyDB"
|
||||||
|
|
||||||
@@ -73,7 +79,13 @@ param(
|
|||||||
[string]$CParam,
|
[string]$CParam,
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
[Parameter(Mandatory=$false)]
|
||||||
[string]$URL
|
[string]$URL,
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[string[]]$AdditionalV8Arguments = @(),
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[string[]]$AdditionalIbcmdArguments = @()
|
||||||
)
|
)
|
||||||
|
|
||||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
@@ -86,6 +98,151 @@ function Protect-Secrets {
|
|||||||
return $Text
|
return $Text
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- Additional platform arguments ---
|
||||||
|
$script:V8OwnedKeys = @(
|
||||||
|
'DESIGNER', 'ENTERPRISE', 'CREATEINFOBASE', 'CONFIG',
|
||||||
|
'/F', '/S', '/N', '/P', '/Out', '/DisableStartupDialogs',
|
||||||
|
'/UseTemplate', '/AddToList', '/Execute', '/C', '/URL', '/UC',
|
||||||
|
'/DumpIB', '/RestoreIB', '/DumpCfg', '/LoadCfg',
|
||||||
|
'/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg',
|
||||||
|
'/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles'
|
||||||
|
)
|
||||||
|
$script:IbcmdOwnedKeys = @(
|
||||||
|
'--db-path', '--data', '--out', '--file', '--load', '--restore',
|
||||||
|
'--import', '--export', '--apply', '--force', '--create-database',
|
||||||
|
'--user', '--password'
|
||||||
|
)
|
||||||
|
$script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP')
|
||||||
|
$script:IbcmdSecretKeys = @('--password', '--token', '--db-pwd')
|
||||||
|
|
||||||
|
function Test-ArgKeyMatch {
|
||||||
|
# A token matches a key when it equals the key, or starts with it and the next
|
||||||
|
# character is not a letter — catches glued /N"user" and --password=x, while
|
||||||
|
# keeping /ClearCache distinct from /C.
|
||||||
|
param([string]$Token, [string]$Key)
|
||||||
|
if ($Token.Length -lt $Key.Length) { return $false }
|
||||||
|
if (-not $Token.Substring(0, $Key.Length).Equals($Key, [System.StringComparison]::OrdinalIgnoreCase)) { return $false }
|
||||||
|
if ($Token.Length -eq $Key.Length) { return $true }
|
||||||
|
return -not [char]::IsLetter($Token[$Key.Length])
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-ProjectExtraArgs {
|
||||||
|
# v8args / ibcmdargs from .v8-project.json — same upward walk as v8path.
|
||||||
|
param([string]$Name)
|
||||||
|
$dir = (Get-Location).Path
|
||||||
|
while ($dir) {
|
||||||
|
$pf = Join-Path $dir ".v8-project.json"
|
||||||
|
if (Test-Path $pf) {
|
||||||
|
try {
|
||||||
|
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||||
|
if ($j.$Name) { return @($j.$Name | ForEach-Object { [string]$_ }) }
|
||||||
|
} catch {}
|
||||||
|
return @()
|
||||||
|
}
|
||||||
|
$parent = Split-Path $dir -Parent
|
||||||
|
if (-not $parent -or $parent -eq $dir) { break }
|
||||||
|
$dir = $parent
|
||||||
|
}
|
||||||
|
return @()
|
||||||
|
}
|
||||||
|
|
||||||
|
function Assert-ExtraArgs {
|
||||||
|
# The platform accepts only one batch operation, and a duplicate connection or
|
||||||
|
# output key fails with an opaque 1C error — reject what the skill owns itself.
|
||||||
|
param([string[]]$ExtraArgs, [string]$Engine, [hashtable]$Hints)
|
||||||
|
$paramName = if ($Engine -eq 'ibcmd') { '-AdditionalIbcmdArguments' } else { '-AdditionalV8Arguments' }
|
||||||
|
$owned = if ($Engine -eq 'ibcmd') { $script:IbcmdOwnedKeys } else { $script:V8OwnedKeys }
|
||||||
|
foreach ($tok in $ExtraArgs) {
|
||||||
|
if ($Engine -eq 'ibcmd' -and $tok -notmatch '^-') {
|
||||||
|
Write-Host "Error: '$tok' is a positional token — pass values as --key=value ($paramName cannot extend the ibcmd command)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
foreach ($k in $owned) {
|
||||||
|
if (Test-ArgKeyMatch $tok $k) {
|
||||||
|
$hint = ''
|
||||||
|
if ($Hints -and $Hints.ContainsKey($k)) { $hint = " (use $($Hints[$k]))" }
|
||||||
|
Write-Host "Error: $k is controlled by the skill and cannot be passed via $paramName$hint" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Resolve-ExtraArgs {
|
||||||
|
# Pick the argument list for the selected engine and validate it. An explicitly passed
|
||||||
|
# parameter for the other engine is an error; the same keys coming from .v8-project.json
|
||||||
|
# simply do not apply — a project may describe both engines.
|
||||||
|
param([string]$Engine, [string[]]$V8Extra, [string[]]$IbcmdExtra, [hashtable]$Hints)
|
||||||
|
# powershell.exe -File — how skills are invoked — cannot bind an array parameter:
|
||||||
|
# space-separated values spill into positional ones, a comma-joined list arrives as a
|
||||||
|
# single token. So accept the repo's list convention (comma-separated) and split here;
|
||||||
|
# a native array call keeps working. A value containing a comma is not supported.
|
||||||
|
$V8Extra = @($V8Extra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
|
||||||
|
$IbcmdExtra = @($IbcmdExtra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
|
||||||
|
if ($Engine -eq 'ibcmd' -and $V8Extra.Count -gt 0) {
|
||||||
|
Write-Host "Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd (use -AdditionalIbcmdArguments)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
if ($Engine -ne 'ibcmd' -and $IbcmdExtra.Count -gt 0) {
|
||||||
|
Write-Host "Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 (use -AdditionalV8Arguments)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
if ($Engine -eq 'ibcmd') {
|
||||||
|
$extra = @(Get-ProjectExtraArgs 'ibcmdargs') + @($IbcmdExtra)
|
||||||
|
} else {
|
||||||
|
$extra = @(Get-ProjectExtraArgs 'v8args') + @($V8Extra)
|
||||||
|
}
|
||||||
|
if ($extra.Count -gt 0) { Assert-ExtraArgs $extra $Engine $Hints }
|
||||||
|
# Plain return, no comma trick: the caller re-collects with @(...), and ,@() there
|
||||||
|
# would nest the array — the tokens would then be glued into one argument.
|
||||||
|
return $extra
|
||||||
|
}
|
||||||
|
|
||||||
|
function Format-ArgsForDisplay {
|
||||||
|
# Redact values of secret-prone keys in glued, =-joined and separate forms.
|
||||||
|
# Matching here is a plain prefix (no letter rule): over-masking costs nothing,
|
||||||
|
# a leaked password does.
|
||||||
|
param([string[]]$ArgList, [string]$Engine)
|
||||||
|
$keys = if ($Engine -eq 'ibcmd') { $script:IbcmdSecretKeys } else { $script:V8SecretKeys }
|
||||||
|
$res = @()
|
||||||
|
$maskNext = $false
|
||||||
|
foreach ($tok in $ArgList) {
|
||||||
|
if ($maskNext) { $res += '***'; $maskNext = $false; continue }
|
||||||
|
$hit = $null
|
||||||
|
foreach ($k in $keys) {
|
||||||
|
if ($tok.Length -ge $k.Length -and $tok.Substring(0, $k.Length).Equals($k, [System.StringComparison]::OrdinalIgnoreCase)) { $hit = $k; break }
|
||||||
|
}
|
||||||
|
if (-not $hit) { $res += $tok; continue }
|
||||||
|
if ($tok.Length -eq $hit.Length) { $res += $tok; $maskNext = $true }
|
||||||
|
elseif ($tok[$hit.Length] -eq '=') { $res += ($hit + '=***') }
|
||||||
|
else { $res += ($hit + '***') }
|
||||||
|
}
|
||||||
|
return ,$res
|
||||||
|
}
|
||||||
|
|
||||||
|
function ConvertTo-CleanPath {
|
||||||
|
# Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
|
||||||
|
# surrounding quotes that survived shell parsing, a trailing separator. A quote left
|
||||||
|
# inside afterwards cannot be part of a real path — reject it by name instead of letting
|
||||||
|
# 1C answer with its opaque "Неверные или отсутствующие параметры соединения".
|
||||||
|
param([string]$Value, [string]$ParamName)
|
||||||
|
if (-not $Value) { return $Value }
|
||||||
|
$v = $Value.Trim()
|
||||||
|
if ($v.Length -ge 2 -and $v[0] -eq $v[-1] -and ($v[0] -eq '"' -or $v[0] -eq "'")) {
|
||||||
|
$v = $v.Substring(1, $v.Length - 2).Trim()
|
||||||
|
}
|
||||||
|
if ($v.Length -gt 3 -and ($v[-1] -eq '\' -or $v[-1] -eq '/')) { $v = $v.Substring(0, $v.Length - 1) }
|
||||||
|
if ($v.Contains('"')) {
|
||||||
|
Write-Host "Error: $ParamName contains a quote character: $Value" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
return $v
|
||||||
|
}
|
||||||
|
|
||||||
|
$V8Path = ConvertTo-CleanPath $V8Path '-V8Path'
|
||||||
|
$InfoBasePath = ConvertTo-CleanPath $InfoBasePath '-InfoBasePath'
|
||||||
|
$Execute = ConvertTo-CleanPath $Execute '-Execute'
|
||||||
|
|
||||||
# --- Resolve V8Path ---
|
# --- Resolve V8Path ---
|
||||||
function Find-ProjectV8Path {
|
function Find-ProjectV8Path {
|
||||||
$dir = (Get-Location).Path
|
$dir = (Get-Location).Path
|
||||||
@@ -129,6 +286,19 @@ if (-not (Test-Path $V8Path)) {
|
|||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- Resolve additional arguments ---
|
||||||
|
# 1C:Enterprise is always launched by 1cv8 — ibcmd has no interactive mode.
|
||||||
|
$engine = "1cv8"
|
||||||
|
$argHints = @{ '/F' = '-InfoBasePath'; '/S' = '-InfoBaseServer + -InfoBaseRef'; '/N' = '-UserName'; '/P' = '-Password'; '/Execute' = '-Execute'; '/C' = '-CParam'; '/URL' = '-URL' }
|
||||||
|
$extraArgs = @(Resolve-ExtraArgs $engine $AdditionalV8Arguments $AdditionalIbcmdArguments $argHints)
|
||||||
|
|
||||||
|
function Format-ArgToken {
|
||||||
|
# ShellExecute re-joins the argument string, so quote each extra token that needs it.
|
||||||
|
param([string]$Token)
|
||||||
|
if ($Token -match '[\s"]') { return ' "' + ($Token -replace '"', '\"') + '"' }
|
||||||
|
return " $Token"
|
||||||
|
}
|
||||||
|
|
||||||
# --- Validate connection ---
|
# --- Validate connection ---
|
||||||
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
||||||
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
||||||
@@ -172,9 +342,14 @@ if ($URL) {
|
|||||||
|
|
||||||
$argString += " /DisableStartupDialogs"
|
$argString += " /DisableStartupDialogs"
|
||||||
|
|
||||||
|
# The display string is built from the same tokens with secret-prone values redacted.
|
||||||
|
$displayString = $argString
|
||||||
|
foreach ($tok in $extraArgs) { $argString += (Format-ArgToken $tok) }
|
||||||
|
foreach ($tok in (Format-ArgsForDisplay $extraArgs $engine)) { $displayString += (Format-ArgToken $tok) }
|
||||||
|
|
||||||
# --- Execute (background) ---
|
# --- Execute (background) ---
|
||||||
# Redact the password/user before printing the command line — never leak secrets.
|
# Redact the password/user before printing the command line — never leak secrets.
|
||||||
$displayArg = Protect-Secrets $argString @($Password, $UserName)
|
$displayArg = Protect-Secrets $displayString @($Password, $UserName)
|
||||||
Write-Host "Running: 1cv8.exe $displayArg"
|
Write-Host "Running: 1cv8.exe $displayArg"
|
||||||
$proc = Start-Process -FilePath $V8Path -ArgumentList $argString -PassThru
|
$proc = Start-Process -FilePath $V8Path -ArgumentList $argString -PassThru
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# db-run v1.4 — Launch 1C:Enterprise
|
# db-run v1.8 — Launch 1C:Enterprise
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -11,6 +11,28 @@ import subprocess
|
|||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
# Регистронезависимый ввод — паритет с 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)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def _find_project_v8path():
|
def _find_project_v8path():
|
||||||
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
||||||
@@ -33,6 +55,181 @@ def _find_project_v8path():
|
|||||||
d = parent
|
d = parent
|
||||||
|
|
||||||
|
|
||||||
|
# --- Additional platform arguments ---
|
||||||
|
V8_OWNED_KEYS = [
|
||||||
|
"DESIGNER", "ENTERPRISE", "CREATEINFOBASE", "CONFIG",
|
||||||
|
"/F", "/S", "/N", "/P", "/Out", "/DisableStartupDialogs",
|
||||||
|
"/UseTemplate", "/AddToList", "/Execute", "/C", "/URL", "/UC",
|
||||||
|
"/DumpIB", "/RestoreIB", "/DumpCfg", "/LoadCfg",
|
||||||
|
"/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg",
|
||||||
|
"/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles",
|
||||||
|
]
|
||||||
|
IBCMD_OWNED_KEYS = [
|
||||||
|
"--db-path", "--data", "--out", "--file", "--load", "--restore",
|
||||||
|
"--import", "--export", "--apply", "--force", "--create-database",
|
||||||
|
"--user", "--password",
|
||||||
|
]
|
||||||
|
V8_SECRET_KEYS = ["/P", "/UC", "/WSP", "/AWSP"]
|
||||||
|
IBCMD_SECRET_KEYS = ["--password", "--token", "--db-pwd"]
|
||||||
|
|
||||||
|
|
||||||
|
def arg_key_match(token, key):
|
||||||
|
"""Token matches a key when it equals it, or starts with it and the next character
|
||||||
|
is not a letter — catches glued /N"user" and --password=x, while keeping
|
||||||
|
/ClearCache distinct from /C."""
|
||||||
|
if len(token) < len(key):
|
||||||
|
return False
|
||||||
|
if token[: len(key)].lower() != key.lower():
|
||||||
|
return False
|
||||||
|
if len(token) == len(key):
|
||||||
|
return True
|
||||||
|
return not token[len(key)].isalpha()
|
||||||
|
|
||||||
|
|
||||||
|
def project_extra_args(name):
|
||||||
|
"""v8args / ibcmdargs from .v8-project.json — same upward walk as v8path."""
|
||||||
|
d = os.getcwd()
|
||||||
|
while True:
|
||||||
|
pf = os.path.join(d, ".v8-project.json")
|
||||||
|
if os.path.isfile(pf):
|
||||||
|
try:
|
||||||
|
with open(pf, encoding="utf-8-sig") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
v = data.get(name)
|
||||||
|
if v:
|
||||||
|
return [str(x) for x in v]
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return []
|
||||||
|
parent = os.path.dirname(d)
|
||||||
|
if parent == d:
|
||||||
|
return []
|
||||||
|
d = parent
|
||||||
|
|
||||||
|
|
||||||
|
def assert_extra_args(extra, engine, hints):
|
||||||
|
"""The platform accepts only one batch operation, and a duplicate connection or
|
||||||
|
output key fails with an opaque 1C error — reject what the skill owns itself."""
|
||||||
|
param = "-AdditionalIbcmdArguments" if engine == "ibcmd" else "-AdditionalV8Arguments"
|
||||||
|
owned = IBCMD_OWNED_KEYS if engine == "ibcmd" else V8_OWNED_KEYS
|
||||||
|
for tok in extra:
|
||||||
|
if engine == "ibcmd" and not tok.startswith("-"):
|
||||||
|
print(
|
||||||
|
f"Error: '{tok}' is a positional token — pass values as --key=value "
|
||||||
|
f"({param} cannot extend the ibcmd command)",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
for k in owned:
|
||||||
|
if arg_key_match(tok, k):
|
||||||
|
hint = f" (use {hints[k]})" if hints and k in hints else ""
|
||||||
|
print(
|
||||||
|
f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def format_args_for_display(arglist, engine):
|
||||||
|
"""Redact values of secret-prone keys in glued, =-joined and separate forms.
|
||||||
|
Matching here is a plain prefix (no letter rule): over-masking costs nothing,
|
||||||
|
a leaked password does."""
|
||||||
|
keys = IBCMD_SECRET_KEYS if engine == "ibcmd" else V8_SECRET_KEYS
|
||||||
|
res = []
|
||||||
|
mask_next = False
|
||||||
|
for tok in arglist:
|
||||||
|
if mask_next:
|
||||||
|
res.append("***")
|
||||||
|
mask_next = False
|
||||||
|
continue
|
||||||
|
hit = None
|
||||||
|
for k in keys:
|
||||||
|
if tok[: len(k)].lower() == k.lower():
|
||||||
|
hit = k
|
||||||
|
break
|
||||||
|
if hit is None:
|
||||||
|
res.append(tok)
|
||||||
|
elif len(tok) == len(hit):
|
||||||
|
res.append(tok)
|
||||||
|
mask_next = True
|
||||||
|
elif tok[len(hit)] == "=":
|
||||||
|
res.append(hit + "=***")
|
||||||
|
else:
|
||||||
|
res.append(hit + "***")
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
def extract_extra_args(argv, known_opts):
|
||||||
|
"""argparse refuses values that start with '-' (every ibcmd key does), so pull the two
|
||||||
|
escape-hatch lists out of argv by hand: after the flag, take everything up to the next
|
||||||
|
declared skill option. Returns (remaining_argv, v8_extra, ibcmd_extra)."""
|
||||||
|
rest, v8, ibcmd = [], [], []
|
||||||
|
i = 0
|
||||||
|
while i < len(argv):
|
||||||
|
low = argv[i].lower()
|
||||||
|
if low in ("-additionalv8arguments", "-additionalibcmdarguments"):
|
||||||
|
target = v8 if low == "-additionalv8arguments" else ibcmd
|
||||||
|
i += 1
|
||||||
|
while i < len(argv) and argv[i].lower() not in known_opts:
|
||||||
|
target.append(argv[i])
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
rest.append(argv[i])
|
||||||
|
i += 1
|
||||||
|
return rest, v8, ibcmd
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints):
|
||||||
|
"""Pick the argument list for the selected engine and validate it. An explicitly
|
||||||
|
passed parameter for the other engine is an error; the same keys coming from
|
||||||
|
.v8-project.json simply do not apply — a project may describe both engines.
|
||||||
|
|
||||||
|
Comma-separated elements are split apart: PowerShell's -File cannot bind an array,
|
||||||
|
so that form is the documented one and both ports must accept it. A value containing
|
||||||
|
a comma is not supported."""
|
||||||
|
v8_extra = [p for tok in v8_extra for p in str(tok).split(",") if p]
|
||||||
|
ibcmd_extra = [p for tok in ibcmd_extra for p in str(tok).split(",") if p]
|
||||||
|
if engine == "ibcmd" and v8_extra:
|
||||||
|
print(
|
||||||
|
"Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd "
|
||||||
|
"(use -AdditionalIbcmdArguments)",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
if engine != "ibcmd" and ibcmd_extra:
|
||||||
|
print(
|
||||||
|
"Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 "
|
||||||
|
"(use -AdditionalV8Arguments)",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
if engine == "ibcmd":
|
||||||
|
extra = project_extra_args("ibcmdargs") + list(ibcmd_extra)
|
||||||
|
else:
|
||||||
|
extra = project_extra_args("v8args") + list(v8_extra)
|
||||||
|
if extra:
|
||||||
|
assert_extra_args(extra, engine, hints)
|
||||||
|
return extra
|
||||||
|
|
||||||
|
|
||||||
|
def clean_path(value, param=""):
|
||||||
|
"""Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
|
||||||
|
surrounding quotes that survived shell parsing, a trailing separator. A quote left
|
||||||
|
inside afterwards cannot be part of a real path — reject it by name instead of letting
|
||||||
|
1C answer with its opaque "Неверные или отсутствующие параметры соединения"."""
|
||||||
|
if not value:
|
||||||
|
return value
|
||||||
|
v = value.strip()
|
||||||
|
if len(v) >= 2 and v[0] == v[-1] and v[0] in "\"'":
|
||||||
|
v = v[1:-1].strip()
|
||||||
|
if len(v) > 3 and v[-1] in "\\/":
|
||||||
|
v = v[:-1]
|
||||||
|
if '"' in v:
|
||||||
|
print(f"Error: {param or 'path'} contains a quote character: {value}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
return v
|
||||||
|
|
||||||
|
|
||||||
def _version_dir(p):
|
def _version_dir(p):
|
||||||
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
|
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
|
||||||
parent = os.path.dirname(p)
|
parent = os.path.dirname(p)
|
||||||
@@ -100,10 +297,34 @@ def main():
|
|||||||
parser.add_argument("-Execute", default="")
|
parser.add_argument("-Execute", default="")
|
||||||
parser.add_argument("-CParam", default="")
|
parser.add_argument("-CParam", default="")
|
||||||
parser.add_argument("-URL", default="")
|
parser.add_argument("-URL", default="")
|
||||||
args = parser.parse_args()
|
parser.add_argument("-AdditionalV8Arguments", nargs="*", default=[],
|
||||||
|
help="Extra 1cv8 arguments, e.g. /UseHwLicenses+")
|
||||||
|
parser.add_argument("-AdditionalIbcmdArguments", nargs="*", default=[],
|
||||||
|
help="Extra ibcmd arguments in --key=value form")
|
||||||
|
known_opts = {s.lower() for a in parser._actions for s in a.option_strings}
|
||||||
|
argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts)
|
||||||
|
args = ci_parse_args(parser, argv)
|
||||||
|
|
||||||
|
args.V8Path = clean_path(args.V8Path, "-V8Path")
|
||||||
|
args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath")
|
||||||
|
args.Execute = clean_path(args.Execute, "-Execute")
|
||||||
|
|
||||||
v8path = resolve_v8path(args.V8Path)
|
v8path = resolve_v8path(args.V8Path)
|
||||||
|
|
||||||
|
# --- Resolve additional arguments ---
|
||||||
|
# 1C:Enterprise is always launched by 1cv8 — ibcmd has no interactive mode.
|
||||||
|
engine = "1cv8"
|
||||||
|
arg_hints = {
|
||||||
|
"/F": "-InfoBasePath",
|
||||||
|
"/S": "-InfoBaseServer + -InfoBaseRef",
|
||||||
|
"/N": "-UserName",
|
||||||
|
"/P": "-Password",
|
||||||
|
"/Execute": "-Execute",
|
||||||
|
"/C": "-CParam",
|
||||||
|
"/URL": "-URL",
|
||||||
|
}
|
||||||
|
extra_args = resolve_extra_args(engine, v8_extra, ibcmd_extra, arg_hints)
|
||||||
|
|
||||||
# --- Validate connection ---
|
# --- Validate connection ---
|
||||||
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
||||||
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
||||||
@@ -140,10 +361,11 @@ def main():
|
|||||||
arguments.extend(["/URL", args.URL])
|
arguments.extend(["/URL", args.URL])
|
||||||
|
|
||||||
arguments.append("/DisableStartupDialogs")
|
arguments.append("/DisableStartupDialogs")
|
||||||
|
arguments.extend(extra_args)
|
||||||
|
|
||||||
# --- Execute (background) ---
|
# --- Execute (background) ---
|
||||||
# Redact the password/user before printing the command line — never leak secrets.
|
# Redact the password/user before printing the command line — never leak secrets.
|
||||||
print(f"Running: 1cv8.exe {_redact(' '.join(arguments), args.Password, args.UserName)}")
|
print(f"Running: 1cv8.exe {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
|
||||||
proc = subprocess.Popen([v8path] + arguments)
|
proc = subprocess.Popen([v8path] + arguments)
|
||||||
|
|
||||||
# --- Bounded early-exit check ---
|
# --- Bounded early-exit check ---
|
||||||
|
|||||||
@@ -53,6 +53,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-update.ps1" <п
|
|||||||
| `-Dynamic <+/->` | нет | `+` — динамическое обновление, `-` — отключить |
|
| `-Dynamic <+/->` | нет | `+` — динамическое обновление, `-` — отключить |
|
||||||
| `-Server` | нет | Обновление на стороне сервера |
|
| `-Server` | нет | Обновление на стороне сервера |
|
||||||
| `-WarningsAsErrors` | нет | Предупреждения считать ошибками |
|
| `-WarningsAsErrors` | нет | Предупреждения считать ошибками |
|
||||||
|
| `-AdditionalV8Arguments <список>` | нет | Доп. аргументы запуска `1cv8.exe` через запятую, напр. `/UseHwLicenses+` |
|
||||||
|
| `-AdditionalIbcmdArguments <список>` | нет | Доп. аргументы `ibcmd` через запятую, в форме `--ключ=значение` |
|
||||||
|
|
||||||
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
|
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# db-update v1.10 — Update 1C database configuration
|
# db-update v1.14 — Update 1C database configuration
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||||
<#
|
<#
|
||||||
@@ -42,6 +42,12 @@
|
|||||||
.PARAMETER WarningsAsErrors
|
.PARAMETER WarningsAsErrors
|
||||||
Предупреждения считать ошибками
|
Предупреждения считать ошибками
|
||||||
|
|
||||||
|
.PARAMETER AdditionalV8Arguments
|
||||||
|
Дополнительные аргументы запуска 1cv8.exe (например /UseHwLicenses+)
|
||||||
|
|
||||||
|
.PARAMETER AdditionalIbcmdArguments
|
||||||
|
Дополнительные аргументы запуска ibcmd (форма --ключ=значение)
|
||||||
|
|
||||||
.EXAMPLE
|
.EXAMPLE
|
||||||
.\db-update.ps1 -InfoBasePath "C:\Bases\MyDB"
|
.\db-update.ps1 -InfoBasePath "C:\Bases\MyDB"
|
||||||
|
|
||||||
@@ -83,7 +89,13 @@ param(
|
|||||||
[switch]$Server,
|
[switch]$Server,
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
[Parameter(Mandatory=$false)]
|
||||||
[switch]$WarningsAsErrors
|
[switch]$WarningsAsErrors,
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[string[]]$AdditionalV8Arguments = @(),
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[string[]]$AdditionalIbcmdArguments = @()
|
||||||
)
|
)
|
||||||
|
|
||||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
@@ -113,6 +125,163 @@ function Get-ExitAnnotation {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- Additional platform arguments ---
|
||||||
|
$script:V8OwnedKeys = @(
|
||||||
|
'DESIGNER', 'ENTERPRISE', 'CREATEINFOBASE', 'CONFIG',
|
||||||
|
'/F', '/S', '/N', '/P', '/Out', '/DisableStartupDialogs',
|
||||||
|
'/UseTemplate', '/AddToList', '/Execute', '/C', '/URL', '/UC',
|
||||||
|
'/DumpIB', '/RestoreIB', '/DumpCfg', '/LoadCfg',
|
||||||
|
'/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg',
|
||||||
|
'/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles'
|
||||||
|
)
|
||||||
|
$script:IbcmdOwnedKeys = @(
|
||||||
|
'--db-path', '--data', '--out', '--file', '--load', '--restore',
|
||||||
|
'--import', '--export', '--apply', '--force', '--create-database',
|
||||||
|
'--user', '--password'
|
||||||
|
)
|
||||||
|
$script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP')
|
||||||
|
$script:IbcmdSecretKeys = @('--password', '--token', '--db-pwd')
|
||||||
|
|
||||||
|
function Test-ArgKeyMatch {
|
||||||
|
# A token matches a key when it equals the key, or starts with it and the next
|
||||||
|
# character is not a letter — catches glued /N"user" and --password=x, while
|
||||||
|
# keeping /ClearCache distinct from /C.
|
||||||
|
param([string]$Token, [string]$Key)
|
||||||
|
if ($Token.Length -lt $Key.Length) { return $false }
|
||||||
|
if (-not $Token.Substring(0, $Key.Length).Equals($Key, [System.StringComparison]::OrdinalIgnoreCase)) { return $false }
|
||||||
|
if ($Token.Length -eq $Key.Length) { return $true }
|
||||||
|
return -not [char]::IsLetter($Token[$Key.Length])
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-ProjectExtraArgs {
|
||||||
|
# v8args / ibcmdargs from .v8-project.json — same upward walk as v8path.
|
||||||
|
param([string]$Name)
|
||||||
|
$dir = (Get-Location).Path
|
||||||
|
while ($dir) {
|
||||||
|
$pf = Join-Path $dir ".v8-project.json"
|
||||||
|
if (Test-Path $pf) {
|
||||||
|
try {
|
||||||
|
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||||
|
if ($j.$Name) { return @($j.$Name | ForEach-Object { [string]$_ }) }
|
||||||
|
} catch {}
|
||||||
|
return @()
|
||||||
|
}
|
||||||
|
$parent = Split-Path $dir -Parent
|
||||||
|
if (-not $parent -or $parent -eq $dir) { break }
|
||||||
|
$dir = $parent
|
||||||
|
}
|
||||||
|
return @()
|
||||||
|
}
|
||||||
|
|
||||||
|
function Assert-ExtraArgs {
|
||||||
|
# The platform accepts only one batch operation, and a duplicate connection or
|
||||||
|
# output key fails with an opaque 1C error — reject what the skill owns itself.
|
||||||
|
param([string[]]$ExtraArgs, [string]$Engine, [hashtable]$Hints)
|
||||||
|
$paramName = if ($Engine -eq 'ibcmd') { '-AdditionalIbcmdArguments' } else { '-AdditionalV8Arguments' }
|
||||||
|
$owned = if ($Engine -eq 'ibcmd') { $script:IbcmdOwnedKeys } else { $script:V8OwnedKeys }
|
||||||
|
foreach ($tok in $ExtraArgs) {
|
||||||
|
if ($Engine -eq 'ibcmd' -and $tok -notmatch '^-') {
|
||||||
|
Write-Host "Error: '$tok' is a positional token — pass values as --key=value ($paramName cannot extend the ibcmd command)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
foreach ($k in $owned) {
|
||||||
|
if (Test-ArgKeyMatch $tok $k) {
|
||||||
|
$hint = ''
|
||||||
|
if ($Hints -and $Hints.ContainsKey($k)) { $hint = " (use $($Hints[$k]))" }
|
||||||
|
Write-Host "Error: $k is controlled by the skill and cannot be passed via $paramName$hint" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Resolve-ExtraArgs {
|
||||||
|
# Pick the argument list for the selected engine and validate it. An explicitly passed
|
||||||
|
# parameter for the other engine is an error; the same keys coming from .v8-project.json
|
||||||
|
# simply do not apply — a project may describe both engines.
|
||||||
|
param([string]$Engine, [string[]]$V8Extra, [string[]]$IbcmdExtra, [hashtable]$Hints)
|
||||||
|
# powershell.exe -File — how skills are invoked — cannot bind an array parameter:
|
||||||
|
# space-separated values spill into positional ones, a comma-joined list arrives as a
|
||||||
|
# single token. So accept the repo's list convention (comma-separated) and split here;
|
||||||
|
# a native array call keeps working. A value containing a comma is not supported.
|
||||||
|
$V8Extra = @($V8Extra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
|
||||||
|
$IbcmdExtra = @($IbcmdExtra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
|
||||||
|
if ($Engine -eq 'ibcmd' -and $V8Extra.Count -gt 0) {
|
||||||
|
Write-Host "Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd (use -AdditionalIbcmdArguments)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
if ($Engine -ne 'ibcmd' -and $IbcmdExtra.Count -gt 0) {
|
||||||
|
Write-Host "Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 (use -AdditionalV8Arguments)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
if ($Engine -eq 'ibcmd') {
|
||||||
|
$extra = @(Get-ProjectExtraArgs 'ibcmdargs') + @($IbcmdExtra)
|
||||||
|
} else {
|
||||||
|
$extra = @(Get-ProjectExtraArgs 'v8args') + @($V8Extra)
|
||||||
|
}
|
||||||
|
if ($extra.Count -gt 0) { Assert-ExtraArgs $extra $Engine $Hints }
|
||||||
|
# Plain return, no comma trick: the caller re-collects with @(...), and ,@() there
|
||||||
|
# would nest the array — the tokens would then be glued into one argument.
|
||||||
|
return $extra
|
||||||
|
}
|
||||||
|
|
||||||
|
function Format-ArgsForDisplay {
|
||||||
|
# Redact values of secret-prone keys in glued, =-joined and separate forms.
|
||||||
|
# Matching here is a plain prefix (no letter rule): over-masking costs nothing,
|
||||||
|
# a leaked password does.
|
||||||
|
param([string[]]$ArgList, [string]$Engine)
|
||||||
|
$keys = if ($Engine -eq 'ibcmd') { $script:IbcmdSecretKeys } else { $script:V8SecretKeys }
|
||||||
|
$res = @()
|
||||||
|
$maskNext = $false
|
||||||
|
foreach ($tok in $ArgList) {
|
||||||
|
if ($maskNext) { $res += '***'; $maskNext = $false; continue }
|
||||||
|
$hit = $null
|
||||||
|
foreach ($k in $keys) {
|
||||||
|
if ($tok.Length -ge $k.Length -and $tok.Substring(0, $k.Length).Equals($k, [System.StringComparison]::OrdinalIgnoreCase)) { $hit = $k; break }
|
||||||
|
}
|
||||||
|
if (-not $hit) { $res += $tok; continue }
|
||||||
|
if ($tok.Length -eq $hit.Length) { $res += $tok; $maskNext = $true }
|
||||||
|
elseif ($tok[$hit.Length] -eq '=') { $res += ($hit + '=***') }
|
||||||
|
else { $res += ($hit + '***') }
|
||||||
|
}
|
||||||
|
return ,$res
|
||||||
|
}
|
||||||
|
|
||||||
|
function ConvertTo-CleanPath {
|
||||||
|
# Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
|
||||||
|
# surrounding quotes that survived shell parsing, a trailing separator. A quote left
|
||||||
|
# inside afterwards cannot be part of a real path — reject it by name instead of letting
|
||||||
|
# 1C answer with its opaque "Неверные или отсутствующие параметры соединения".
|
||||||
|
param([string]$Value, [string]$ParamName)
|
||||||
|
if (-not $Value) { return $Value }
|
||||||
|
$v = $Value.Trim()
|
||||||
|
if ($v.Length -ge 2 -and $v[0] -eq $v[-1] -and ($v[0] -eq '"' -or $v[0] -eq "'")) {
|
||||||
|
$v = $v.Substring(1, $v.Length - 2).Trim()
|
||||||
|
}
|
||||||
|
if ($v.Length -gt 3 -and ($v[-1] -eq '\' -or $v[-1] -eq '/')) { $v = $v.Substring(0, $v.Length - 1) }
|
||||||
|
if ($v.Contains('"')) {
|
||||||
|
Write-Host "Error: $ParamName contains a quote character: $Value" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
return $v
|
||||||
|
}
|
||||||
|
|
||||||
|
$V8Path = ConvertTo-CleanPath $V8Path '-V8Path'
|
||||||
|
$InfoBasePath = ConvertTo-CleanPath $InfoBasePath '-InfoBasePath'
|
||||||
|
|
||||||
|
function Assert-InfoBaseExists {
|
||||||
|
# These skills work on a ready infobase. Saying so up front beats the platform's
|
||||||
|
# "Неверные или отсутствующие параметры соединения" after a launch.
|
||||||
|
param([string]$Path)
|
||||||
|
if (-not $Path) { return }
|
||||||
|
if (-not (Test-Path (Join-Path $Path "1Cv8.1CD"))) {
|
||||||
|
Write-Host "Error: information base not found at $Path (no 1Cv8.1CD)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert-InfoBaseExists $InfoBasePath
|
||||||
|
|
||||||
# --- Resolve V8Path ---
|
# --- Resolve V8Path ---
|
||||||
function Find-ProjectV8Path {
|
function Find-ProjectV8Path {
|
||||||
$dir = (Get-Location).Path
|
$dir = (Get-Location).Path
|
||||||
@@ -157,35 +326,82 @@ if (-not (Test-Path $V8Path)) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
|
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
|
||||||
function Invoke-IbcmdProcess {
|
function ConvertFrom-PlatformBytes {
|
||||||
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
|
# ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
|
||||||
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
|
# OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes — guessing
|
||||||
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
|
# one of them outright mangles Cyrillic.
|
||||||
param([string]$Exe, [string[]]$IbArgs)
|
param([byte[]]$Bytes)
|
||||||
|
if (-not $Bytes -or $Bytes.Length -eq 0) { return '' }
|
||||||
|
try {
|
||||||
|
$strict = New-Object System.Text.UTF8Encoding($false, $true)
|
||||||
|
return $strict.GetString($Bytes)
|
||||||
|
} catch {
|
||||||
|
return [System.Text.Encoding]::GetEncoding(866).GetString($Bytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-PlatformProcess {
|
||||||
|
# Run the platform non-interactively and capture its console output. A closed stdin pipe
|
||||||
|
# (EOF) makes an auth prompt fast-fail instead of hanging; capturing keeps the child's
|
||||||
|
# text out of our stream until we print it labelled (and out of the wrong encoding).
|
||||||
|
# Returns @{ Output; ExitCode }.
|
||||||
|
#
|
||||||
|
# Quoting differs by engine, so the caller says which it built:
|
||||||
|
# ibcmd — tokens are bare (--db-path=C:\a b), the whole token gets quoted here;
|
||||||
|
# 1cv8 — -PreQuoted: the caller already put quotes inside the token (File="C:\a b"),
|
||||||
|
# which is where 1C's own parser expects them; quoting again breaks the value.
|
||||||
|
param([string]$Exe, [string[]]$ProcArgs, [switch]$PreQuoted)
|
||||||
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
||||||
$psi.FileName = $Exe
|
$psi.FileName = $Exe
|
||||||
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
|
$psi.Arguments = if ($PreQuoted) {
|
||||||
|
$ProcArgs -join ' '
|
||||||
|
} else {
|
||||||
|
($ProcArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
|
||||||
|
}
|
||||||
$psi.UseShellExecute = $false
|
$psi.UseShellExecute = $false
|
||||||
$psi.CreateNoWindow = $true
|
$psi.CreateNoWindow = $true
|
||||||
$psi.RedirectStandardInput = $true
|
$psi.RedirectStandardInput = $true
|
||||||
$psi.RedirectStandardOutput = $true
|
$psi.RedirectStandardOutput = $true
|
||||||
$psi.RedirectStandardError = $true
|
$psi.RedirectStandardError = $true
|
||||||
try {
|
|
||||||
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
|
|
||||||
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
|
|
||||||
} catch {}
|
|
||||||
$p = [System.Diagnostics.Process]::Start($psi)
|
$p = [System.Diagnostics.Process]::Start($psi)
|
||||||
$p.StandardInput.Close()
|
$p.StandardInput.Close()
|
||||||
$out = $p.StandardOutput.ReadToEnd()
|
# stderr is drained in parallel: reading the streams one after another deadlocks
|
||||||
$err = $p.StandardError.ReadToEnd()
|
# as soon as the other one fills its pipe buffer.
|
||||||
|
$errMs = New-Object System.IO.MemoryStream
|
||||||
|
$errTask = $p.StandardError.BaseStream.CopyToAsync($errMs)
|
||||||
|
$outMs = New-Object System.IO.MemoryStream
|
||||||
|
$p.StandardOutput.BaseStream.CopyTo($outMs)
|
||||||
|
$errTask.Wait()
|
||||||
$p.WaitForExit()
|
$p.WaitForExit()
|
||||||
|
$out = ConvertFrom-PlatformBytes $outMs.ToArray()
|
||||||
|
$err = ConvertFrom-PlatformBytes $errMs.ToArray()
|
||||||
if ($err) { $out += $err }
|
if ($err) { $out += $err }
|
||||||
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
|
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Write-PlatformOutput {
|
||||||
|
# Print what the platform wrote to the console as its own labelled block. Silence stays
|
||||||
|
# silent: in batch mode 1cv8 reports through /Out and prints nothing here.
|
||||||
|
param([string]$Text)
|
||||||
|
if (-not $Text) { return }
|
||||||
|
$t = $Text.TrimEnd()
|
||||||
|
if (-not $t) { return }
|
||||||
|
$limit = 65536
|
||||||
|
if ($t.Length -gt $limit) {
|
||||||
|
$t = "[... обрезано, показаны последние $limit символов ...]`r`n" + $t.Substring($t.Length - $limit)
|
||||||
|
}
|
||||||
|
Write-Host "--- Вывод платформы ---"
|
||||||
|
Write-Host $t
|
||||||
|
Write-Host "--- End ---"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
||||||
|
|
||||||
|
# --- Resolve additional arguments for the selected engine ---
|
||||||
|
$argHints = @{ '/F' = '-InfoBasePath'; '/S' = '-InfoBaseServer + -InfoBaseRef'; '/N' = '-UserName'; '/P' = '-Password'; '--db-path' = '-InfoBasePath'; '--user' = '-UserName'; '--password' = '-Password' }
|
||||||
|
$extraArgs = @(Resolve-ExtraArgs $engine $AdditionalV8Arguments $AdditionalIbcmdArguments $argHints)
|
||||||
|
|
||||||
# --- Validate connection ---
|
# --- Validate connection ---
|
||||||
if ($engine -eq "ibcmd") {
|
if ($engine -eq "ibcmd") {
|
||||||
if (-not $InfoBasePath) {
|
if (-not $InfoBasePath) {
|
||||||
@@ -215,8 +431,9 @@ try {
|
|||||||
if ($UserName) { $arguments += "--user=$UserName" }
|
if ($UserName) { $arguments += "--user=$UserName" }
|
||||||
if ($Password) { $arguments += "--password=$Password" }
|
if ($Password) { $arguments += "--password=$Password" }
|
||||||
$arguments += "--data=$tempDir"
|
$arguments += "--data=$tempDir"
|
||||||
Write-Host "Running: ibcmd $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))"
|
$arguments += $extraArgs
|
||||||
$__ib = Invoke-IbcmdProcess $V8Path $arguments
|
Write-Host "Running: ibcmd $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
|
||||||
|
$__ib = Invoke-PlatformProcess $V8Path $arguments
|
||||||
$output = $__ib.Output
|
$output = $__ib.Output
|
||||||
$exitCode = $__ib.ExitCode
|
$exitCode = $__ib.ExitCode
|
||||||
if ($exitCode -eq 0) {
|
if ($exitCode -eq 0) {
|
||||||
@@ -224,7 +441,7 @@ try {
|
|||||||
} else {
|
} else {
|
||||||
Write-Host "Error updating database configuration (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
|
Write-Host "Error updating database configuration (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
|
||||||
}
|
}
|
||||||
if ($output) { Write-Host ($output | Out-String) }
|
Write-PlatformOutput $output
|
||||||
exit $exitCode
|
exit $exitCode
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -265,11 +482,12 @@ try {
|
|||||||
$outFile = Join-Path $tempDir "update_log.txt"
|
$outFile = Join-Path $tempDir "update_log.txt"
|
||||||
$arguments += "/Out", "`"$outFile`""
|
$arguments += "/Out", "`"$outFile`""
|
||||||
$arguments += "/DisableStartupDialogs"
|
$arguments += "/DisableStartupDialogs"
|
||||||
|
$arguments += $extraArgs
|
||||||
|
|
||||||
# --- Execute ---
|
# --- Execute ---
|
||||||
Write-Host "Running: 1cv8.exe $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))"
|
Write-Host "Running: 1cv8.exe $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
|
||||||
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
|
$__v8 = Invoke-PlatformProcess $V8Path $arguments -PreQuoted
|
||||||
$exitCode = $process.ExitCode
|
$exitCode = $__v8.ExitCode
|
||||||
|
|
||||||
# --- Result ---
|
# --- Result ---
|
||||||
if ($exitCode -eq 0) {
|
if ($exitCode -eq 0) {
|
||||||
@@ -286,6 +504,7 @@ try {
|
|||||||
Write-Host "--- End ---"
|
Write-Host "--- End ---"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Write-PlatformOutput $__v8.Output
|
||||||
|
|
||||||
exit $exitCode
|
exit $exitCode
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# db-update v1.10 — Update 1C database configuration
|
# db-update v1.14 — Update 1C database configuration
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -14,6 +14,28 @@ import subprocess
|
|||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
|
# Регистронезависимый ввод — паритет с 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)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def _find_project_v8path():
|
def _find_project_v8path():
|
||||||
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
||||||
@@ -36,6 +58,163 @@ def _find_project_v8path():
|
|||||||
d = parent
|
d = parent
|
||||||
|
|
||||||
|
|
||||||
|
# --- Additional platform arguments ---
|
||||||
|
V8_OWNED_KEYS = [
|
||||||
|
"DESIGNER", "ENTERPRISE", "CREATEINFOBASE", "CONFIG",
|
||||||
|
"/F", "/S", "/N", "/P", "/Out", "/DisableStartupDialogs",
|
||||||
|
"/UseTemplate", "/AddToList", "/Execute", "/C", "/URL", "/UC",
|
||||||
|
"/DumpIB", "/RestoreIB", "/DumpCfg", "/LoadCfg",
|
||||||
|
"/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg",
|
||||||
|
"/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles",
|
||||||
|
]
|
||||||
|
IBCMD_OWNED_KEYS = [
|
||||||
|
"--db-path", "--data", "--out", "--file", "--load", "--restore",
|
||||||
|
"--import", "--export", "--apply", "--force", "--create-database",
|
||||||
|
"--user", "--password",
|
||||||
|
]
|
||||||
|
V8_SECRET_KEYS = ["/P", "/UC", "/WSP", "/AWSP"]
|
||||||
|
IBCMD_SECRET_KEYS = ["--password", "--token", "--db-pwd"]
|
||||||
|
|
||||||
|
|
||||||
|
def arg_key_match(token, key):
|
||||||
|
"""Token matches a key when it equals it, or starts with it and the next character
|
||||||
|
is not a letter — catches glued /N"user" and --password=x, while keeping
|
||||||
|
/ClearCache distinct from /C."""
|
||||||
|
if len(token) < len(key):
|
||||||
|
return False
|
||||||
|
if token[: len(key)].lower() != key.lower():
|
||||||
|
return False
|
||||||
|
if len(token) == len(key):
|
||||||
|
return True
|
||||||
|
return not token[len(key)].isalpha()
|
||||||
|
|
||||||
|
|
||||||
|
def project_extra_args(name):
|
||||||
|
"""v8args / ibcmdargs from .v8-project.json — same upward walk as v8path."""
|
||||||
|
d = os.getcwd()
|
||||||
|
while True:
|
||||||
|
pf = os.path.join(d, ".v8-project.json")
|
||||||
|
if os.path.isfile(pf):
|
||||||
|
try:
|
||||||
|
with open(pf, encoding="utf-8-sig") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
v = data.get(name)
|
||||||
|
if v:
|
||||||
|
return [str(x) for x in v]
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return []
|
||||||
|
parent = os.path.dirname(d)
|
||||||
|
if parent == d:
|
||||||
|
return []
|
||||||
|
d = parent
|
||||||
|
|
||||||
|
|
||||||
|
def assert_extra_args(extra, engine, hints):
|
||||||
|
"""The platform accepts only one batch operation, and a duplicate connection or
|
||||||
|
output key fails with an opaque 1C error — reject what the skill owns itself."""
|
||||||
|
param = "-AdditionalIbcmdArguments" if engine == "ibcmd" else "-AdditionalV8Arguments"
|
||||||
|
owned = IBCMD_OWNED_KEYS if engine == "ibcmd" else V8_OWNED_KEYS
|
||||||
|
for tok in extra:
|
||||||
|
if engine == "ibcmd" and not tok.startswith("-"):
|
||||||
|
print(
|
||||||
|
f"Error: '{tok}' is a positional token — pass values as --key=value "
|
||||||
|
f"({param} cannot extend the ibcmd command)",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
for k in owned:
|
||||||
|
if arg_key_match(tok, k):
|
||||||
|
hint = f" (use {hints[k]})" if hints and k in hints else ""
|
||||||
|
print(
|
||||||
|
f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def format_args_for_display(arglist, engine):
|
||||||
|
"""Redact values of secret-prone keys in glued, =-joined and separate forms.
|
||||||
|
Matching here is a plain prefix (no letter rule): over-masking costs nothing,
|
||||||
|
a leaked password does."""
|
||||||
|
keys = IBCMD_SECRET_KEYS if engine == "ibcmd" else V8_SECRET_KEYS
|
||||||
|
res = []
|
||||||
|
mask_next = False
|
||||||
|
for tok in arglist:
|
||||||
|
if mask_next:
|
||||||
|
res.append("***")
|
||||||
|
mask_next = False
|
||||||
|
continue
|
||||||
|
hit = None
|
||||||
|
for k in keys:
|
||||||
|
if tok[: len(k)].lower() == k.lower():
|
||||||
|
hit = k
|
||||||
|
break
|
||||||
|
if hit is None:
|
||||||
|
res.append(tok)
|
||||||
|
elif len(tok) == len(hit):
|
||||||
|
res.append(tok)
|
||||||
|
mask_next = True
|
||||||
|
elif tok[len(hit)] == "=":
|
||||||
|
res.append(hit + "=***")
|
||||||
|
else:
|
||||||
|
res.append(hit + "***")
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
def extract_extra_args(argv, known_opts):
|
||||||
|
"""argparse refuses values that start with '-' (every ibcmd key does), so pull the two
|
||||||
|
escape-hatch lists out of argv by hand: after the flag, take everything up to the next
|
||||||
|
declared skill option. Returns (remaining_argv, v8_extra, ibcmd_extra)."""
|
||||||
|
rest, v8, ibcmd = [], [], []
|
||||||
|
i = 0
|
||||||
|
while i < len(argv):
|
||||||
|
low = argv[i].lower()
|
||||||
|
if low in ("-additionalv8arguments", "-additionalibcmdarguments"):
|
||||||
|
target = v8 if low == "-additionalv8arguments" else ibcmd
|
||||||
|
i += 1
|
||||||
|
while i < len(argv) and argv[i].lower() not in known_opts:
|
||||||
|
target.append(argv[i])
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
rest.append(argv[i])
|
||||||
|
i += 1
|
||||||
|
return rest, v8, ibcmd
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints):
|
||||||
|
"""Pick the argument list for the selected engine and validate it. An explicitly
|
||||||
|
passed parameter for the other engine is an error; the same keys coming from
|
||||||
|
.v8-project.json simply do not apply — a project may describe both engines.
|
||||||
|
|
||||||
|
Comma-separated elements are split apart: PowerShell's -File cannot bind an array,
|
||||||
|
so that form is the documented one and both ports must accept it. A value containing
|
||||||
|
a comma is not supported."""
|
||||||
|
v8_extra = [p for tok in v8_extra for p in str(tok).split(",") if p]
|
||||||
|
ibcmd_extra = [p for tok in ibcmd_extra for p in str(tok).split(",") if p]
|
||||||
|
if engine == "ibcmd" and v8_extra:
|
||||||
|
print(
|
||||||
|
"Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd "
|
||||||
|
"(use -AdditionalIbcmdArguments)",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
if engine != "ibcmd" and ibcmd_extra:
|
||||||
|
print(
|
||||||
|
"Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 "
|
||||||
|
"(use -AdditionalV8Arguments)",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
if engine == "ibcmd":
|
||||||
|
extra = project_extra_args("ibcmdargs") + list(ibcmd_extra)
|
||||||
|
else:
|
||||||
|
extra = project_extra_args("v8args") + list(v8_extra)
|
||||||
|
if extra:
|
||||||
|
assert_extra_args(extra, engine, hints)
|
||||||
|
return extra
|
||||||
|
|
||||||
|
|
||||||
def _version_dir(p):
|
def _version_dir(p):
|
||||||
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
|
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
|
||||||
parent = os.path.dirname(p)
|
parent = os.path.dirname(p)
|
||||||
@@ -86,6 +265,85 @@ IBCMD_NOUSER_HINT = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def decode_platform_bytes(data):
|
||||||
|
"""ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
|
||||||
|
OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes — the locale
|
||||||
|
code page (what text=True uses) mangles both."""
|
||||||
|
if not data:
|
||||||
|
return ""
|
||||||
|
try:
|
||||||
|
return data.decode("utf-8")
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
return data.decode("cp866", errors="replace")
|
||||||
|
|
||||||
|
|
||||||
|
def assert_infobase_exists(path):
|
||||||
|
"""These skills work on a ready infobase. Saying so up front beats the platform's
|
||||||
|
"Неверные или отсутствующие параметры соединения" after a launch."""
|
||||||
|
if not path:
|
||||||
|
return
|
||||||
|
if not os.path.isfile(os.path.join(path, "1Cv8.1CD")):
|
||||||
|
print(f"Error: information base not found at {path} (no 1Cv8.1CD)", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def clean_path(value, param=""):
|
||||||
|
"""Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
|
||||||
|
surrounding quotes that survived shell parsing, a trailing separator. A quote left
|
||||||
|
inside afterwards cannot be part of a real path — reject it by name instead of letting
|
||||||
|
1C answer with its opaque "Неверные или отсутствующие параметры соединения"."""
|
||||||
|
if not value:
|
||||||
|
return value
|
||||||
|
v = value.strip()
|
||||||
|
if len(v) >= 2 and v[0] == v[-1] and v[0] in "\"'":
|
||||||
|
v = v[1:-1].strip()
|
||||||
|
if len(v) > 3 and v[-1] in "\\/":
|
||||||
|
v = v[:-1]
|
||||||
|
if '"' in v:
|
||||||
|
print(f"Error: {param or 'path'} contains a quote character: {value}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
return v
|
||||||
|
|
||||||
|
|
||||||
|
def quote_if_needed(token):
|
||||||
|
"""Extra arguments come from the caller unquoted; the 1cv8 command line is joined
|
||||||
|
verbatim, so a token with a space needs quotes of its own."""
|
||||||
|
if token and (" " in token or "\t" in token) and '"' not in token:
|
||||||
|
return f'"{token}"'
|
||||||
|
return token
|
||||||
|
|
||||||
|
|
||||||
|
def run_v8(v8path, arguments):
|
||||||
|
"""Run 1cv8 in batch mode and capture its console output.
|
||||||
|
|
||||||
|
The arguments carry their own quotes inside the value (File="C:\\a b") — that is where
|
||||||
|
1C's parser expects them, on Windows and on *nix alike. Windows list2cmdline would
|
||||||
|
escape those quotes, so there the command line is handed over ready-made.
|
||||||
|
"""
|
||||||
|
if os.name == "nt":
|
||||||
|
cmd = '"' + v8path + '" ' + " ".join(arguments)
|
||||||
|
else:
|
||||||
|
cmd = [v8path] + arguments
|
||||||
|
r = subprocess.run(cmd, input=b"", capture_output=True)
|
||||||
|
r.stdout = decode_platform_bytes(r.stdout)
|
||||||
|
r.stderr = decode_platform_bytes(r.stderr)
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
|
def print_platform_output(result):
|
||||||
|
"""Print what the platform wrote to the console as its own labelled block. Silence stays
|
||||||
|
silent: in batch mode 1cv8 reports through /Out and prints nothing here."""
|
||||||
|
text = ((result.stdout or "") + (result.stderr or "")).rstrip()
|
||||||
|
if not text:
|
||||||
|
return
|
||||||
|
limit = 65536
|
||||||
|
if len(text) > limit:
|
||||||
|
text = f"[... обрезано, показаны последние {limit} символов ...]\n" + text[-limit:]
|
||||||
|
print("--- Вывод платформы ---")
|
||||||
|
print(text)
|
||||||
|
print("--- End ---")
|
||||||
|
|
||||||
|
|
||||||
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
||||||
"""Run an ibcmd command non-interactively.
|
"""Run an ibcmd command non-interactively.
|
||||||
|
|
||||||
@@ -96,7 +354,10 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
|||||||
if warn_no_user and os.name == "nt" and not has_username:
|
if warn_no_user and os.name == "nt" and not has_username:
|
||||||
sys.stderr.write(IBCMD_NOUSER_HINT)
|
sys.stderr.write(IBCMD_NOUSER_HINT)
|
||||||
sys.stderr.flush()
|
sys.stderr.flush()
|
||||||
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
|
r = subprocess.run(cmd, input=b"", capture_output=True)
|
||||||
|
r.stdout = decode_platform_bytes(r.stdout)
|
||||||
|
r.stderr = decode_platform_bytes(r.stderr)
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
def describe_exit(code):
|
def describe_exit(code):
|
||||||
@@ -150,12 +411,34 @@ def main():
|
|||||||
parser.add_argument("-Dynamic", default="", choices=["", "+", "-"])
|
parser.add_argument("-Dynamic", default="", choices=["", "+", "-"])
|
||||||
parser.add_argument("-Server", action="store_true")
|
parser.add_argument("-Server", action="store_true")
|
||||||
parser.add_argument("-WarningsAsErrors", action="store_true")
|
parser.add_argument("-WarningsAsErrors", action="store_true")
|
||||||
args = parser.parse_args()
|
parser.add_argument("-AdditionalV8Arguments", nargs="*", default=[],
|
||||||
|
help="Extra 1cv8 arguments, e.g. /UseHwLicenses+")
|
||||||
|
parser.add_argument("-AdditionalIbcmdArguments", nargs="*", default=[],
|
||||||
|
help="Extra ibcmd arguments in --key=value form")
|
||||||
|
known_opts = {s.lower() for a in parser._actions for s in a.option_strings}
|
||||||
|
argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts)
|
||||||
|
args = ci_parse_args(parser, argv)
|
||||||
|
|
||||||
|
args.V8Path = clean_path(args.V8Path, "-V8Path")
|
||||||
|
args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath")
|
||||||
|
assert_infobase_exists(args.InfoBasePath)
|
||||||
|
|
||||||
v8path = resolve_v8path(args.V8Path)
|
v8path = resolve_v8path(args.V8Path)
|
||||||
|
|
||||||
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
|
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
|
||||||
|
|
||||||
|
# --- Resolve additional arguments for the selected engine ---
|
||||||
|
arg_hints = {
|
||||||
|
"/F": "-InfoBasePath",
|
||||||
|
"/S": "-InfoBaseServer + -InfoBaseRef",
|
||||||
|
"/N": "-UserName",
|
||||||
|
"/P": "-Password",
|
||||||
|
"--db-path": "-InfoBasePath",
|
||||||
|
"--user": "-UserName",
|
||||||
|
"--password": "-Password",
|
||||||
|
}
|
||||||
|
extra_args = resolve_extra_args(engine, v8_extra, ibcmd_extra, arg_hints)
|
||||||
|
|
||||||
# --- Validate connection ---
|
# --- Validate connection ---
|
||||||
if engine == "ibcmd":
|
if engine == "ibcmd":
|
||||||
if not args.InfoBasePath:
|
if not args.InfoBasePath:
|
||||||
@@ -184,16 +467,13 @@ def main():
|
|||||||
if args.Password:
|
if args.Password:
|
||||||
arguments.append(f"--password={args.Password}")
|
arguments.append(f"--password={args.Password}")
|
||||||
arguments.append(f"--data={ib_data}")
|
arguments.append(f"--data={ib_data}")
|
||||||
print(f"Running: ibcmd {_redact(' '.join(arguments), args.Password, args.UserName)}")
|
arguments.extend(extra_args)
|
||||||
|
print(f"Running: ibcmd {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
|
||||||
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
|
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
|
||||||
if result.returncode == 0:
|
if result.returncode == 0:
|
||||||
print("Database configuration updated successfully")
|
print("Database configuration updated successfully")
|
||||||
else:
|
else:
|
||||||
print(f"Error updating database configuration (code: {result.returncode}){describe_exit(result.returncode)}", file=sys.stderr)
|
print(f"Error updating database configuration (code: {result.returncode}){describe_exit(result.returncode)}", file=sys.stderr)
|
||||||
if result.stdout:
|
|
||||||
print(result.stdout)
|
|
||||||
if result.stderr:
|
|
||||||
print(result.stderr, file=sys.stderr)
|
|
||||||
sys.exit(result.returncode)
|
sys.exit(result.returncode)
|
||||||
|
|
||||||
# --- Temp dir ---
|
# --- Temp dir ---
|
||||||
@@ -205,14 +485,14 @@ def main():
|
|||||||
arguments = ["DESIGNER"]
|
arguments = ["DESIGNER"]
|
||||||
|
|
||||||
if args.InfoBaseServer and args.InfoBaseRef:
|
if args.InfoBaseServer and args.InfoBaseRef:
|
||||||
arguments.extend(["/S", f"{args.InfoBaseServer}/{args.InfoBaseRef}"])
|
arguments.extend(["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"'])
|
||||||
else:
|
else:
|
||||||
arguments.extend(["/F", args.InfoBasePath])
|
arguments.extend(["/F", f'"{args.InfoBasePath}"'])
|
||||||
|
|
||||||
if args.UserName:
|
if args.UserName:
|
||||||
arguments.append(f"/N{args.UserName}")
|
arguments.append(f'/N"{args.UserName}"')
|
||||||
if args.Password:
|
if args.Password:
|
||||||
arguments.append(f"/P{args.Password}")
|
arguments.append(f'/P"{args.Password}"')
|
||||||
|
|
||||||
arguments.append("/UpdateDBCfg")
|
arguments.append("/UpdateDBCfg")
|
||||||
|
|
||||||
@@ -226,22 +506,19 @@ def main():
|
|||||||
|
|
||||||
# --- Extensions ---
|
# --- Extensions ---
|
||||||
if args.Extension:
|
if args.Extension:
|
||||||
arguments.extend(["-Extension", args.Extension])
|
arguments.extend(["-Extension", f'"{args.Extension}"'])
|
||||||
elif args.AllExtensions:
|
elif args.AllExtensions:
|
||||||
arguments.append("-AllExtensions")
|
arguments.append("-AllExtensions")
|
||||||
|
|
||||||
# --- Output ---
|
# --- Output ---
|
||||||
out_file = os.path.join(temp_dir, "update_log.txt")
|
out_file = os.path.join(temp_dir, "update_log.txt")
|
||||||
arguments.extend(["/Out", out_file])
|
arguments.extend(["/Out", f'"{out_file}"'])
|
||||||
arguments.append("/DisableStartupDialogs")
|
arguments.append("/DisableStartupDialogs")
|
||||||
|
arguments.extend(quote_if_needed(a) for a in extra_args)
|
||||||
|
|
||||||
# --- Execute ---
|
# --- Execute ---
|
||||||
print(f"Running: 1cv8.exe {_redact(' '.join(arguments), args.Password, args.UserName)}")
|
print(f"Running: 1cv8.exe {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
|
||||||
result = subprocess.run(
|
result = run_v8(v8path, arguments)
|
||||||
[v8path] + arguments,
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
)
|
|
||||||
exit_code = result.returncode
|
exit_code = result.returncode
|
||||||
|
|
||||||
# --- Result ---
|
# --- Result ---
|
||||||
@@ -261,6 +538,7 @@ def main():
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
print_platform_output(result)
|
||||||
sys.exit(exit_code)
|
sys.exit(exit_code)
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
|
|||||||
@@ -55,6 +55,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-build.ps1" <п
|
|||||||
| `-Password <пароль>` | нет | Пароль |
|
| `-Password <пароль>` | нет | Пароль |
|
||||||
| `-SourceFile <путь>` | да | Путь к корневому XML-файлу исходников |
|
| `-SourceFile <путь>` | да | Путь к корневому XML-файлу исходников |
|
||||||
| `-OutputFile <путь>` | да | Путь к выходному EPF/ERF-файлу |
|
| `-OutputFile <путь>` | да | Путь к выходному EPF/ERF-файлу |
|
||||||
|
| `-AdditionalV8Arguments <список>` | нет | Доп. аргументы запуска `1cv8.exe` через запятую, напр. `/UseHwLicenses+` |
|
||||||
|
| `-AdditionalIbcmdArguments <список>` | нет | Доп. аргументы `ibcmd` через запятую, в форме `--ключ=значение` |
|
||||||
|
|
||||||
> `*` — опционально. Если не указано — автоматически создаётся временная база со заглушками метаданных
|
> `*` — опционально. Если не указано — автоматически создаётся временная база со заглушками метаданных
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# epf-build v1.9 — Build external data processor or report (EPF/ERF) from XML sources
|
# epf-build v1.13 — Build external data processor or report (EPF/ERF) from XML sources
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||||
<#
|
<#
|
||||||
@@ -33,6 +33,12 @@
|
|||||||
.PARAMETER OutputFile
|
.PARAMETER OutputFile
|
||||||
Путь к выходному EPF/ERF-файлу
|
Путь к выходному EPF/ERF-файлу
|
||||||
|
|
||||||
|
.PARAMETER AdditionalV8Arguments
|
||||||
|
Дополнительные аргументы запуска 1cv8.exe (например /UseHwLicenses+)
|
||||||
|
|
||||||
|
.PARAMETER AdditionalIbcmdArguments
|
||||||
|
Дополнительные аргументы запуска ibcmd (форма --ключ=значение)
|
||||||
|
|
||||||
.EXAMPLE
|
.EXAMPLE
|
||||||
.\epf-build.ps1 -InfoBasePath "C:\Bases\MyDB" -SourceFile "src\МояОбработка.xml" -OutputFile "build\МояОбработка.epf"
|
.\epf-build.ps1 -InfoBasePath "C:\Bases\MyDB" -SourceFile "src\МояОбработка.xml" -OutputFile "build\МояОбработка.epf"
|
||||||
|
|
||||||
@@ -64,7 +70,13 @@ param(
|
|||||||
[string]$SourceFile,
|
[string]$SourceFile,
|
||||||
|
|
||||||
[Parameter(Mandatory=$true)]
|
[Parameter(Mandatory=$true)]
|
||||||
[string]$OutputFile
|
[string]$OutputFile,
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[string[]]$AdditionalV8Arguments = @(),
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[string[]]$AdditionalIbcmdArguments = @()
|
||||||
)
|
)
|
||||||
|
|
||||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
@@ -77,6 +89,165 @@ function Protect-Secrets {
|
|||||||
return $Text
|
return $Text
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- Additional platform arguments ---
|
||||||
|
$script:V8OwnedKeys = @(
|
||||||
|
'DESIGNER', 'ENTERPRISE', 'CREATEINFOBASE', 'CONFIG',
|
||||||
|
'/F', '/S', '/N', '/P', '/Out', '/DisableStartupDialogs',
|
||||||
|
'/UseTemplate', '/AddToList', '/Execute', '/C', '/URL', '/UC',
|
||||||
|
'/DumpIB', '/RestoreIB', '/DumpCfg', '/LoadCfg',
|
||||||
|
'/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg',
|
||||||
|
'/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles'
|
||||||
|
)
|
||||||
|
$script:IbcmdOwnedKeys = @(
|
||||||
|
'--db-path', '--data', '--out', '--file', '--load', '--restore',
|
||||||
|
'--import', '--export', '--apply', '--force', '--create-database',
|
||||||
|
'--user', '--password'
|
||||||
|
)
|
||||||
|
$script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP')
|
||||||
|
$script:IbcmdSecretKeys = @('--password', '--token', '--db-pwd')
|
||||||
|
|
||||||
|
function Test-ArgKeyMatch {
|
||||||
|
# A token matches a key when it equals the key, or starts with it and the next
|
||||||
|
# character is not a letter — catches glued /N"user" and --password=x, while
|
||||||
|
# keeping /ClearCache distinct from /C.
|
||||||
|
param([string]$Token, [string]$Key)
|
||||||
|
if ($Token.Length -lt $Key.Length) { return $false }
|
||||||
|
if (-not $Token.Substring(0, $Key.Length).Equals($Key, [System.StringComparison]::OrdinalIgnoreCase)) { return $false }
|
||||||
|
if ($Token.Length -eq $Key.Length) { return $true }
|
||||||
|
return -not [char]::IsLetter($Token[$Key.Length])
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-ProjectExtraArgs {
|
||||||
|
# v8args / ibcmdargs from .v8-project.json — same upward walk as v8path.
|
||||||
|
param([string]$Name)
|
||||||
|
$dir = (Get-Location).Path
|
||||||
|
while ($dir) {
|
||||||
|
$pf = Join-Path $dir ".v8-project.json"
|
||||||
|
if (Test-Path $pf) {
|
||||||
|
try {
|
||||||
|
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||||
|
if ($j.$Name) { return @($j.$Name | ForEach-Object { [string]$_ }) }
|
||||||
|
} catch {}
|
||||||
|
return @()
|
||||||
|
}
|
||||||
|
$parent = Split-Path $dir -Parent
|
||||||
|
if (-not $parent -or $parent -eq $dir) { break }
|
||||||
|
$dir = $parent
|
||||||
|
}
|
||||||
|
return @()
|
||||||
|
}
|
||||||
|
|
||||||
|
function Assert-ExtraArgs {
|
||||||
|
# The platform accepts only one batch operation, and a duplicate connection or
|
||||||
|
# output key fails with an opaque 1C error — reject what the skill owns itself.
|
||||||
|
param([string[]]$ExtraArgs, [string]$Engine, [hashtable]$Hints)
|
||||||
|
$paramName = if ($Engine -eq 'ibcmd') { '-AdditionalIbcmdArguments' } else { '-AdditionalV8Arguments' }
|
||||||
|
$owned = if ($Engine -eq 'ibcmd') { $script:IbcmdOwnedKeys } else { $script:V8OwnedKeys }
|
||||||
|
foreach ($tok in $ExtraArgs) {
|
||||||
|
if ($Engine -eq 'ibcmd' -and $tok -notmatch '^-') {
|
||||||
|
Write-Host "Error: '$tok' is a positional token — pass values as --key=value ($paramName cannot extend the ibcmd command)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
foreach ($k in $owned) {
|
||||||
|
if (Test-ArgKeyMatch $tok $k) {
|
||||||
|
$hint = ''
|
||||||
|
if ($Hints -and $Hints.ContainsKey($k)) { $hint = " (use $($Hints[$k]))" }
|
||||||
|
Write-Host "Error: $k is controlled by the skill and cannot be passed via $paramName$hint" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Resolve-ExtraArgs {
|
||||||
|
# Pick the argument list for the selected engine and validate it. An explicitly passed
|
||||||
|
# parameter for the other engine is an error; the same keys coming from .v8-project.json
|
||||||
|
# simply do not apply — a project may describe both engines.
|
||||||
|
param([string]$Engine, [string[]]$V8Extra, [string[]]$IbcmdExtra, [hashtable]$Hints)
|
||||||
|
# powershell.exe -File — how skills are invoked — cannot bind an array parameter:
|
||||||
|
# space-separated values spill into positional ones, a comma-joined list arrives as a
|
||||||
|
# single token. So accept the repo's list convention (comma-separated) and split here;
|
||||||
|
# a native array call keeps working. A value containing a comma is not supported.
|
||||||
|
$V8Extra = @($V8Extra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
|
||||||
|
$IbcmdExtra = @($IbcmdExtra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
|
||||||
|
if ($Engine -eq 'ibcmd' -and $V8Extra.Count -gt 0) {
|
||||||
|
Write-Host "Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd (use -AdditionalIbcmdArguments)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
if ($Engine -ne 'ibcmd' -and $IbcmdExtra.Count -gt 0) {
|
||||||
|
Write-Host "Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 (use -AdditionalV8Arguments)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
if ($Engine -eq 'ibcmd') {
|
||||||
|
$extra = @(Get-ProjectExtraArgs 'ibcmdargs') + @($IbcmdExtra)
|
||||||
|
} else {
|
||||||
|
$extra = @(Get-ProjectExtraArgs 'v8args') + @($V8Extra)
|
||||||
|
}
|
||||||
|
if ($extra.Count -gt 0) { Assert-ExtraArgs $extra $Engine $Hints }
|
||||||
|
# Plain return, no comma trick: the caller re-collects with @(...), and ,@() there
|
||||||
|
# would nest the array — the tokens would then be glued into one argument.
|
||||||
|
return $extra
|
||||||
|
}
|
||||||
|
|
||||||
|
function Format-ArgsForDisplay {
|
||||||
|
# Redact values of secret-prone keys in glued, =-joined and separate forms.
|
||||||
|
# Matching here is a plain prefix (no letter rule): over-masking costs nothing,
|
||||||
|
# a leaked password does.
|
||||||
|
param([string[]]$ArgList, [string]$Engine)
|
||||||
|
$keys = if ($Engine -eq 'ibcmd') { $script:IbcmdSecretKeys } else { $script:V8SecretKeys }
|
||||||
|
$res = @()
|
||||||
|
$maskNext = $false
|
||||||
|
foreach ($tok in $ArgList) {
|
||||||
|
if ($maskNext) { $res += '***'; $maskNext = $false; continue }
|
||||||
|
$hit = $null
|
||||||
|
foreach ($k in $keys) {
|
||||||
|
if ($tok.Length -ge $k.Length -and $tok.Substring(0, $k.Length).Equals($k, [System.StringComparison]::OrdinalIgnoreCase)) { $hit = $k; break }
|
||||||
|
}
|
||||||
|
if (-not $hit) { $res += $tok; continue }
|
||||||
|
if ($tok.Length -eq $hit.Length) { $res += $tok; $maskNext = $true }
|
||||||
|
elseif ($tok[$hit.Length] -eq '=') { $res += ($hit + '=***') }
|
||||||
|
else { $res += ($hit + '***') }
|
||||||
|
}
|
||||||
|
return ,$res
|
||||||
|
}
|
||||||
|
|
||||||
|
function ConvertTo-CleanPath {
|
||||||
|
# Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
|
||||||
|
# surrounding quotes that survived shell parsing, a trailing separator. A quote left
|
||||||
|
# inside afterwards cannot be part of a real path — reject it by name instead of letting
|
||||||
|
# 1C answer with its opaque "Неверные или отсутствующие параметры соединения".
|
||||||
|
param([string]$Value, [string]$ParamName)
|
||||||
|
if (-not $Value) { return $Value }
|
||||||
|
$v = $Value.Trim()
|
||||||
|
if ($v.Length -ge 2 -and $v[0] -eq $v[-1] -and ($v[0] -eq '"' -or $v[0] -eq "'")) {
|
||||||
|
$v = $v.Substring(1, $v.Length - 2).Trim()
|
||||||
|
}
|
||||||
|
if ($v.Length -gt 3 -and ($v[-1] -eq '\' -or $v[-1] -eq '/')) { $v = $v.Substring(0, $v.Length - 1) }
|
||||||
|
if ($v.Contains('"')) {
|
||||||
|
Write-Host "Error: $ParamName contains a quote character: $Value" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
return $v
|
||||||
|
}
|
||||||
|
|
||||||
|
$V8Path = ConvertTo-CleanPath $V8Path '-V8Path'
|
||||||
|
$InfoBasePath = ConvertTo-CleanPath $InfoBasePath '-InfoBasePath'
|
||||||
|
$SourceFile = ConvertTo-CleanPath $SourceFile '-SourceFile'
|
||||||
|
$OutputFile = ConvertTo-CleanPath $OutputFile '-OutputFile'
|
||||||
|
|
||||||
|
function Assert-InfoBaseExists {
|
||||||
|
# These skills work on a ready infobase. Saying so up front beats the platform's
|
||||||
|
# "Неверные или отсутствующие параметры соединения" after a launch.
|
||||||
|
param([string]$Path)
|
||||||
|
if (-not $Path) { return }
|
||||||
|
if (-not (Test-Path (Join-Path $Path "1Cv8.1CD"))) {
|
||||||
|
Write-Host "Error: information base not found at $Path (no 1Cv8.1CD)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert-InfoBaseExists $InfoBasePath
|
||||||
|
|
||||||
# --- Resolve V8Path ---
|
# --- Resolve V8Path ---
|
||||||
function Find-ProjectV8Path {
|
function Find-ProjectV8Path {
|
||||||
$dir = (Get-Location).Path
|
$dir = (Get-Location).Path
|
||||||
@@ -121,32 +292,75 @@ if (-not (Test-Path $V8Path)) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
|
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
|
||||||
function Invoke-IbcmdProcess {
|
function ConvertFrom-PlatformBytes {
|
||||||
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
|
# ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
|
||||||
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
|
# OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes — guessing
|
||||||
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
|
# one of them outright mangles Cyrillic.
|
||||||
param([string]$Exe, [string[]]$IbArgs)
|
param([byte[]]$Bytes)
|
||||||
|
if (-not $Bytes -or $Bytes.Length -eq 0) { return '' }
|
||||||
|
try {
|
||||||
|
$strict = New-Object System.Text.UTF8Encoding($false, $true)
|
||||||
|
return $strict.GetString($Bytes)
|
||||||
|
} catch {
|
||||||
|
return [System.Text.Encoding]::GetEncoding(866).GetString($Bytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-PlatformProcess {
|
||||||
|
# Run the platform non-interactively and capture its console output. A closed stdin pipe
|
||||||
|
# (EOF) makes an auth prompt fast-fail instead of hanging; capturing keeps the child's
|
||||||
|
# text out of our stream until we print it labelled (and out of the wrong encoding).
|
||||||
|
# Returns @{ Output; ExitCode }.
|
||||||
|
#
|
||||||
|
# Quoting differs by engine, so the caller says which it built:
|
||||||
|
# ibcmd — tokens are bare (--db-path=C:\a b), the whole token gets quoted here;
|
||||||
|
# 1cv8 — -PreQuoted: the caller already put quotes inside the token (File="C:\a b"),
|
||||||
|
# which is where 1C's own parser expects them; quoting again breaks the value.
|
||||||
|
param([string]$Exe, [string[]]$ProcArgs, [switch]$PreQuoted)
|
||||||
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
||||||
$psi.FileName = $Exe
|
$psi.FileName = $Exe
|
||||||
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
|
$psi.Arguments = if ($PreQuoted) {
|
||||||
|
$ProcArgs -join ' '
|
||||||
|
} else {
|
||||||
|
($ProcArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
|
||||||
|
}
|
||||||
$psi.UseShellExecute = $false
|
$psi.UseShellExecute = $false
|
||||||
$psi.CreateNoWindow = $true
|
$psi.CreateNoWindow = $true
|
||||||
$psi.RedirectStandardInput = $true
|
$psi.RedirectStandardInput = $true
|
||||||
$psi.RedirectStandardOutput = $true
|
$psi.RedirectStandardOutput = $true
|
||||||
$psi.RedirectStandardError = $true
|
$psi.RedirectStandardError = $true
|
||||||
try {
|
|
||||||
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
|
|
||||||
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
|
|
||||||
} catch {}
|
|
||||||
$p = [System.Diagnostics.Process]::Start($psi)
|
$p = [System.Diagnostics.Process]::Start($psi)
|
||||||
$p.StandardInput.Close()
|
$p.StandardInput.Close()
|
||||||
$out = $p.StandardOutput.ReadToEnd()
|
# stderr is drained in parallel: reading the streams one after another deadlocks
|
||||||
$err = $p.StandardError.ReadToEnd()
|
# as soon as the other one fills its pipe buffer.
|
||||||
|
$errMs = New-Object System.IO.MemoryStream
|
||||||
|
$errTask = $p.StandardError.BaseStream.CopyToAsync($errMs)
|
||||||
|
$outMs = New-Object System.IO.MemoryStream
|
||||||
|
$p.StandardOutput.BaseStream.CopyTo($outMs)
|
||||||
|
$errTask.Wait()
|
||||||
$p.WaitForExit()
|
$p.WaitForExit()
|
||||||
|
$out = ConvertFrom-PlatformBytes $outMs.ToArray()
|
||||||
|
$err = ConvertFrom-PlatformBytes $errMs.ToArray()
|
||||||
if ($err) { $out += $err }
|
if ($err) { $out += $err }
|
||||||
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
|
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Write-PlatformOutput {
|
||||||
|
# Print what the platform wrote to the console as its own labelled block. Silence stays
|
||||||
|
# silent: in batch mode 1cv8 reports through /Out and prints nothing here.
|
||||||
|
param([string]$Text)
|
||||||
|
if (-not $Text) { return }
|
||||||
|
$t = $Text.TrimEnd()
|
||||||
|
if (-not $t) { return }
|
||||||
|
$limit = 65536
|
||||||
|
if ($t.Length -gt $limit) {
|
||||||
|
$t = "[... обрезано, показаны последние $limit символов ...]`r`n" + $t.Substring($t.Length - $limit)
|
||||||
|
}
|
||||||
|
Write-Host "--- Вывод платформы ---"
|
||||||
|
Write-Host $t
|
||||||
|
Write-Host "--- End ---"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function Test-OutputNonEmpty {
|
function Test-OutputNonEmpty {
|
||||||
# Postcondition: the platform must have produced a non-empty output file.
|
# Postcondition: the platform must have produced a non-empty output file.
|
||||||
@@ -156,6 +370,10 @@ function Test-OutputNonEmpty {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
||||||
|
|
||||||
|
# --- Resolve additional arguments for the selected engine ---
|
||||||
|
$argHints = @{ '/F' = '-InfoBasePath'; '/S' = '-InfoBaseServer + -InfoBaseRef'; '/N' = '-UserName'; '/P' = '-Password'; '--db-path' = '-InfoBasePath'; '--user' = '-UserName'; '--password' = '-Password' }
|
||||||
|
$extraArgs = @(Resolve-ExtraArgs $engine $AdditionalV8Arguments $AdditionalIbcmdArguments $argHints)
|
||||||
if ($engine -eq "ibcmd" -and $InfoBaseServer -and $InfoBaseRef) {
|
if ($engine -eq "ibcmd" -and $InfoBaseServer -and $InfoBaseRef) {
|
||||||
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath or omit for stub)" -ForegroundColor Red
|
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath or omit for stub)" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
@@ -168,8 +386,20 @@ if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
|||||||
$autoBasePath = Join-Path $env:TEMP "epf_stub_db_$(Get-Random)"
|
$autoBasePath = Join-Path $env:TEMP "epf_stub_db_$(Get-Random)"
|
||||||
$stubScript = Join-Path $PSScriptRoot "stub-db-create.ps1"
|
$stubScript = Join-Path $PSScriptRoot "stub-db-create.ps1"
|
||||||
Write-Host "No database specified. Creating temporary stub database..."
|
Write-Host "No database specified. Creating temporary stub database..."
|
||||||
$stubArgs = "-SourceDir `"$sourceDir`" -V8Path `"$V8Path`" -TempBasePath `"$autoBasePath`""
|
# The stub runs its own platform processes (CREATEINFOBASE, LoadConfigFromFiles,
|
||||||
$stubProc = Start-Process -FilePath "powershell.exe" -ArgumentList "-NoProfile -File `"$stubScript`" $stubArgs" -NoNewWindow -Wait -PassThru
|
# UpdateDBCfg) — they need the same extra arguments as the final build. Only the
|
||||||
|
# explicit ones are forwarded: the stub reads .v8-project.json itself.
|
||||||
|
# Invoked via -Command, not -File: -File takes the tail literally, so an array
|
||||||
|
# parameter would arrive as a single comma-glued token.
|
||||||
|
$q = { param($s) "'" + ($s -replace "'", "''") + "'" }
|
||||||
|
$stubCmd = "& $(& $q $stubScript) -SourceDir $(& $q $sourceDir) -V8Path $(& $q $V8Path) -TempBasePath $(& $q $autoBasePath)"
|
||||||
|
if ($AdditionalV8Arguments.Count -gt 0) {
|
||||||
|
$stubCmd += " -AdditionalV8Arguments " + (($AdditionalV8Arguments | ForEach-Object { & $q $_ }) -join ',')
|
||||||
|
}
|
||||||
|
if ($AdditionalIbcmdArguments.Count -gt 0) {
|
||||||
|
$stubCmd += " -AdditionalIbcmdArguments " + (($AdditionalIbcmdArguments | ForEach-Object { & $q $_ }) -join ',')
|
||||||
|
}
|
||||||
|
$stubProc = Start-Process -FilePath "powershell.exe" -ArgumentList "-NoProfile -Command `"$stubCmd`"" -NoNewWindow -Wait -PassThru
|
||||||
if ($stubProc.ExitCode -ne 0) {
|
if ($stubProc.ExitCode -ne 0) {
|
||||||
Write-Host "Error: failed to create stub database" -ForegroundColor Red
|
Write-Host "Error: failed to create stub database" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
@@ -202,8 +432,9 @@ try {
|
|||||||
if ($UserName) { $arguments += "--user=$UserName" }
|
if ($UserName) { $arguments += "--user=$UserName" }
|
||||||
if ($Password) { $arguments += "--password=$Password" }
|
if ($Password) { $arguments += "--password=$Password" }
|
||||||
$arguments += "--data=$tempDir"
|
$arguments += "--data=$tempDir"
|
||||||
Write-Host "Running: ibcmd $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))"
|
$arguments += $extraArgs
|
||||||
$__ib = Invoke-IbcmdProcess $V8Path $arguments
|
Write-Host "Running: ibcmd $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
|
||||||
|
$__ib = Invoke-PlatformProcess $V8Path $arguments
|
||||||
$output = $__ib.Output
|
$output = $__ib.Output
|
||||||
$exitCode = $__ib.ExitCode
|
$exitCode = $__ib.ExitCode
|
||||||
$outMissing = ($exitCode -eq 0) -and -not (Test-OutputNonEmpty $OutputFile)
|
$outMissing = ($exitCode -eq 0) -and -not (Test-OutputNonEmpty $OutputFile)
|
||||||
@@ -215,7 +446,7 @@ try {
|
|||||||
} else {
|
} else {
|
||||||
Write-Host "Error building external data processor/report (code: $exitCode)" -ForegroundColor Red
|
Write-Host "Error building external data processor/report (code: $exitCode)" -ForegroundColor Red
|
||||||
}
|
}
|
||||||
if ($output) { Write-Host ($output | Out-String) }
|
Write-PlatformOutput $output
|
||||||
exit $exitCode
|
exit $exitCode
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,11 +469,12 @@ try {
|
|||||||
$outFile = Join-Path $tempDir "build_log.txt"
|
$outFile = Join-Path $tempDir "build_log.txt"
|
||||||
$arguments += "/Out", "`"$outFile`""
|
$arguments += "/Out", "`"$outFile`""
|
||||||
$arguments += "/DisableStartupDialogs"
|
$arguments += "/DisableStartupDialogs"
|
||||||
|
$arguments += $extraArgs
|
||||||
|
|
||||||
# --- Execute ---
|
# --- Execute ---
|
||||||
Write-Host "Running: 1cv8.exe $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))"
|
Write-Host "Running: 1cv8.exe $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
|
||||||
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
|
$__v8 = Invoke-PlatformProcess $V8Path $arguments -PreQuoted
|
||||||
$exitCode = $process.ExitCode
|
$exitCode = $__v8.ExitCode
|
||||||
|
|
||||||
# --- Result ---
|
# --- Result ---
|
||||||
# Postcondition: exit 0 without a non-empty output file is a false success.
|
# Postcondition: exit 0 without a non-empty output file is a false success.
|
||||||
@@ -264,6 +496,7 @@ try {
|
|||||||
Write-Host "--- End ---"
|
Write-Host "--- End ---"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Write-PlatformOutput $__v8.Output
|
||||||
|
|
||||||
exit $exitCode
|
exit $exitCode
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# epf-build v1.9 — Build external data processor or report (EPF/ERF) from XML sources
|
# epf-build v1.13 — Build external data processor or report (EPF/ERF) from XML sources
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -14,6 +14,28 @@ import subprocess
|
|||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
|
# Регистронезависимый ввод — паритет с 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)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def _find_project_v8path():
|
def _find_project_v8path():
|
||||||
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
||||||
@@ -36,6 +58,163 @@ def _find_project_v8path():
|
|||||||
d = parent
|
d = parent
|
||||||
|
|
||||||
|
|
||||||
|
# --- Additional platform arguments ---
|
||||||
|
V8_OWNED_KEYS = [
|
||||||
|
"DESIGNER", "ENTERPRISE", "CREATEINFOBASE", "CONFIG",
|
||||||
|
"/F", "/S", "/N", "/P", "/Out", "/DisableStartupDialogs",
|
||||||
|
"/UseTemplate", "/AddToList", "/Execute", "/C", "/URL", "/UC",
|
||||||
|
"/DumpIB", "/RestoreIB", "/DumpCfg", "/LoadCfg",
|
||||||
|
"/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg",
|
||||||
|
"/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles",
|
||||||
|
]
|
||||||
|
IBCMD_OWNED_KEYS = [
|
||||||
|
"--db-path", "--data", "--out", "--file", "--load", "--restore",
|
||||||
|
"--import", "--export", "--apply", "--force", "--create-database",
|
||||||
|
"--user", "--password",
|
||||||
|
]
|
||||||
|
V8_SECRET_KEYS = ["/P", "/UC", "/WSP", "/AWSP"]
|
||||||
|
IBCMD_SECRET_KEYS = ["--password", "--token", "--db-pwd"]
|
||||||
|
|
||||||
|
|
||||||
|
def arg_key_match(token, key):
|
||||||
|
"""Token matches a key when it equals it, or starts with it and the next character
|
||||||
|
is not a letter — catches glued /N"user" and --password=x, while keeping
|
||||||
|
/ClearCache distinct from /C."""
|
||||||
|
if len(token) < len(key):
|
||||||
|
return False
|
||||||
|
if token[: len(key)].lower() != key.lower():
|
||||||
|
return False
|
||||||
|
if len(token) == len(key):
|
||||||
|
return True
|
||||||
|
return not token[len(key)].isalpha()
|
||||||
|
|
||||||
|
|
||||||
|
def project_extra_args(name):
|
||||||
|
"""v8args / ibcmdargs from .v8-project.json — same upward walk as v8path."""
|
||||||
|
d = os.getcwd()
|
||||||
|
while True:
|
||||||
|
pf = os.path.join(d, ".v8-project.json")
|
||||||
|
if os.path.isfile(pf):
|
||||||
|
try:
|
||||||
|
with open(pf, encoding="utf-8-sig") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
v = data.get(name)
|
||||||
|
if v:
|
||||||
|
return [str(x) for x in v]
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return []
|
||||||
|
parent = os.path.dirname(d)
|
||||||
|
if parent == d:
|
||||||
|
return []
|
||||||
|
d = parent
|
||||||
|
|
||||||
|
|
||||||
|
def assert_extra_args(extra, engine, hints):
|
||||||
|
"""The platform accepts only one batch operation, and a duplicate connection or
|
||||||
|
output key fails with an opaque 1C error — reject what the skill owns itself."""
|
||||||
|
param = "-AdditionalIbcmdArguments" if engine == "ibcmd" else "-AdditionalV8Arguments"
|
||||||
|
owned = IBCMD_OWNED_KEYS if engine == "ibcmd" else V8_OWNED_KEYS
|
||||||
|
for tok in extra:
|
||||||
|
if engine == "ibcmd" and not tok.startswith("-"):
|
||||||
|
print(
|
||||||
|
f"Error: '{tok}' is a positional token — pass values as --key=value "
|
||||||
|
f"({param} cannot extend the ibcmd command)",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
for k in owned:
|
||||||
|
if arg_key_match(tok, k):
|
||||||
|
hint = f" (use {hints[k]})" if hints and k in hints else ""
|
||||||
|
print(
|
||||||
|
f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def format_args_for_display(arglist, engine):
|
||||||
|
"""Redact values of secret-prone keys in glued, =-joined and separate forms.
|
||||||
|
Matching here is a plain prefix (no letter rule): over-masking costs nothing,
|
||||||
|
a leaked password does."""
|
||||||
|
keys = IBCMD_SECRET_KEYS if engine == "ibcmd" else V8_SECRET_KEYS
|
||||||
|
res = []
|
||||||
|
mask_next = False
|
||||||
|
for tok in arglist:
|
||||||
|
if mask_next:
|
||||||
|
res.append("***")
|
||||||
|
mask_next = False
|
||||||
|
continue
|
||||||
|
hit = None
|
||||||
|
for k in keys:
|
||||||
|
if tok[: len(k)].lower() == k.lower():
|
||||||
|
hit = k
|
||||||
|
break
|
||||||
|
if hit is None:
|
||||||
|
res.append(tok)
|
||||||
|
elif len(tok) == len(hit):
|
||||||
|
res.append(tok)
|
||||||
|
mask_next = True
|
||||||
|
elif tok[len(hit)] == "=":
|
||||||
|
res.append(hit + "=***")
|
||||||
|
else:
|
||||||
|
res.append(hit + "***")
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
def extract_extra_args(argv, known_opts):
|
||||||
|
"""argparse refuses values that start with '-' (every ibcmd key does), so pull the two
|
||||||
|
escape-hatch lists out of argv by hand: after the flag, take everything up to the next
|
||||||
|
declared skill option. Returns (remaining_argv, v8_extra, ibcmd_extra)."""
|
||||||
|
rest, v8, ibcmd = [], [], []
|
||||||
|
i = 0
|
||||||
|
while i < len(argv):
|
||||||
|
low = argv[i].lower()
|
||||||
|
if low in ("-additionalv8arguments", "-additionalibcmdarguments"):
|
||||||
|
target = v8 if low == "-additionalv8arguments" else ibcmd
|
||||||
|
i += 1
|
||||||
|
while i < len(argv) and argv[i].lower() not in known_opts:
|
||||||
|
target.append(argv[i])
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
rest.append(argv[i])
|
||||||
|
i += 1
|
||||||
|
return rest, v8, ibcmd
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints):
|
||||||
|
"""Pick the argument list for the selected engine and validate it. An explicitly
|
||||||
|
passed parameter for the other engine is an error; the same keys coming from
|
||||||
|
.v8-project.json simply do not apply — a project may describe both engines.
|
||||||
|
|
||||||
|
Comma-separated elements are split apart: PowerShell's -File cannot bind an array,
|
||||||
|
so that form is the documented one and both ports must accept it. A value containing
|
||||||
|
a comma is not supported."""
|
||||||
|
v8_extra = [p for tok in v8_extra for p in str(tok).split(",") if p]
|
||||||
|
ibcmd_extra = [p for tok in ibcmd_extra for p in str(tok).split(",") if p]
|
||||||
|
if engine == "ibcmd" and v8_extra:
|
||||||
|
print(
|
||||||
|
"Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd "
|
||||||
|
"(use -AdditionalIbcmdArguments)",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
if engine != "ibcmd" and ibcmd_extra:
|
||||||
|
print(
|
||||||
|
"Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 "
|
||||||
|
"(use -AdditionalV8Arguments)",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
if engine == "ibcmd":
|
||||||
|
extra = project_extra_args("ibcmdargs") + list(ibcmd_extra)
|
||||||
|
else:
|
||||||
|
extra = project_extra_args("v8args") + list(v8_extra)
|
||||||
|
if extra:
|
||||||
|
assert_extra_args(extra, engine, hints)
|
||||||
|
return extra
|
||||||
|
|
||||||
|
|
||||||
def _version_dir(p):
|
def _version_dir(p):
|
||||||
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
|
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
|
||||||
parent = os.path.dirname(p)
|
parent = os.path.dirname(p)
|
||||||
@@ -86,6 +265,85 @@ IBCMD_NOUSER_HINT = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def decode_platform_bytes(data):
|
||||||
|
"""ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
|
||||||
|
OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes — the locale
|
||||||
|
code page (what text=True uses) mangles both."""
|
||||||
|
if not data:
|
||||||
|
return ""
|
||||||
|
try:
|
||||||
|
return data.decode("utf-8")
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
return data.decode("cp866", errors="replace")
|
||||||
|
|
||||||
|
|
||||||
|
def assert_infobase_exists(path):
|
||||||
|
"""These skills work on a ready infobase. Saying so up front beats the platform's
|
||||||
|
"Неверные или отсутствующие параметры соединения" after a launch."""
|
||||||
|
if not path:
|
||||||
|
return
|
||||||
|
if not os.path.isfile(os.path.join(path, "1Cv8.1CD")):
|
||||||
|
print(f"Error: information base not found at {path} (no 1Cv8.1CD)", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def clean_path(value, param=""):
|
||||||
|
"""Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
|
||||||
|
surrounding quotes that survived shell parsing, a trailing separator. A quote left
|
||||||
|
inside afterwards cannot be part of a real path — reject it by name instead of letting
|
||||||
|
1C answer with its opaque "Неверные или отсутствующие параметры соединения"."""
|
||||||
|
if not value:
|
||||||
|
return value
|
||||||
|
v = value.strip()
|
||||||
|
if len(v) >= 2 and v[0] == v[-1] and v[0] in "\"'":
|
||||||
|
v = v[1:-1].strip()
|
||||||
|
if len(v) > 3 and v[-1] in "\\/":
|
||||||
|
v = v[:-1]
|
||||||
|
if '"' in v:
|
||||||
|
print(f"Error: {param or 'path'} contains a quote character: {value}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
return v
|
||||||
|
|
||||||
|
|
||||||
|
def quote_if_needed(token):
|
||||||
|
"""Extra arguments come from the caller unquoted; the 1cv8 command line is joined
|
||||||
|
verbatim, so a token with a space needs quotes of its own."""
|
||||||
|
if token and (" " in token or "\t" in token) and '"' not in token:
|
||||||
|
return f'"{token}"'
|
||||||
|
return token
|
||||||
|
|
||||||
|
|
||||||
|
def run_v8(v8path, arguments):
|
||||||
|
"""Run 1cv8 in batch mode and capture its console output.
|
||||||
|
|
||||||
|
The arguments carry their own quotes inside the value (File="C:\\a b") — that is where
|
||||||
|
1C's parser expects them, on Windows and on *nix alike. Windows list2cmdline would
|
||||||
|
escape those quotes, so there the command line is handed over ready-made.
|
||||||
|
"""
|
||||||
|
if os.name == "nt":
|
||||||
|
cmd = '"' + v8path + '" ' + " ".join(arguments)
|
||||||
|
else:
|
||||||
|
cmd = [v8path] + arguments
|
||||||
|
r = subprocess.run(cmd, input=b"", capture_output=True)
|
||||||
|
r.stdout = decode_platform_bytes(r.stdout)
|
||||||
|
r.stderr = decode_platform_bytes(r.stderr)
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
|
def print_platform_output(result):
|
||||||
|
"""Print what the platform wrote to the console as its own labelled block. Silence stays
|
||||||
|
silent: in batch mode 1cv8 reports through /Out and prints nothing here."""
|
||||||
|
text = ((result.stdout or "") + (result.stderr or "")).rstrip()
|
||||||
|
if not text:
|
||||||
|
return
|
||||||
|
limit = 65536
|
||||||
|
if len(text) > limit:
|
||||||
|
text = f"[... обрезано, показаны последние {limit} символов ...]\n" + text[-limit:]
|
||||||
|
print("--- Вывод платформы ---")
|
||||||
|
print(text)
|
||||||
|
print("--- End ---")
|
||||||
|
|
||||||
|
|
||||||
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
||||||
"""Run an ibcmd command non-interactively.
|
"""Run an ibcmd command non-interactively.
|
||||||
|
|
||||||
@@ -96,7 +354,10 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
|||||||
if warn_no_user and os.name == "nt" and not has_username:
|
if warn_no_user and os.name == "nt" and not has_username:
|
||||||
sys.stderr.write(IBCMD_NOUSER_HINT)
|
sys.stderr.write(IBCMD_NOUSER_HINT)
|
||||||
sys.stderr.flush()
|
sys.stderr.flush()
|
||||||
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
|
r = subprocess.run(cmd, input=b"", capture_output=True)
|
||||||
|
r.stdout = decode_platform_bytes(r.stdout)
|
||||||
|
r.stderr = decode_platform_bytes(r.stderr)
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
def output_nonempty(path):
|
def output_nonempty(path):
|
||||||
@@ -129,11 +390,35 @@ def main():
|
|||||||
parser.add_argument("-Password", default="", help="1C user password")
|
parser.add_argument("-Password", default="", help="1C user password")
|
||||||
parser.add_argument("-SourceFile", required=True, help="Path to root XML source file")
|
parser.add_argument("-SourceFile", required=True, help="Path to root XML source file")
|
||||||
parser.add_argument("-OutputFile", required=True, help="Path to output EPF/ERF file")
|
parser.add_argument("-OutputFile", required=True, help="Path to output EPF/ERF file")
|
||||||
args = parser.parse_args()
|
parser.add_argument("-AdditionalV8Arguments", nargs="*", default=[],
|
||||||
|
help="Extra 1cv8 arguments, e.g. /UseHwLicenses+")
|
||||||
|
parser.add_argument("-AdditionalIbcmdArguments", nargs="*", default=[],
|
||||||
|
help="Extra ibcmd arguments in --key=value form")
|
||||||
|
known_opts = {s.lower() for a in parser._actions for s in a.option_strings}
|
||||||
|
argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts)
|
||||||
|
args = ci_parse_args(parser, argv)
|
||||||
|
|
||||||
|
args.V8Path = clean_path(args.V8Path, "-V8Path")
|
||||||
|
args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath")
|
||||||
|
assert_infobase_exists(args.InfoBasePath)
|
||||||
|
args.SourceFile = clean_path(args.SourceFile, "-SourceFile")
|
||||||
|
args.OutputFile = clean_path(args.OutputFile, "-OutputFile")
|
||||||
|
|
||||||
# --- Resolve V8Path ---
|
# --- Resolve V8Path ---
|
||||||
v8path = resolve_v8path(args.V8Path)
|
v8path = resolve_v8path(args.V8Path)
|
||||||
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
|
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
|
||||||
|
|
||||||
|
# --- Resolve additional arguments for the selected engine ---
|
||||||
|
arg_hints = {
|
||||||
|
"/F": "-InfoBasePath",
|
||||||
|
"/S": "-InfoBaseServer + -InfoBaseRef",
|
||||||
|
"/N": "-UserName",
|
||||||
|
"/P": "-Password",
|
||||||
|
"--db-path": "-InfoBasePath",
|
||||||
|
"--user": "-UserName",
|
||||||
|
"--password": "-Password",
|
||||||
|
}
|
||||||
|
extra_args = resolve_extra_args(engine, v8_extra, ibcmd_extra, arg_hints)
|
||||||
if engine == "ibcmd" and args.InfoBaseServer and args.InfoBaseRef:
|
if engine == "ibcmd" and args.InfoBaseServer and args.InfoBaseRef:
|
||||||
print("Error: ibcmd supports file infobases only (use -InfoBasePath or omit for stub)", file=sys.stderr)
|
print("Error: ibcmd supports file infobases only (use -InfoBasePath or omit for stub)", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
@@ -145,10 +430,16 @@ def main():
|
|||||||
auto_base_path = os.path.join(tempfile.gettempdir(), f"epf_stub_db_{random.randint(0, 999999)}")
|
auto_base_path = os.path.join(tempfile.gettempdir(), f"epf_stub_db_{random.randint(0, 999999)}")
|
||||||
stub_script = os.path.join(os.path.dirname(os.path.abspath(__file__)), "stub-db-create.py")
|
stub_script = os.path.join(os.path.dirname(os.path.abspath(__file__)), "stub-db-create.py")
|
||||||
print("No database specified. Creating temporary stub database...")
|
print("No database specified. Creating temporary stub database...")
|
||||||
result = subprocess.run(
|
stub_cmd = [sys.executable, stub_script, "-SourceDir", source_dir, "-V8Path", v8path,
|
||||||
[sys.executable, stub_script, "-SourceDir", source_dir, "-V8Path", v8path, "-TempBasePath", auto_base_path],
|
"-TempBasePath", auto_base_path]
|
||||||
capture_output=False,
|
# The stub runs its own platform processes (CREATEINFOBASE, LoadConfigFromFiles,
|
||||||
)
|
# UpdateDBCfg) — they need the same extra arguments as the final build. Only the
|
||||||
|
# explicit ones are forwarded: the stub reads .v8-project.json itself.
|
||||||
|
if v8_extra:
|
||||||
|
stub_cmd += ["-AdditionalV8Arguments"] + list(v8_extra)
|
||||||
|
if ibcmd_extra:
|
||||||
|
stub_cmd += ["-AdditionalIbcmdArguments"] + list(ibcmd_extra)
|
||||||
|
result = subprocess.run(stub_cmd, capture_output=False)
|
||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
print("Error: failed to create stub database", file=sys.stderr)
|
print("Error: failed to create stub database", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
@@ -181,7 +472,8 @@ def main():
|
|||||||
if args.Password:
|
if args.Password:
|
||||||
arguments.append(f"--password={args.Password}")
|
arguments.append(f"--password={args.Password}")
|
||||||
arguments.append(f"--data={ib_data}")
|
arguments.append(f"--data={ib_data}")
|
||||||
print(f"Running: ibcmd {_redact(' '.join(arguments), args.Password, args.UserName)}")
|
arguments.extend(extra_args)
|
||||||
|
print(f"Running: ibcmd {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
|
||||||
result = run_ibcmd([v8path] + arguments, warn_no_user=False)
|
result = run_ibcmd([v8path] + arguments, warn_no_user=False)
|
||||||
exit_code = result.returncode
|
exit_code = result.returncode
|
||||||
out_missing = exit_code == 0 and not output_nonempty(args.OutputFile)
|
out_missing = exit_code == 0 and not output_nonempty(args.OutputFile)
|
||||||
@@ -193,39 +485,32 @@ def main():
|
|||||||
print(f"Error: exit code 0 but no non-empty file at {args.OutputFile} — build produced no output", file=sys.stderr)
|
print(f"Error: exit code 0 but no non-empty file at {args.OutputFile} — build produced no output", file=sys.stderr)
|
||||||
else:
|
else:
|
||||||
print(f"Error building external data processor/report (code: {exit_code})", file=sys.stderr)
|
print(f"Error building external data processor/report (code: {exit_code})", file=sys.stderr)
|
||||||
if result.stdout:
|
|
||||||
print(result.stdout)
|
|
||||||
if result.stderr:
|
|
||||||
print(result.stderr, file=sys.stderr)
|
|
||||||
sys.exit(exit_code)
|
sys.exit(exit_code)
|
||||||
|
|
||||||
# --- Build arguments ---
|
# --- Build arguments ---
|
||||||
arguments = ["DESIGNER"]
|
arguments = ["DESIGNER"]
|
||||||
|
|
||||||
if args.InfoBaseServer and args.InfoBaseRef:
|
if args.InfoBaseServer and args.InfoBaseRef:
|
||||||
arguments += ["/S", f"{args.InfoBaseServer}/{args.InfoBaseRef}"]
|
arguments += ["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"']
|
||||||
else:
|
else:
|
||||||
arguments += ["/F", args.InfoBasePath]
|
arguments += ["/F", f'"{args.InfoBasePath}"']
|
||||||
|
|
||||||
if args.UserName:
|
if args.UserName:
|
||||||
arguments.append(f"/N{args.UserName}")
|
arguments.append(f'/N"{args.UserName}"')
|
||||||
if args.Password:
|
if args.Password:
|
||||||
arguments.append(f"/P{args.Password}")
|
arguments.append(f'/P"{args.Password}"')
|
||||||
|
|
||||||
arguments += ["/LoadExternalDataProcessorOrReportFromFiles", args.SourceFile, args.OutputFile]
|
arguments += ["/LoadExternalDataProcessorOrReportFromFiles", f'"{args.SourceFile}"', f'"{args.OutputFile}"']
|
||||||
|
|
||||||
# --- Output ---
|
# --- Output ---
|
||||||
out_file = os.path.join(temp_dir, "build_log.txt")
|
out_file = os.path.join(temp_dir, "build_log.txt")
|
||||||
arguments += ["/Out", out_file]
|
arguments += ["/Out", f'"{out_file}"']
|
||||||
arguments.append("/DisableStartupDialogs")
|
arguments.append("/DisableStartupDialogs")
|
||||||
|
arguments.extend(quote_if_needed(a) for a in extra_args)
|
||||||
|
|
||||||
# --- Execute ---
|
# --- Execute ---
|
||||||
print(f"Running: 1cv8.exe {_redact(' '.join(arguments), args.Password, args.UserName)}")
|
print(f"Running: 1cv8.exe {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
|
||||||
result = subprocess.run(
|
result = run_v8(v8path, arguments)
|
||||||
[v8path] + arguments,
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
)
|
|
||||||
exit_code = result.returncode
|
exit_code = result.returncode
|
||||||
|
|
||||||
# --- Result ---
|
# --- Result ---
|
||||||
@@ -251,6 +536,7 @@ def main():
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
print_platform_output(result)
|
||||||
sys.exit(exit_code)
|
sys.exit(exit_code)
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# stub-db-create v1.3 — Create temp 1C infobase with metadata stubs for EPF/ERF build
|
# stub-db-create v1.7 — Create temp 1C infobase with metadata stubs for EPF/ERF build
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory)]
|
||||||
@@ -7,12 +7,162 @@ param(
|
|||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory)]
|
||||||
[string]$V8Path,
|
[string]$V8Path,
|
||||||
|
|
||||||
[string]$TempBasePath
|
[string]$TempBasePath,
|
||||||
|
|
||||||
|
[string[]]$AdditionalV8Arguments = @(),
|
||||||
|
|
||||||
|
[string[]]$AdditionalIbcmdArguments = @()
|
||||||
)
|
)
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
|
||||||
|
function ConvertTo-CleanPath {
|
||||||
|
# Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
|
||||||
|
# surrounding quotes that survived shell parsing, a trailing separator. A quote left
|
||||||
|
# inside afterwards cannot be part of a real path — reject it by name instead of letting
|
||||||
|
# 1C answer with its opaque "Неверные или отсутствующие параметры соединения".
|
||||||
|
param([string]$Value, [string]$ParamName)
|
||||||
|
if (-not $Value) { return $Value }
|
||||||
|
$v = $Value.Trim()
|
||||||
|
if ($v.Length -ge 2 -and $v[0] -eq $v[-1] -and ($v[0] -eq '"' -or $v[0] -eq "'")) {
|
||||||
|
$v = $v.Substring(1, $v.Length - 2).Trim()
|
||||||
|
}
|
||||||
|
if ($v.Length -gt 3 -and ($v[-1] -eq '\' -or $v[-1] -eq '/')) { $v = $v.Substring(0, $v.Length - 1) }
|
||||||
|
if ($v.Contains('"')) {
|
||||||
|
Write-Host "Error: $ParamName contains a quote character: $Value" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
return $v
|
||||||
|
}
|
||||||
|
|
||||||
|
$SourceDir = ConvertTo-CleanPath $SourceDir '-SourceDir'
|
||||||
|
$V8Path = ConvertTo-CleanPath $V8Path '-V8Path'
|
||||||
|
$TempBasePath = ConvertTo-CleanPath $TempBasePath '-TempBasePath'
|
||||||
|
|
||||||
|
# --- Additional platform arguments ---
|
||||||
|
$script:V8OwnedKeys = @(
|
||||||
|
'DESIGNER', 'ENTERPRISE', 'CREATEINFOBASE', 'CONFIG',
|
||||||
|
'/F', '/S', '/N', '/P', '/Out', '/DisableStartupDialogs',
|
||||||
|
'/UseTemplate', '/AddToList', '/Execute', '/C', '/URL', '/UC',
|
||||||
|
'/DumpIB', '/RestoreIB', '/DumpCfg', '/LoadCfg',
|
||||||
|
'/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg',
|
||||||
|
'/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles'
|
||||||
|
)
|
||||||
|
$script:IbcmdOwnedKeys = @(
|
||||||
|
'--db-path', '--data', '--out', '--file', '--load', '--restore',
|
||||||
|
'--import', '--export', '--apply', '--force', '--create-database',
|
||||||
|
'--user', '--password'
|
||||||
|
)
|
||||||
|
$script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP')
|
||||||
|
$script:IbcmdSecretKeys = @('--password', '--token', '--db-pwd')
|
||||||
|
|
||||||
|
function Test-ArgKeyMatch {
|
||||||
|
# A token matches a key when it equals the key, or starts with it and the next
|
||||||
|
# character is not a letter — catches glued /N"user" and --password=x, while
|
||||||
|
# keeping /ClearCache distinct from /C.
|
||||||
|
param([string]$Token, [string]$Key)
|
||||||
|
if ($Token.Length -lt $Key.Length) { return $false }
|
||||||
|
if (-not $Token.Substring(0, $Key.Length).Equals($Key, [System.StringComparison]::OrdinalIgnoreCase)) { return $false }
|
||||||
|
if ($Token.Length -eq $Key.Length) { return $true }
|
||||||
|
return -not [char]::IsLetter($Token[$Key.Length])
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-ProjectExtraArgs {
|
||||||
|
# v8args / ibcmdargs from .v8-project.json — same upward walk as v8path.
|
||||||
|
param([string]$Name)
|
||||||
|
$dir = (Get-Location).Path
|
||||||
|
while ($dir) {
|
||||||
|
$pf = Join-Path $dir ".v8-project.json"
|
||||||
|
if (Test-Path $pf) {
|
||||||
|
try {
|
||||||
|
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||||
|
if ($j.$Name) { return @($j.$Name | ForEach-Object { [string]$_ }) }
|
||||||
|
} catch {}
|
||||||
|
return @()
|
||||||
|
}
|
||||||
|
$parent = Split-Path $dir -Parent
|
||||||
|
if (-not $parent -or $parent -eq $dir) { break }
|
||||||
|
$dir = $parent
|
||||||
|
}
|
||||||
|
return @()
|
||||||
|
}
|
||||||
|
|
||||||
|
function Assert-ExtraArgs {
|
||||||
|
# The platform accepts only one batch operation, and a duplicate connection or
|
||||||
|
# output key fails with an opaque 1C error — reject what the skill owns itself.
|
||||||
|
param([string[]]$ExtraArgs, [string]$Engine, [hashtable]$Hints)
|
||||||
|
$paramName = if ($Engine -eq 'ibcmd') { '-AdditionalIbcmdArguments' } else { '-AdditionalV8Arguments' }
|
||||||
|
$owned = if ($Engine -eq 'ibcmd') { $script:IbcmdOwnedKeys } else { $script:V8OwnedKeys }
|
||||||
|
foreach ($tok in $ExtraArgs) {
|
||||||
|
if ($Engine -eq 'ibcmd' -and $tok -notmatch '^-') {
|
||||||
|
Write-Host "Error: '$tok' is a positional token — pass values as --key=value ($paramName cannot extend the ibcmd command)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
foreach ($k in $owned) {
|
||||||
|
if (Test-ArgKeyMatch $tok $k) {
|
||||||
|
$hint = ''
|
||||||
|
if ($Hints -and $Hints.ContainsKey($k)) { $hint = " (use $($Hints[$k]))" }
|
||||||
|
Write-Host "Error: $k is controlled by the skill and cannot be passed via $paramName$hint" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Resolve-ExtraArgs {
|
||||||
|
# Pick the argument list for the selected engine and validate it. An explicitly passed
|
||||||
|
# parameter for the other engine is an error; the same keys coming from .v8-project.json
|
||||||
|
# simply do not apply — a project may describe both engines.
|
||||||
|
param([string]$Engine, [string[]]$V8Extra, [string[]]$IbcmdExtra, [hashtable]$Hints)
|
||||||
|
# powershell.exe -File — how skills are invoked — cannot bind an array parameter:
|
||||||
|
# space-separated values spill into positional ones, a comma-joined list arrives as a
|
||||||
|
# single token. So accept the repo's list convention (comma-separated) and split here;
|
||||||
|
# a native array call keeps working. A value containing a comma is not supported.
|
||||||
|
$V8Extra = @($V8Extra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
|
||||||
|
$IbcmdExtra = @($IbcmdExtra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
|
||||||
|
if ($Engine -eq 'ibcmd' -and $V8Extra.Count -gt 0) {
|
||||||
|
Write-Host "Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd (use -AdditionalIbcmdArguments)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
if ($Engine -ne 'ibcmd' -and $IbcmdExtra.Count -gt 0) {
|
||||||
|
Write-Host "Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 (use -AdditionalV8Arguments)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
if ($Engine -eq 'ibcmd') {
|
||||||
|
$extra = @(Get-ProjectExtraArgs 'ibcmdargs') + @($IbcmdExtra)
|
||||||
|
} else {
|
||||||
|
$extra = @(Get-ProjectExtraArgs 'v8args') + @($V8Extra)
|
||||||
|
}
|
||||||
|
if ($extra.Count -gt 0) { Assert-ExtraArgs $extra $Engine $Hints }
|
||||||
|
# Plain return, no comma trick: the caller re-collects with @(...), and ,@() there
|
||||||
|
# would nest the array — the tokens would then be glued into one argument.
|
||||||
|
return $extra
|
||||||
|
}
|
||||||
|
|
||||||
|
function Format-ArgsForDisplay {
|
||||||
|
# Redact values of secret-prone keys in glued, =-joined and separate forms.
|
||||||
|
# Matching here is a plain prefix (no letter rule): over-masking costs nothing,
|
||||||
|
# a leaked password does.
|
||||||
|
param([string[]]$ArgList, [string]$Engine)
|
||||||
|
$keys = if ($Engine -eq 'ibcmd') { $script:IbcmdSecretKeys } else { $script:V8SecretKeys }
|
||||||
|
$res = @()
|
||||||
|
$maskNext = $false
|
||||||
|
foreach ($tok in $ArgList) {
|
||||||
|
if ($maskNext) { $res += '***'; $maskNext = $false; continue }
|
||||||
|
$hit = $null
|
||||||
|
foreach ($k in $keys) {
|
||||||
|
if ($tok.Length -ge $k.Length -and $tok.Substring(0, $k.Length).Equals($k, [System.StringComparison]::OrdinalIgnoreCase)) { $hit = $k; break }
|
||||||
|
}
|
||||||
|
if (-not $hit) { $res += $tok; continue }
|
||||||
|
if ($tok.Length -eq $hit.Length) { $res += $tok; $maskNext = $true }
|
||||||
|
elseif ($tok[$hit.Length] -eq '=') { $res += ($hit + '=***') }
|
||||||
|
else { $res += ($hit + '***') }
|
||||||
|
}
|
||||||
|
return ,$res
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# --- 1. Scan XML files for reference types ---
|
# --- 1. Scan XML files for reference types ---
|
||||||
|
|
||||||
$typeMap = @{} # MetadataType -> @(Name1, Name2, ...)
|
$typeMap = @{} # MetadataType -> @(Name1, Name2, ...)
|
||||||
@@ -1253,34 +1403,89 @@ $propsXml </Properties>$childObjLine
|
|||||||
}
|
}
|
||||||
|
|
||||||
# --- 5a. Stub via ibcmd (one call: create [--import --apply]) ---
|
# --- 5a. Stub via ibcmd (one call: create [--import --apply]) ---
|
||||||
function Invoke-IbcmdProcess {
|
function ConvertFrom-PlatformBytes {
|
||||||
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
|
# ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
|
||||||
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
|
# OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes — guessing
|
||||||
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
|
# one of them outright mangles Cyrillic.
|
||||||
param([string]$Exe, [string[]]$IbArgs)
|
param([byte[]]$Bytes)
|
||||||
|
if (-not $Bytes -or $Bytes.Length -eq 0) { return '' }
|
||||||
|
try {
|
||||||
|
$strict = New-Object System.Text.UTF8Encoding($false, $true)
|
||||||
|
return $strict.GetString($Bytes)
|
||||||
|
} catch {
|
||||||
|
return [System.Text.Encoding]::GetEncoding(866).GetString($Bytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-PlatformProcess {
|
||||||
|
# Run the platform non-interactively and capture its console output. A closed stdin pipe
|
||||||
|
# (EOF) makes an auth prompt fast-fail instead of hanging; capturing keeps the child's
|
||||||
|
# text out of our stream until we print it labelled (and out of the wrong encoding).
|
||||||
|
# Returns @{ Output; ExitCode }.
|
||||||
|
#
|
||||||
|
# Quoting differs by engine, so the caller says which it built:
|
||||||
|
# ibcmd — tokens are bare (--db-path=C:\a b), the whole token gets quoted here;
|
||||||
|
# 1cv8 — -PreQuoted: the caller already put quotes inside the token (File="C:\a b"),
|
||||||
|
# which is where 1C's own parser expects them; quoting again breaks the value.
|
||||||
|
param([string]$Exe, [string[]]$ProcArgs, [switch]$PreQuoted)
|
||||||
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
||||||
$psi.FileName = $Exe
|
$psi.FileName = $Exe
|
||||||
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
|
$psi.Arguments = if ($PreQuoted) {
|
||||||
|
$ProcArgs -join ' '
|
||||||
|
} else {
|
||||||
|
($ProcArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
|
||||||
|
}
|
||||||
$psi.UseShellExecute = $false
|
$psi.UseShellExecute = $false
|
||||||
$psi.CreateNoWindow = $true
|
$psi.CreateNoWindow = $true
|
||||||
$psi.RedirectStandardInput = $true
|
$psi.RedirectStandardInput = $true
|
||||||
$psi.RedirectStandardOutput = $true
|
$psi.RedirectStandardOutput = $true
|
||||||
$psi.RedirectStandardError = $true
|
$psi.RedirectStandardError = $true
|
||||||
try {
|
|
||||||
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
|
|
||||||
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
|
|
||||||
} catch {}
|
|
||||||
$p = [System.Diagnostics.Process]::Start($psi)
|
$p = [System.Diagnostics.Process]::Start($psi)
|
||||||
$p.StandardInput.Close()
|
$p.StandardInput.Close()
|
||||||
$out = $p.StandardOutput.ReadToEnd()
|
# stderr is drained in parallel: reading the streams one after another deadlocks
|
||||||
$err = $p.StandardError.ReadToEnd()
|
# as soon as the other one fills its pipe buffer.
|
||||||
|
$errMs = New-Object System.IO.MemoryStream
|
||||||
|
$errTask = $p.StandardError.BaseStream.CopyToAsync($errMs)
|
||||||
|
$outMs = New-Object System.IO.MemoryStream
|
||||||
|
$p.StandardOutput.BaseStream.CopyTo($outMs)
|
||||||
|
$errTask.Wait()
|
||||||
$p.WaitForExit()
|
$p.WaitForExit()
|
||||||
|
$out = ConvertFrom-PlatformBytes $outMs.ToArray()
|
||||||
|
$err = ConvertFrom-PlatformBytes $errMs.ToArray()
|
||||||
if ($err) { $out += $err }
|
if ($err) { $out += $err }
|
||||||
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
|
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Write-PlatformOutput {
|
||||||
|
# Print what the platform wrote to the console as its own labelled block. Silence stays
|
||||||
|
# silent: in batch mode 1cv8 reports through /Out and prints nothing here.
|
||||||
|
param([string]$Text)
|
||||||
|
if (-not $Text) { return }
|
||||||
|
$t = $Text.TrimEnd()
|
||||||
|
if (-not $t) { return }
|
||||||
|
$limit = 65536
|
||||||
|
if ($t.Length -gt $limit) {
|
||||||
|
$t = "[... обрезано, показаны последние $limit символов ...]`r`n" + $t.Substring($t.Length - $limit)
|
||||||
|
}
|
||||||
|
Write-Host "--- Вывод платформы ---"
|
||||||
|
Write-Host $t
|
||||||
|
Write-Host "--- End ---"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
$stubEngine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
$stubEngine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
||||||
|
|
||||||
|
# --- Resolve additional arguments for the selected engine ---
|
||||||
|
$argHints = @{ '/F' = '-TempBasePath'; '--db-path' = '-TempBasePath' }
|
||||||
|
$extraArgs = @(Resolve-ExtraArgs $stubEngine $AdditionalV8Arguments $AdditionalIbcmdArguments $argHints)
|
||||||
|
|
||||||
|
function Format-ArgToken {
|
||||||
|
# Start-Process takes these argument lists as one string, so quote each token that needs it.
|
||||||
|
param([string]$Token)
|
||||||
|
if ($Token -match '[\s"]') { return ' "' + ($Token -replace '"', '\"') + '"' }
|
||||||
|
return " $Token"
|
||||||
|
}
|
||||||
|
$extraArgString = -join ($extraArgs | ForEach-Object { Format-ArgToken $_ })
|
||||||
if ($stubEngine -eq "ibcmd") {
|
if ($stubEngine -eq "ibcmd") {
|
||||||
Write-Host "Creating infobase (ibcmd): $TempBasePath"
|
Write-Host "Creating infobase (ibcmd): $TempBasePath"
|
||||||
$ibData = Join-Path $env:TEMP "stub_data_$(Get-Random)"
|
$ibData = Join-Path $env:TEMP "stub_data_$(Get-Random)"
|
||||||
@@ -1288,12 +1493,13 @@ if ($stubEngine -eq "ibcmd") {
|
|||||||
$ibArgs = @("infobase", "create", "--db-path=$TempBasePath", "--create-database")
|
$ibArgs = @("infobase", "create", "--db-path=$TempBasePath", "--create-database")
|
||||||
if ($hasRefTypes) { $ibArgs += "--import=$(Join-Path $TempBasePath 'cfg')", "--apply", "--force" }
|
if ($hasRefTypes) { $ibArgs += "--import=$(Join-Path $TempBasePath 'cfg')", "--apply", "--force" }
|
||||||
$ibArgs += "--data=$ibData"
|
$ibArgs += "--data=$ibData"
|
||||||
$__ib = Invoke-IbcmdProcess $V8Path $ibArgs
|
$ibArgs += $extraArgs
|
||||||
|
$__ib = Invoke-PlatformProcess $V8Path $ibArgs
|
||||||
$ibOut = $__ib.Output
|
$ibOut = $__ib.Output
|
||||||
$ibRc = $__ib.ExitCode
|
$ibRc = $__ib.ExitCode
|
||||||
Remove-Item -Path $ibData -Recurse -Force -ErrorAction SilentlyContinue
|
Remove-Item -Path $ibData -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
if ($ibRc -ne 0) {
|
if ($ibRc -ne 0) {
|
||||||
if ($ibOut) { Write-Host ($ibOut | Out-String) }
|
Write-PlatformOutput $ibOut
|
||||||
Write-Error "Failed to create stub infobase (code: $ibRc)"
|
Write-Error "Failed to create stub infobase (code: $ibRc)"
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
@@ -1305,9 +1511,10 @@ if ($stubEngine -eq "ibcmd") {
|
|||||||
|
|
||||||
# --- 5. Create infobase ---
|
# --- 5. Create infobase ---
|
||||||
Write-Host "Creating infobase: $TempBasePath"
|
Write-Host "Creating infobase: $TempBasePath"
|
||||||
$createArgs = "CREATEINFOBASE File=`"$TempBasePath`" /DisableStartupDialogs"
|
$createArgs = "CREATEINFOBASE File=`"$TempBasePath`" /DisableStartupDialogs" + $extraArgString
|
||||||
$proc = Start-Process -FilePath $V8Path -ArgumentList $createArgs -NoNewWindow -Wait -PassThru
|
$proc = Invoke-PlatformProcess $V8Path @($createArgs) -PreQuoted
|
||||||
if ($proc.ExitCode -ne 0) {
|
if ($proc.ExitCode -ne 0) {
|
||||||
|
Write-PlatformOutput $proc.Output
|
||||||
Write-Error "Failed to create infobase (code: $($proc.ExitCode))"
|
Write-Error "Failed to create infobase (code: $($proc.ExitCode))"
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
@@ -1318,10 +1525,11 @@ if ($hasRefTypes) {
|
|||||||
# LoadConfigFromFiles
|
# LoadConfigFromFiles
|
||||||
Write-Host "Loading configuration from files..."
|
Write-Host "Loading configuration from files..."
|
||||||
$loadLog = Join-Path $env:TEMP "stub_load_log.txt"
|
$loadLog = Join-Path $env:TEMP "stub_load_log.txt"
|
||||||
$loadArgs = "DESIGNER /F`"$TempBasePath`" /LoadConfigFromFiles `"$cfgDir`" /Out `"$loadLog`" /DisableStartupDialogs"
|
$loadArgs = "DESIGNER /F`"$TempBasePath`" /LoadConfigFromFiles `"$cfgDir`" /Out `"$loadLog`" /DisableStartupDialogs" + $extraArgString
|
||||||
$proc = Start-Process -FilePath $V8Path -ArgumentList $loadArgs -NoNewWindow -Wait -PassThru
|
$proc = Invoke-PlatformProcess $V8Path @($loadArgs) -PreQuoted
|
||||||
if ($proc.ExitCode -ne 0) {
|
if ($proc.ExitCode -ne 0) {
|
||||||
if (Test-Path $loadLog) { Get-Content $loadLog -Raw -ErrorAction SilentlyContinue | Write-Host }
|
if (Test-Path $loadLog) { Get-Content $loadLog -Raw -ErrorAction SilentlyContinue | Write-Host }
|
||||||
|
Write-PlatformOutput $proc.Output
|
||||||
Write-Error "Failed to load config (code: $($proc.ExitCode))"
|
Write-Error "Failed to load config (code: $($proc.ExitCode))"
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
@@ -1329,10 +1537,11 @@ if ($hasRefTypes) {
|
|||||||
# UpdateDBCfg
|
# UpdateDBCfg
|
||||||
Write-Host "Updating database configuration..."
|
Write-Host "Updating database configuration..."
|
||||||
$updateLog = Join-Path $env:TEMP "stub_update_log.txt"
|
$updateLog = Join-Path $env:TEMP "stub_update_log.txt"
|
||||||
$updateArgs = "DESIGNER /F`"$TempBasePath`" /UpdateDBCfg /Out `"$updateLog`" /DisableStartupDialogs"
|
$updateArgs = "DESIGNER /F`"$TempBasePath`" /UpdateDBCfg /Out `"$updateLog`" /DisableStartupDialogs" + $extraArgString
|
||||||
$proc = Start-Process -FilePath $V8Path -ArgumentList $updateArgs -NoNewWindow -Wait -PassThru
|
$proc = Invoke-PlatformProcess $V8Path @($updateArgs) -PreQuoted
|
||||||
if ($proc.ExitCode -ne 0) {
|
if ($proc.ExitCode -ne 0) {
|
||||||
if (Test-Path $updateLog) { Get-Content $updateLog -Raw -ErrorAction SilentlyContinue | Write-Host }
|
if (Test-Path $updateLog) { Get-Content $updateLog -Raw -ErrorAction SilentlyContinue | Write-Host }
|
||||||
|
Write-PlatformOutput $proc.Output
|
||||||
Write-Error "Failed to update DB config (code: $($proc.ExitCode))"
|
Write-Error "Failed to update DB config (code: $($proc.ExitCode))"
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# stub-db-create v1.3 — Create temp 1C infobase with metadata stubs for EPF/ERF build
|
# stub-db-create v1.7 — Create temp 1C infobase with metadata stubs for EPF/ERF build
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -20,6 +20,75 @@ IBCMD_NOUSER_HINT = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def decode_platform_bytes(data):
|
||||||
|
"""ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
|
||||||
|
OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes — the locale
|
||||||
|
code page (what text=True uses) mangles both."""
|
||||||
|
if not data:
|
||||||
|
return ""
|
||||||
|
try:
|
||||||
|
return data.decode("utf-8")
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
return data.decode("cp866", errors="replace")
|
||||||
|
|
||||||
|
|
||||||
|
def clean_path(value, param=""):
|
||||||
|
"""Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
|
||||||
|
surrounding quotes that survived shell parsing, a trailing separator. A quote left
|
||||||
|
inside afterwards cannot be part of a real path — reject it by name instead of letting
|
||||||
|
1C answer with its opaque "Неверные или отсутствующие параметры соединения"."""
|
||||||
|
if not value:
|
||||||
|
return value
|
||||||
|
v = value.strip()
|
||||||
|
if len(v) >= 2 and v[0] == v[-1] and v[0] in "\"'":
|
||||||
|
v = v[1:-1].strip()
|
||||||
|
if len(v) > 3 and v[-1] in "\\/":
|
||||||
|
v = v[:-1]
|
||||||
|
if '"' in v:
|
||||||
|
print(f"Error: {param or 'path'} contains a quote character: {value}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
return v
|
||||||
|
|
||||||
|
|
||||||
|
def quote_if_needed(token):
|
||||||
|
"""Extra arguments come from the caller unquoted; the 1cv8 command line is joined
|
||||||
|
verbatim, so a token with a space needs quotes of its own."""
|
||||||
|
if token and (" " in token or "\t" in token) and '"' not in token:
|
||||||
|
return f'"{token}"'
|
||||||
|
return token
|
||||||
|
|
||||||
|
|
||||||
|
def run_v8(v8path, arguments):
|
||||||
|
"""Run 1cv8 in batch mode and capture its console output.
|
||||||
|
|
||||||
|
The arguments carry their own quotes inside the value (File="C:\\a b") — that is where
|
||||||
|
1C's parser expects them, on Windows and on *nix alike. Windows list2cmdline would
|
||||||
|
escape those quotes, so there the command line is handed over ready-made.
|
||||||
|
"""
|
||||||
|
if os.name == "nt":
|
||||||
|
cmd = '"' + v8path + '" ' + " ".join(arguments)
|
||||||
|
else:
|
||||||
|
cmd = [v8path] + arguments
|
||||||
|
r = subprocess.run(cmd, input=b"", capture_output=True)
|
||||||
|
r.stdout = decode_platform_bytes(r.stdout)
|
||||||
|
r.stderr = decode_platform_bytes(r.stderr)
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
|
def print_platform_output(result):
|
||||||
|
"""Print what the platform wrote to the console as its own labelled block. Silence stays
|
||||||
|
silent: in batch mode 1cv8 reports through /Out and prints nothing here."""
|
||||||
|
text = ((result.stdout or "") + (result.stderr or "")).rstrip()
|
||||||
|
if not text:
|
||||||
|
return
|
||||||
|
limit = 65536
|
||||||
|
if len(text) > limit:
|
||||||
|
text = f"[... обрезано, показаны последние {limit} символов ...]\n" + text[-limit:]
|
||||||
|
print("--- Вывод платформы ---")
|
||||||
|
print(text)
|
||||||
|
print("--- End ---")
|
||||||
|
|
||||||
|
|
||||||
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
||||||
"""Run an ibcmd command non-interactively.
|
"""Run an ibcmd command non-interactively.
|
||||||
|
|
||||||
@@ -30,7 +99,167 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
|||||||
if warn_no_user and os.name == "nt" and not has_username:
|
if warn_no_user and os.name == "nt" and not has_username:
|
||||||
sys.stderr.write(IBCMD_NOUSER_HINT)
|
sys.stderr.write(IBCMD_NOUSER_HINT)
|
||||||
sys.stderr.flush()
|
sys.stderr.flush()
|
||||||
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
|
r = subprocess.run(cmd, input=b"", capture_output=True)
|
||||||
|
r.stdout = decode_platform_bytes(r.stdout)
|
||||||
|
r.stderr = decode_platform_bytes(r.stderr)
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
|
# --- Additional platform arguments ---
|
||||||
|
V8_OWNED_KEYS = [
|
||||||
|
"DESIGNER", "ENTERPRISE", "CREATEINFOBASE", "CONFIG",
|
||||||
|
"/F", "/S", "/N", "/P", "/Out", "/DisableStartupDialogs",
|
||||||
|
"/UseTemplate", "/AddToList", "/Execute", "/C", "/URL", "/UC",
|
||||||
|
"/DumpIB", "/RestoreIB", "/DumpCfg", "/LoadCfg",
|
||||||
|
"/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg",
|
||||||
|
"/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles",
|
||||||
|
]
|
||||||
|
IBCMD_OWNED_KEYS = [
|
||||||
|
"--db-path", "--data", "--out", "--file", "--load", "--restore",
|
||||||
|
"--import", "--export", "--apply", "--force", "--create-database",
|
||||||
|
"--user", "--password",
|
||||||
|
]
|
||||||
|
V8_SECRET_KEYS = ["/P", "/UC", "/WSP", "/AWSP"]
|
||||||
|
IBCMD_SECRET_KEYS = ["--password", "--token", "--db-pwd"]
|
||||||
|
|
||||||
|
|
||||||
|
def arg_key_match(token, key):
|
||||||
|
"""Token matches a key when it equals it, or starts with it and the next character
|
||||||
|
is not a letter — catches glued /N"user" and --password=x, while keeping
|
||||||
|
/ClearCache distinct from /C."""
|
||||||
|
if len(token) < len(key):
|
||||||
|
return False
|
||||||
|
if token[: len(key)].lower() != key.lower():
|
||||||
|
return False
|
||||||
|
if len(token) == len(key):
|
||||||
|
return True
|
||||||
|
return not token[len(key)].isalpha()
|
||||||
|
|
||||||
|
|
||||||
|
def project_extra_args(name):
|
||||||
|
"""v8args / ibcmdargs from .v8-project.json — same upward walk as v8path."""
|
||||||
|
d = os.getcwd()
|
||||||
|
while True:
|
||||||
|
pf = os.path.join(d, ".v8-project.json")
|
||||||
|
if os.path.isfile(pf):
|
||||||
|
try:
|
||||||
|
with open(pf, encoding="utf-8-sig") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
v = data.get(name)
|
||||||
|
if v:
|
||||||
|
return [str(x) for x in v]
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return []
|
||||||
|
parent = os.path.dirname(d)
|
||||||
|
if parent == d:
|
||||||
|
return []
|
||||||
|
d = parent
|
||||||
|
|
||||||
|
|
||||||
|
def assert_extra_args(extra, engine, hints):
|
||||||
|
"""The platform accepts only one batch operation, and a duplicate connection or
|
||||||
|
output key fails with an opaque 1C error — reject what the skill owns itself."""
|
||||||
|
param = "-AdditionalIbcmdArguments" if engine == "ibcmd" else "-AdditionalV8Arguments"
|
||||||
|
owned = IBCMD_OWNED_KEYS if engine == "ibcmd" else V8_OWNED_KEYS
|
||||||
|
for tok in extra:
|
||||||
|
if engine == "ibcmd" and not tok.startswith("-"):
|
||||||
|
print(
|
||||||
|
f"Error: '{tok}' is a positional token — pass values as --key=value "
|
||||||
|
f"({param} cannot extend the ibcmd command)",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
for k in owned:
|
||||||
|
if arg_key_match(tok, k):
|
||||||
|
hint = f" (use {hints[k]})" if hints and k in hints else ""
|
||||||
|
print(
|
||||||
|
f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def format_args_for_display(arglist, engine):
|
||||||
|
"""Redact values of secret-prone keys in glued, =-joined and separate forms.
|
||||||
|
Matching here is a plain prefix (no letter rule): over-masking costs nothing,
|
||||||
|
a leaked password does."""
|
||||||
|
keys = IBCMD_SECRET_KEYS if engine == "ibcmd" else V8_SECRET_KEYS
|
||||||
|
res = []
|
||||||
|
mask_next = False
|
||||||
|
for tok in arglist:
|
||||||
|
if mask_next:
|
||||||
|
res.append("***")
|
||||||
|
mask_next = False
|
||||||
|
continue
|
||||||
|
hit = None
|
||||||
|
for k in keys:
|
||||||
|
if tok[: len(k)].lower() == k.lower():
|
||||||
|
hit = k
|
||||||
|
break
|
||||||
|
if hit is None:
|
||||||
|
res.append(tok)
|
||||||
|
elif len(tok) == len(hit):
|
||||||
|
res.append(tok)
|
||||||
|
mask_next = True
|
||||||
|
elif tok[len(hit)] == "=":
|
||||||
|
res.append(hit + "=***")
|
||||||
|
else:
|
||||||
|
res.append(hit + "***")
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
def extract_extra_args(argv, known_opts):
|
||||||
|
"""argparse refuses values that start with '-' (every ibcmd key does), so pull the two
|
||||||
|
escape-hatch lists out of argv by hand: after the flag, take everything up to the next
|
||||||
|
declared skill option. Returns (remaining_argv, v8_extra, ibcmd_extra)."""
|
||||||
|
rest, v8, ibcmd = [], [], []
|
||||||
|
i = 0
|
||||||
|
while i < len(argv):
|
||||||
|
low = argv[i].lower()
|
||||||
|
if low in ("-additionalv8arguments", "-additionalibcmdarguments"):
|
||||||
|
target = v8 if low == "-additionalv8arguments" else ibcmd
|
||||||
|
i += 1
|
||||||
|
while i < len(argv) and argv[i].lower() not in known_opts:
|
||||||
|
target.append(argv[i])
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
rest.append(argv[i])
|
||||||
|
i += 1
|
||||||
|
return rest, v8, ibcmd
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints):
|
||||||
|
"""Pick the argument list for the selected engine and validate it. An explicitly
|
||||||
|
passed parameter for the other engine is an error; the same keys coming from
|
||||||
|
.v8-project.json simply do not apply — a project may describe both engines.
|
||||||
|
|
||||||
|
Comma-separated elements are split apart: PowerShell's -File cannot bind an array,
|
||||||
|
so that form is the documented one and both ports must accept it. A value containing
|
||||||
|
a comma is not supported."""
|
||||||
|
v8_extra = [p for tok in v8_extra for p in str(tok).split(",") if p]
|
||||||
|
ibcmd_extra = [p for tok in ibcmd_extra for p in str(tok).split(",") if p]
|
||||||
|
if engine == "ibcmd" and v8_extra:
|
||||||
|
print(
|
||||||
|
"Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd "
|
||||||
|
"(use -AdditionalIbcmdArguments)",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
if engine != "ibcmd" and ibcmd_extra:
|
||||||
|
print(
|
||||||
|
"Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 "
|
||||||
|
"(use -AdditionalV8Arguments)",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
if engine == "ibcmd":
|
||||||
|
extra = project_extra_args("ibcmdargs") + list(ibcmd_extra)
|
||||||
|
else:
|
||||||
|
extra = project_extra_args("v8args") + list(v8_extra)
|
||||||
|
if extra:
|
||||||
|
assert_extra_args(extra, engine, hints)
|
||||||
|
return extra
|
||||||
|
|
||||||
|
|
||||||
def new_uuid():
|
def new_uuid():
|
||||||
@@ -802,7 +1031,17 @@ def main():
|
|||||||
parser.add_argument('-SourceDir', required=True)
|
parser.add_argument('-SourceDir', required=True)
|
||||||
parser.add_argument('-V8Path', required=True)
|
parser.add_argument('-V8Path', required=True)
|
||||||
parser.add_argument('-TempBasePath', default='')
|
parser.add_argument('-TempBasePath', default='')
|
||||||
args = parser.parse_args()
|
parser.add_argument('-AdditionalV8Arguments', nargs='*', default=[],
|
||||||
|
help='Extra 1cv8 arguments, e.g. /UseHwLicenses+')
|
||||||
|
parser.add_argument('-AdditionalIbcmdArguments', nargs='*', default=[],
|
||||||
|
help='Extra ibcmd arguments in --key=value form')
|
||||||
|
known_opts = {s.lower() for a in parser._actions for s in a.option_strings}
|
||||||
|
argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts)
|
||||||
|
args = parser.parse_args(argv)
|
||||||
|
|
||||||
|
args.SourceDir = clean_path(args.SourceDir, "-SourceDir")
|
||||||
|
args.V8Path = clean_path(args.V8Path, "-V8Path")
|
||||||
|
args.TempBasePath = clean_path(args.TempBasePath, "-TempBasePath")
|
||||||
|
|
||||||
type_map = scan_ref_types(args.SourceDir)
|
type_map = scan_ref_types(args.SourceDir)
|
||||||
register_columns = scan_register_columns(args.SourceDir)
|
register_columns = scan_register_columns(args.SourceDir)
|
||||||
@@ -1057,6 +1296,10 @@ def main():
|
|||||||
|
|
||||||
# Stub via ibcmd (one call: create [--import --apply])
|
# Stub via ibcmd (one call: create [--import --apply])
|
||||||
stub_engine = "ibcmd" if os.path.basename(args.V8Path).lower().startswith("ibcmd") else "1cv8"
|
stub_engine = "ibcmd" if os.path.basename(args.V8Path).lower().startswith("ibcmd") else "1cv8"
|
||||||
|
|
||||||
|
# --- Resolve additional arguments for the selected engine ---
|
||||||
|
arg_hints = {"/F": "-TempBasePath", "--db-path": "-TempBasePath"}
|
||||||
|
extra_args = resolve_extra_args(stub_engine, v8_extra, ibcmd_extra, arg_hints)
|
||||||
if stub_engine == "ibcmd":
|
if stub_engine == "ibcmd":
|
||||||
import shutil
|
import shutil
|
||||||
print(f'Creating infobase (ibcmd): {temp_base}')
|
print(f'Creating infobase (ibcmd): {temp_base}')
|
||||||
@@ -1065,6 +1308,7 @@ def main():
|
|||||||
if has_ref_types:
|
if has_ref_types:
|
||||||
ib_args += [f'--import={os.path.join(temp_base, "cfg")}', '--apply', '--force']
|
ib_args += [f'--import={os.path.join(temp_base, "cfg")}', '--apply', '--force']
|
||||||
ib_args.append(f'--data={ib_data}')
|
ib_args.append(f'--data={ib_data}')
|
||||||
|
ib_args.extend(extra_args)
|
||||||
result = run_ibcmd(ib_args, warn_no_user=False)
|
result = run_ibcmd(ib_args, warn_no_user=False)
|
||||||
shutil.rmtree(ib_data, ignore_errors=True)
|
shutil.rmtree(ib_data, ignore_errors=True)
|
||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
@@ -1083,11 +1327,10 @@ def main():
|
|||||||
|
|
||||||
# Create infobase
|
# Create infobase
|
||||||
print(f'Creating infobase: {temp_base}')
|
print(f'Creating infobase: {temp_base}')
|
||||||
result = subprocess.run(
|
result = run_v8(args.V8Path, ['CREATEINFOBASE', f'File="{temp_base}"', '/DisableStartupDialogs']
|
||||||
[args.V8Path, 'CREATEINFOBASE', f'File={temp_base}', '/DisableStartupDialogs'],
|
+ [quote_if_needed(a) for a in extra_args])
|
||||||
capture_output=True, text=True,
|
|
||||||
)
|
|
||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
|
print_platform_output(result)
|
||||||
print(f'Failed to create infobase (code: {result.returncode})', file=sys.stderr)
|
print(f'Failed to create infobase (code: {result.returncode})', file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
@@ -1095,21 +1338,18 @@ def main():
|
|||||||
cfg_dir = os.path.join(temp_base, 'cfg')
|
cfg_dir = os.path.join(temp_base, 'cfg')
|
||||||
# LoadConfigFromFiles
|
# LoadConfigFromFiles
|
||||||
print('Loading configuration from files...')
|
print('Loading configuration from files...')
|
||||||
result = subprocess.run(
|
result = run_v8(args.V8Path, ['DESIGNER', f'/F"{temp_base}"', '/LoadConfigFromFiles', f'"{cfg_dir}"',
|
||||||
[args.V8Path, 'DESIGNER', f'/F{temp_base}', '/LoadConfigFromFiles', cfg_dir, '/DisableStartupDialogs'],
|
'/DisableStartupDialogs'] + [quote_if_needed(a) for a in extra_args])
|
||||||
capture_output=True, text=True,
|
|
||||||
)
|
|
||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
|
print_platform_output(result)
|
||||||
print(f'Failed to load config (code: {result.returncode})', file=sys.stderr)
|
print(f'Failed to load config (code: {result.returncode})', file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# UpdateDBCfg
|
# UpdateDBCfg
|
||||||
print('Updating database configuration...')
|
print('Updating database configuration...')
|
||||||
update_log = os.path.join(tempfile.gettempdir(), 'stub_update_log.txt')
|
update_log = os.path.join(tempfile.gettempdir(), 'stub_update_log.txt')
|
||||||
result = subprocess.run(
|
result = run_v8(args.V8Path, ['DESIGNER', f'/F"{temp_base}"', '/UpdateDBCfg', '/Out', f'"{update_log}"',
|
||||||
[args.V8Path, 'DESIGNER', f'/F{temp_base}', '/UpdateDBCfg', '/Out', update_log, '/DisableStartupDialogs'],
|
'/DisableStartupDialogs'] + [quote_if_needed(a) for a in extra_args])
|
||||||
capture_output=True, text=True,
|
|
||||||
)
|
|
||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
if os.path.isfile(update_log):
|
if os.path.isfile(update_log):
|
||||||
try:
|
try:
|
||||||
@@ -1117,6 +1357,7 @@ def main():
|
|||||||
print(f.read())
|
print(f.read())
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
print_platform_output(result)
|
||||||
print(f'Failed to update DB config (code: {result.returncode})', file=sys.stderr)
|
print(f'Failed to update DB config (code: {result.returncode})', file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|||||||
@@ -55,6 +55,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-dump.ps1" <па
|
|||||||
| `-InputFile <путь>` | да | Путь к EPF/ERF-файлу |
|
| `-InputFile <путь>` | да | Путь к EPF/ERF-файлу |
|
||||||
| `-OutputDir <путь>` | да | Каталог для выгрузки исходников |
|
| `-OutputDir <путь>` | да | Каталог для выгрузки исходников |
|
||||||
| `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` |
|
| `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` |
|
||||||
|
| `-AdditionalV8Arguments <список>` | нет | Доп. аргументы запуска `1cv8.exe` через запятую, напр. `/UseHwLicenses+` |
|
||||||
|
| `-AdditionalIbcmdArguments <список>` | нет | Доп. аргументы `ibcmd` через запятую, в форме `--ключ=значение` |
|
||||||
|
|
||||||
> `*` — обязательно хотя бы одно подключение. Без базы скрипт завершится с ошибкой (dump в пустой базе безвозвратно теряет ссылочные типы)
|
> `*` — обязательно хотя бы одно подключение. Без базы скрипт завершится с ошибкой (dump в пустой базе безвозвратно теряет ссылочные типы)
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# epf-dump v1.8 — Dump external data processor or report (EPF/ERF) to XML sources
|
# epf-dump v1.12 — Dump external data processor or report (EPF/ERF) to XML sources
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||||
<#
|
<#
|
||||||
@@ -36,6 +36,12 @@
|
|||||||
.PARAMETER Format
|
.PARAMETER Format
|
||||||
Формат выгрузки: Hierarchical или Plain (по умолчанию Hierarchical)
|
Формат выгрузки: Hierarchical или Plain (по умолчанию Hierarchical)
|
||||||
|
|
||||||
|
.PARAMETER AdditionalV8Arguments
|
||||||
|
Дополнительные аргументы запуска 1cv8.exe (например /UseHwLicenses+)
|
||||||
|
|
||||||
|
.PARAMETER AdditionalIbcmdArguments
|
||||||
|
Дополнительные аргументы запуска ibcmd (форма --ключ=значение)
|
||||||
|
|
||||||
.EXAMPLE
|
.EXAMPLE
|
||||||
.\epf-dump.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "build\МояОбработка.epf" -OutputDir "src"
|
.\epf-dump.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "build\МояОбработка.epf" -OutputDir "src"
|
||||||
|
|
||||||
@@ -71,12 +77,177 @@ param(
|
|||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
[Parameter(Mandatory=$false)]
|
||||||
[ValidateSet("Hierarchical", "Plain")]
|
[ValidateSet("Hierarchical", "Plain")]
|
||||||
[string]$Format = "Hierarchical"
|
[string]$Format = "Hierarchical",
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[string[]]$AdditionalV8Arguments = @(),
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[string[]]$AdditionalIbcmdArguments = @()
|
||||||
)
|
)
|
||||||
|
|
||||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
|
||||||
|
# --- Additional platform arguments ---
|
||||||
|
$script:V8OwnedKeys = @(
|
||||||
|
'DESIGNER', 'ENTERPRISE', 'CREATEINFOBASE', 'CONFIG',
|
||||||
|
'/F', '/S', '/N', '/P', '/Out', '/DisableStartupDialogs',
|
||||||
|
'/UseTemplate', '/AddToList', '/Execute', '/C', '/URL', '/UC',
|
||||||
|
'/DumpIB', '/RestoreIB', '/DumpCfg', '/LoadCfg',
|
||||||
|
'/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg',
|
||||||
|
'/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles'
|
||||||
|
)
|
||||||
|
$script:IbcmdOwnedKeys = @(
|
||||||
|
'--db-path', '--data', '--out', '--file', '--load', '--restore',
|
||||||
|
'--import', '--export', '--apply', '--force', '--create-database',
|
||||||
|
'--user', '--password'
|
||||||
|
)
|
||||||
|
$script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP')
|
||||||
|
$script:IbcmdSecretKeys = @('--password', '--token', '--db-pwd')
|
||||||
|
|
||||||
|
function Test-ArgKeyMatch {
|
||||||
|
# A token matches a key when it equals the key, or starts with it and the next
|
||||||
|
# character is not a letter — catches glued /N"user" and --password=x, while
|
||||||
|
# keeping /ClearCache distinct from /C.
|
||||||
|
param([string]$Token, [string]$Key)
|
||||||
|
if ($Token.Length -lt $Key.Length) { return $false }
|
||||||
|
if (-not $Token.Substring(0, $Key.Length).Equals($Key, [System.StringComparison]::OrdinalIgnoreCase)) { return $false }
|
||||||
|
if ($Token.Length -eq $Key.Length) { return $true }
|
||||||
|
return -not [char]::IsLetter($Token[$Key.Length])
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-ProjectExtraArgs {
|
||||||
|
# v8args / ibcmdargs from .v8-project.json — same upward walk as v8path.
|
||||||
|
param([string]$Name)
|
||||||
|
$dir = (Get-Location).Path
|
||||||
|
while ($dir) {
|
||||||
|
$pf = Join-Path $dir ".v8-project.json"
|
||||||
|
if (Test-Path $pf) {
|
||||||
|
try {
|
||||||
|
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||||
|
if ($j.$Name) { return @($j.$Name | ForEach-Object { [string]$_ }) }
|
||||||
|
} catch {}
|
||||||
|
return @()
|
||||||
|
}
|
||||||
|
$parent = Split-Path $dir -Parent
|
||||||
|
if (-not $parent -or $parent -eq $dir) { break }
|
||||||
|
$dir = $parent
|
||||||
|
}
|
||||||
|
return @()
|
||||||
|
}
|
||||||
|
|
||||||
|
function Assert-ExtraArgs {
|
||||||
|
# The platform accepts only one batch operation, and a duplicate connection or
|
||||||
|
# output key fails with an opaque 1C error — reject what the skill owns itself.
|
||||||
|
param([string[]]$ExtraArgs, [string]$Engine, [hashtable]$Hints)
|
||||||
|
$paramName = if ($Engine -eq 'ibcmd') { '-AdditionalIbcmdArguments' } else { '-AdditionalV8Arguments' }
|
||||||
|
$owned = if ($Engine -eq 'ibcmd') { $script:IbcmdOwnedKeys } else { $script:V8OwnedKeys }
|
||||||
|
foreach ($tok in $ExtraArgs) {
|
||||||
|
if ($Engine -eq 'ibcmd' -and $tok -notmatch '^-') {
|
||||||
|
Write-Host "Error: '$tok' is a positional token — pass values as --key=value ($paramName cannot extend the ibcmd command)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
foreach ($k in $owned) {
|
||||||
|
if (Test-ArgKeyMatch $tok $k) {
|
||||||
|
$hint = ''
|
||||||
|
if ($Hints -and $Hints.ContainsKey($k)) { $hint = " (use $($Hints[$k]))" }
|
||||||
|
Write-Host "Error: $k is controlled by the skill and cannot be passed via $paramName$hint" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Resolve-ExtraArgs {
|
||||||
|
# Pick the argument list for the selected engine and validate it. An explicitly passed
|
||||||
|
# parameter for the other engine is an error; the same keys coming from .v8-project.json
|
||||||
|
# simply do not apply — a project may describe both engines.
|
||||||
|
param([string]$Engine, [string[]]$V8Extra, [string[]]$IbcmdExtra, [hashtable]$Hints)
|
||||||
|
# powershell.exe -File — how skills are invoked — cannot bind an array parameter:
|
||||||
|
# space-separated values spill into positional ones, a comma-joined list arrives as a
|
||||||
|
# single token. So accept the repo's list convention (comma-separated) and split here;
|
||||||
|
# a native array call keeps working. A value containing a comma is not supported.
|
||||||
|
$V8Extra = @($V8Extra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
|
||||||
|
$IbcmdExtra = @($IbcmdExtra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
|
||||||
|
if ($Engine -eq 'ibcmd' -and $V8Extra.Count -gt 0) {
|
||||||
|
Write-Host "Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd (use -AdditionalIbcmdArguments)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
if ($Engine -ne 'ibcmd' -and $IbcmdExtra.Count -gt 0) {
|
||||||
|
Write-Host "Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 (use -AdditionalV8Arguments)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
if ($Engine -eq 'ibcmd') {
|
||||||
|
$extra = @(Get-ProjectExtraArgs 'ibcmdargs') + @($IbcmdExtra)
|
||||||
|
} else {
|
||||||
|
$extra = @(Get-ProjectExtraArgs 'v8args') + @($V8Extra)
|
||||||
|
}
|
||||||
|
if ($extra.Count -gt 0) { Assert-ExtraArgs $extra $Engine $Hints }
|
||||||
|
# Plain return, no comma trick: the caller re-collects with @(...), and ,@() there
|
||||||
|
# would nest the array — the tokens would then be glued into one argument.
|
||||||
|
return $extra
|
||||||
|
}
|
||||||
|
|
||||||
|
function Format-ArgsForDisplay {
|
||||||
|
# Redact values of secret-prone keys in glued, =-joined and separate forms.
|
||||||
|
# Matching here is a plain prefix (no letter rule): over-masking costs nothing,
|
||||||
|
# a leaked password does.
|
||||||
|
param([string[]]$ArgList, [string]$Engine)
|
||||||
|
$keys = if ($Engine -eq 'ibcmd') { $script:IbcmdSecretKeys } else { $script:V8SecretKeys }
|
||||||
|
$res = @()
|
||||||
|
$maskNext = $false
|
||||||
|
foreach ($tok in $ArgList) {
|
||||||
|
if ($maskNext) { $res += '***'; $maskNext = $false; continue }
|
||||||
|
$hit = $null
|
||||||
|
foreach ($k in $keys) {
|
||||||
|
if ($tok.Length -ge $k.Length -and $tok.Substring(0, $k.Length).Equals($k, [System.StringComparison]::OrdinalIgnoreCase)) { $hit = $k; break }
|
||||||
|
}
|
||||||
|
if (-not $hit) { $res += $tok; continue }
|
||||||
|
if ($tok.Length -eq $hit.Length) { $res += $tok; $maskNext = $true }
|
||||||
|
elseif ($tok[$hit.Length] -eq '=') { $res += ($hit + '=***') }
|
||||||
|
else { $res += ($hit + '***') }
|
||||||
|
}
|
||||||
|
return ,$res
|
||||||
|
}
|
||||||
|
|
||||||
|
function ConvertTo-CleanPath {
|
||||||
|
# Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
|
||||||
|
# surrounding quotes that survived shell parsing, a trailing separator. A quote left
|
||||||
|
# inside afterwards cannot be part of a real path — reject it by name instead of letting
|
||||||
|
# 1C answer with its opaque "Неверные или отсутствующие параметры соединения".
|
||||||
|
param([string]$Value, [string]$ParamName)
|
||||||
|
if (-not $Value) { return $Value }
|
||||||
|
$v = $Value.Trim()
|
||||||
|
if ($v.Length -ge 2 -and $v[0] -eq $v[-1] -and ($v[0] -eq '"' -or $v[0] -eq "'")) {
|
||||||
|
$v = $v.Substring(1, $v.Length - 2).Trim()
|
||||||
|
}
|
||||||
|
if ($v.Length -gt 3 -and ($v[-1] -eq '\' -or $v[-1] -eq '/')) { $v = $v.Substring(0, $v.Length - 1) }
|
||||||
|
if ($v.Contains('"')) {
|
||||||
|
Write-Host "Error: $ParamName contains a quote character: $Value" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
return $v
|
||||||
|
}
|
||||||
|
|
||||||
|
$V8Path = ConvertTo-CleanPath $V8Path '-V8Path'
|
||||||
|
$InfoBasePath = ConvertTo-CleanPath $InfoBasePath '-InfoBasePath'
|
||||||
|
$InputFile = ConvertTo-CleanPath $InputFile '-InputFile'
|
||||||
|
$OutputDir = ConvertTo-CleanPath $OutputDir '-OutputDir'
|
||||||
|
|
||||||
|
function Assert-InfoBaseExists {
|
||||||
|
# These skills work on a ready infobase. Saying so up front beats the platform's
|
||||||
|
# "Неверные или отсутствующие параметры соединения" after a launch.
|
||||||
|
param([string]$Path)
|
||||||
|
if (-not $Path) { return }
|
||||||
|
if (-not (Test-Path (Join-Path $Path "1Cv8.1CD"))) {
|
||||||
|
Write-Host "Error: information base not found at $Path (no 1Cv8.1CD)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert-InfoBaseExists $InfoBasePath
|
||||||
|
|
||||||
# --- Resolve V8Path ---
|
# --- Resolve V8Path ---
|
||||||
function Find-ProjectV8Path {
|
function Find-ProjectV8Path {
|
||||||
$dir = (Get-Location).Path
|
$dir = (Get-Location).Path
|
||||||
@@ -128,32 +299,75 @@ if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
|
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
|
||||||
function Invoke-IbcmdProcess {
|
function ConvertFrom-PlatformBytes {
|
||||||
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
|
# ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
|
||||||
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
|
# OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes — guessing
|
||||||
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
|
# one of them outright mangles Cyrillic.
|
||||||
param([string]$Exe, [string[]]$IbArgs)
|
param([byte[]]$Bytes)
|
||||||
|
if (-not $Bytes -or $Bytes.Length -eq 0) { return '' }
|
||||||
|
try {
|
||||||
|
$strict = New-Object System.Text.UTF8Encoding($false, $true)
|
||||||
|
return $strict.GetString($Bytes)
|
||||||
|
} catch {
|
||||||
|
return [System.Text.Encoding]::GetEncoding(866).GetString($Bytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-PlatformProcess {
|
||||||
|
# Run the platform non-interactively and capture its console output. A closed stdin pipe
|
||||||
|
# (EOF) makes an auth prompt fast-fail instead of hanging; capturing keeps the child's
|
||||||
|
# text out of our stream until we print it labelled (and out of the wrong encoding).
|
||||||
|
# Returns @{ Output; ExitCode }.
|
||||||
|
#
|
||||||
|
# Quoting differs by engine, so the caller says which it built:
|
||||||
|
# ibcmd — tokens are bare (--db-path=C:\a b), the whole token gets quoted here;
|
||||||
|
# 1cv8 — -PreQuoted: the caller already put quotes inside the token (File="C:\a b"),
|
||||||
|
# which is where 1C's own parser expects them; quoting again breaks the value.
|
||||||
|
param([string]$Exe, [string[]]$ProcArgs, [switch]$PreQuoted)
|
||||||
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
||||||
$psi.FileName = $Exe
|
$psi.FileName = $Exe
|
||||||
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
|
$psi.Arguments = if ($PreQuoted) {
|
||||||
|
$ProcArgs -join ' '
|
||||||
|
} else {
|
||||||
|
($ProcArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
|
||||||
|
}
|
||||||
$psi.UseShellExecute = $false
|
$psi.UseShellExecute = $false
|
||||||
$psi.CreateNoWindow = $true
|
$psi.CreateNoWindow = $true
|
||||||
$psi.RedirectStandardInput = $true
|
$psi.RedirectStandardInput = $true
|
||||||
$psi.RedirectStandardOutput = $true
|
$psi.RedirectStandardOutput = $true
|
||||||
$psi.RedirectStandardError = $true
|
$psi.RedirectStandardError = $true
|
||||||
try {
|
|
||||||
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
|
|
||||||
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
|
|
||||||
} catch {}
|
|
||||||
$p = [System.Diagnostics.Process]::Start($psi)
|
$p = [System.Diagnostics.Process]::Start($psi)
|
||||||
$p.StandardInput.Close()
|
$p.StandardInput.Close()
|
||||||
$out = $p.StandardOutput.ReadToEnd()
|
# stderr is drained in parallel: reading the streams one after another deadlocks
|
||||||
$err = $p.StandardError.ReadToEnd()
|
# as soon as the other one fills its pipe buffer.
|
||||||
|
$errMs = New-Object System.IO.MemoryStream
|
||||||
|
$errTask = $p.StandardError.BaseStream.CopyToAsync($errMs)
|
||||||
|
$outMs = New-Object System.IO.MemoryStream
|
||||||
|
$p.StandardOutput.BaseStream.CopyTo($outMs)
|
||||||
|
$errTask.Wait()
|
||||||
$p.WaitForExit()
|
$p.WaitForExit()
|
||||||
|
$out = ConvertFrom-PlatformBytes $outMs.ToArray()
|
||||||
|
$err = ConvertFrom-PlatformBytes $errMs.ToArray()
|
||||||
if ($err) { $out += $err }
|
if ($err) { $out += $err }
|
||||||
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
|
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Write-PlatformOutput {
|
||||||
|
# Print what the platform wrote to the console as its own labelled block. Silence stays
|
||||||
|
# silent: in batch mode 1cv8 reports through /Out and prints nothing here.
|
||||||
|
param([string]$Text)
|
||||||
|
if (-not $Text) { return }
|
||||||
|
$t = $Text.TrimEnd()
|
||||||
|
if (-not $t) { return }
|
||||||
|
$limit = 65536
|
||||||
|
if ($t.Length -gt $limit) {
|
||||||
|
$t = "[... обрезано, показаны последние $limit символов ...]`r`n" + $t.Substring($t.Length - $limit)
|
||||||
|
}
|
||||||
|
Write-Host "--- Вывод платформы ---"
|
||||||
|
Write-Host $t
|
||||||
|
Write-Host "--- End ---"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function Test-DirNonEmpty {
|
function Test-DirNonEmpty {
|
||||||
# Postcondition: the platform must have written files into the output directory.
|
# Postcondition: the platform must have written files into the output directory.
|
||||||
@@ -170,6 +384,10 @@ function Protect-Secrets {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
||||||
|
|
||||||
|
# --- Resolve additional arguments for the selected engine ---
|
||||||
|
$argHints = @{ '/F' = '-InfoBasePath'; '/S' = '-InfoBaseServer + -InfoBaseRef'; '/N' = '-UserName'; '/P' = '-Password'; '--db-path' = '-InfoBasePath'; '--user' = '-UserName'; '--password' = '-Password' }
|
||||||
|
$extraArgs = @(Resolve-ExtraArgs $engine $AdditionalV8Arguments $AdditionalIbcmdArguments $argHints)
|
||||||
if ($engine -eq "ibcmd") {
|
if ($engine -eq "ibcmd") {
|
||||||
if (-not $InfoBasePath) {
|
if (-not $InfoBasePath) {
|
||||||
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
|
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
|
||||||
@@ -203,8 +421,9 @@ try {
|
|||||||
if ($UserName) { $arguments += "--user=$UserName" }
|
if ($UserName) { $arguments += "--user=$UserName" }
|
||||||
if ($Password) { $arguments += "--password=$Password" }
|
if ($Password) { $arguments += "--password=$Password" }
|
||||||
$arguments += "--data=$tempDir"
|
$arguments += "--data=$tempDir"
|
||||||
Write-Host "Running: ibcmd $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))"
|
$arguments += $extraArgs
|
||||||
$__ib = Invoke-IbcmdProcess $V8Path $arguments
|
Write-Host "Running: ibcmd $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
|
||||||
|
$__ib = Invoke-PlatformProcess $V8Path $arguments
|
||||||
$output = $__ib.Output
|
$output = $__ib.Output
|
||||||
$exitCode = $__ib.ExitCode
|
$exitCode = $__ib.ExitCode
|
||||||
$outMissing = ($exitCode -eq 0) -and -not (Test-DirNonEmpty $OutputDir)
|
$outMissing = ($exitCode -eq 0) -and -not (Test-DirNonEmpty $OutputDir)
|
||||||
@@ -216,7 +435,7 @@ try {
|
|||||||
} else {
|
} else {
|
||||||
Write-Host "Error dumping external data processor/report (code: $exitCode)" -ForegroundColor Red
|
Write-Host "Error dumping external data processor/report (code: $exitCode)" -ForegroundColor Red
|
||||||
}
|
}
|
||||||
if ($output) { Write-Host ($output | Out-String) }
|
Write-PlatformOutput $output
|
||||||
exit $exitCode
|
exit $exitCode
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -240,11 +459,12 @@ try {
|
|||||||
$outFile = Join-Path $tempDir "dump_log.txt"
|
$outFile = Join-Path $tempDir "dump_log.txt"
|
||||||
$arguments += "/Out", "`"$outFile`""
|
$arguments += "/Out", "`"$outFile`""
|
||||||
$arguments += "/DisableStartupDialogs"
|
$arguments += "/DisableStartupDialogs"
|
||||||
|
$arguments += $extraArgs
|
||||||
|
|
||||||
# --- Execute ---
|
# --- Execute ---
|
||||||
Write-Host "Running: 1cv8.exe $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))"
|
Write-Host "Running: 1cv8.exe $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
|
||||||
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
|
$__v8 = Invoke-PlatformProcess $V8Path $arguments -PreQuoted
|
||||||
$exitCode = $process.ExitCode
|
$exitCode = $__v8.ExitCode
|
||||||
|
|
||||||
# --- Result ---
|
# --- Result ---
|
||||||
# Postcondition: exit 0 with an empty output directory is a false success.
|
# Postcondition: exit 0 with an empty output directory is a false success.
|
||||||
@@ -266,6 +486,7 @@ try {
|
|||||||
Write-Host "--- End ---"
|
Write-Host "--- End ---"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Write-PlatformOutput $__v8.Output
|
||||||
|
|
||||||
exit $exitCode
|
exit $exitCode
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# epf-dump v1.8 — Dump external data processor or report (EPF/ERF) to XML sources
|
# epf-dump v1.12 — Dump external data processor or report (EPF/ERF) to XML sources
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -14,6 +14,28 @@ import subprocess
|
|||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
|
# Регистронезависимый ввод — паритет с 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)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def _find_project_v8path():
|
def _find_project_v8path():
|
||||||
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
||||||
@@ -36,6 +58,163 @@ def _find_project_v8path():
|
|||||||
d = parent
|
d = parent
|
||||||
|
|
||||||
|
|
||||||
|
# --- Additional platform arguments ---
|
||||||
|
V8_OWNED_KEYS = [
|
||||||
|
"DESIGNER", "ENTERPRISE", "CREATEINFOBASE", "CONFIG",
|
||||||
|
"/F", "/S", "/N", "/P", "/Out", "/DisableStartupDialogs",
|
||||||
|
"/UseTemplate", "/AddToList", "/Execute", "/C", "/URL", "/UC",
|
||||||
|
"/DumpIB", "/RestoreIB", "/DumpCfg", "/LoadCfg",
|
||||||
|
"/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg",
|
||||||
|
"/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles",
|
||||||
|
]
|
||||||
|
IBCMD_OWNED_KEYS = [
|
||||||
|
"--db-path", "--data", "--out", "--file", "--load", "--restore",
|
||||||
|
"--import", "--export", "--apply", "--force", "--create-database",
|
||||||
|
"--user", "--password",
|
||||||
|
]
|
||||||
|
V8_SECRET_KEYS = ["/P", "/UC", "/WSP", "/AWSP"]
|
||||||
|
IBCMD_SECRET_KEYS = ["--password", "--token", "--db-pwd"]
|
||||||
|
|
||||||
|
|
||||||
|
def arg_key_match(token, key):
|
||||||
|
"""Token matches a key when it equals it, or starts with it and the next character
|
||||||
|
is not a letter — catches glued /N"user" and --password=x, while keeping
|
||||||
|
/ClearCache distinct from /C."""
|
||||||
|
if len(token) < len(key):
|
||||||
|
return False
|
||||||
|
if token[: len(key)].lower() != key.lower():
|
||||||
|
return False
|
||||||
|
if len(token) == len(key):
|
||||||
|
return True
|
||||||
|
return not token[len(key)].isalpha()
|
||||||
|
|
||||||
|
|
||||||
|
def project_extra_args(name):
|
||||||
|
"""v8args / ibcmdargs from .v8-project.json — same upward walk as v8path."""
|
||||||
|
d = os.getcwd()
|
||||||
|
while True:
|
||||||
|
pf = os.path.join(d, ".v8-project.json")
|
||||||
|
if os.path.isfile(pf):
|
||||||
|
try:
|
||||||
|
with open(pf, encoding="utf-8-sig") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
v = data.get(name)
|
||||||
|
if v:
|
||||||
|
return [str(x) for x in v]
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return []
|
||||||
|
parent = os.path.dirname(d)
|
||||||
|
if parent == d:
|
||||||
|
return []
|
||||||
|
d = parent
|
||||||
|
|
||||||
|
|
||||||
|
def assert_extra_args(extra, engine, hints):
|
||||||
|
"""The platform accepts only one batch operation, and a duplicate connection or
|
||||||
|
output key fails with an opaque 1C error — reject what the skill owns itself."""
|
||||||
|
param = "-AdditionalIbcmdArguments" if engine == "ibcmd" else "-AdditionalV8Arguments"
|
||||||
|
owned = IBCMD_OWNED_KEYS if engine == "ibcmd" else V8_OWNED_KEYS
|
||||||
|
for tok in extra:
|
||||||
|
if engine == "ibcmd" and not tok.startswith("-"):
|
||||||
|
print(
|
||||||
|
f"Error: '{tok}' is a positional token — pass values as --key=value "
|
||||||
|
f"({param} cannot extend the ibcmd command)",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
for k in owned:
|
||||||
|
if arg_key_match(tok, k):
|
||||||
|
hint = f" (use {hints[k]})" if hints and k in hints else ""
|
||||||
|
print(
|
||||||
|
f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def format_args_for_display(arglist, engine):
|
||||||
|
"""Redact values of secret-prone keys in glued, =-joined and separate forms.
|
||||||
|
Matching here is a plain prefix (no letter rule): over-masking costs nothing,
|
||||||
|
a leaked password does."""
|
||||||
|
keys = IBCMD_SECRET_KEYS if engine == "ibcmd" else V8_SECRET_KEYS
|
||||||
|
res = []
|
||||||
|
mask_next = False
|
||||||
|
for tok in arglist:
|
||||||
|
if mask_next:
|
||||||
|
res.append("***")
|
||||||
|
mask_next = False
|
||||||
|
continue
|
||||||
|
hit = None
|
||||||
|
for k in keys:
|
||||||
|
if tok[: len(k)].lower() == k.lower():
|
||||||
|
hit = k
|
||||||
|
break
|
||||||
|
if hit is None:
|
||||||
|
res.append(tok)
|
||||||
|
elif len(tok) == len(hit):
|
||||||
|
res.append(tok)
|
||||||
|
mask_next = True
|
||||||
|
elif tok[len(hit)] == "=":
|
||||||
|
res.append(hit + "=***")
|
||||||
|
else:
|
||||||
|
res.append(hit + "***")
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
def extract_extra_args(argv, known_opts):
|
||||||
|
"""argparse refuses values that start with '-' (every ibcmd key does), so pull the two
|
||||||
|
escape-hatch lists out of argv by hand: after the flag, take everything up to the next
|
||||||
|
declared skill option. Returns (remaining_argv, v8_extra, ibcmd_extra)."""
|
||||||
|
rest, v8, ibcmd = [], [], []
|
||||||
|
i = 0
|
||||||
|
while i < len(argv):
|
||||||
|
low = argv[i].lower()
|
||||||
|
if low in ("-additionalv8arguments", "-additionalibcmdarguments"):
|
||||||
|
target = v8 if low == "-additionalv8arguments" else ibcmd
|
||||||
|
i += 1
|
||||||
|
while i < len(argv) and argv[i].lower() not in known_opts:
|
||||||
|
target.append(argv[i])
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
rest.append(argv[i])
|
||||||
|
i += 1
|
||||||
|
return rest, v8, ibcmd
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints):
|
||||||
|
"""Pick the argument list for the selected engine and validate it. An explicitly
|
||||||
|
passed parameter for the other engine is an error; the same keys coming from
|
||||||
|
.v8-project.json simply do not apply — a project may describe both engines.
|
||||||
|
|
||||||
|
Comma-separated elements are split apart: PowerShell's -File cannot bind an array,
|
||||||
|
so that form is the documented one and both ports must accept it. A value containing
|
||||||
|
a comma is not supported."""
|
||||||
|
v8_extra = [p for tok in v8_extra for p in str(tok).split(",") if p]
|
||||||
|
ibcmd_extra = [p for tok in ibcmd_extra for p in str(tok).split(",") if p]
|
||||||
|
if engine == "ibcmd" and v8_extra:
|
||||||
|
print(
|
||||||
|
"Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd "
|
||||||
|
"(use -AdditionalIbcmdArguments)",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
if engine != "ibcmd" and ibcmd_extra:
|
||||||
|
print(
|
||||||
|
"Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 "
|
||||||
|
"(use -AdditionalV8Arguments)",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
if engine == "ibcmd":
|
||||||
|
extra = project_extra_args("ibcmdargs") + list(ibcmd_extra)
|
||||||
|
else:
|
||||||
|
extra = project_extra_args("v8args") + list(v8_extra)
|
||||||
|
if extra:
|
||||||
|
assert_extra_args(extra, engine, hints)
|
||||||
|
return extra
|
||||||
|
|
||||||
|
|
||||||
def _version_dir(p):
|
def _version_dir(p):
|
||||||
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
|
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
|
||||||
parent = os.path.dirname(p)
|
parent = os.path.dirname(p)
|
||||||
@@ -86,6 +265,85 @@ IBCMD_NOUSER_HINT = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def decode_platform_bytes(data):
|
||||||
|
"""ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
|
||||||
|
OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes — the locale
|
||||||
|
code page (what text=True uses) mangles both."""
|
||||||
|
if not data:
|
||||||
|
return ""
|
||||||
|
try:
|
||||||
|
return data.decode("utf-8")
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
return data.decode("cp866", errors="replace")
|
||||||
|
|
||||||
|
|
||||||
|
def assert_infobase_exists(path):
|
||||||
|
"""These skills work on a ready infobase. Saying so up front beats the platform's
|
||||||
|
"Неверные или отсутствующие параметры соединения" after a launch."""
|
||||||
|
if not path:
|
||||||
|
return
|
||||||
|
if not os.path.isfile(os.path.join(path, "1Cv8.1CD")):
|
||||||
|
print(f"Error: information base not found at {path} (no 1Cv8.1CD)", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def clean_path(value, param=""):
|
||||||
|
"""Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
|
||||||
|
surrounding quotes that survived shell parsing, a trailing separator. A quote left
|
||||||
|
inside afterwards cannot be part of a real path — reject it by name instead of letting
|
||||||
|
1C answer with its opaque "Неверные или отсутствующие параметры соединения"."""
|
||||||
|
if not value:
|
||||||
|
return value
|
||||||
|
v = value.strip()
|
||||||
|
if len(v) >= 2 and v[0] == v[-1] and v[0] in "\"'":
|
||||||
|
v = v[1:-1].strip()
|
||||||
|
if len(v) > 3 and v[-1] in "\\/":
|
||||||
|
v = v[:-1]
|
||||||
|
if '"' in v:
|
||||||
|
print(f"Error: {param or 'path'} contains a quote character: {value}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
return v
|
||||||
|
|
||||||
|
|
||||||
|
def quote_if_needed(token):
|
||||||
|
"""Extra arguments come from the caller unquoted; the 1cv8 command line is joined
|
||||||
|
verbatim, so a token with a space needs quotes of its own."""
|
||||||
|
if token and (" " in token or "\t" in token) and '"' not in token:
|
||||||
|
return f'"{token}"'
|
||||||
|
return token
|
||||||
|
|
||||||
|
|
||||||
|
def run_v8(v8path, arguments):
|
||||||
|
"""Run 1cv8 in batch mode and capture its console output.
|
||||||
|
|
||||||
|
The arguments carry their own quotes inside the value (File="C:\\a b") — that is where
|
||||||
|
1C's parser expects them, on Windows and on *nix alike. Windows list2cmdline would
|
||||||
|
escape those quotes, so there the command line is handed over ready-made.
|
||||||
|
"""
|
||||||
|
if os.name == "nt":
|
||||||
|
cmd = '"' + v8path + '" ' + " ".join(arguments)
|
||||||
|
else:
|
||||||
|
cmd = [v8path] + arguments
|
||||||
|
r = subprocess.run(cmd, input=b"", capture_output=True)
|
||||||
|
r.stdout = decode_platform_bytes(r.stdout)
|
||||||
|
r.stderr = decode_platform_bytes(r.stderr)
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
|
def print_platform_output(result):
|
||||||
|
"""Print what the platform wrote to the console as its own labelled block. Silence stays
|
||||||
|
silent: in batch mode 1cv8 reports through /Out and prints nothing here."""
|
||||||
|
text = ((result.stdout or "") + (result.stderr or "")).rstrip()
|
||||||
|
if not text:
|
||||||
|
return
|
||||||
|
limit = 65536
|
||||||
|
if len(text) > limit:
|
||||||
|
text = f"[... обрезано, показаны последние {limit} символов ...]\n" + text[-limit:]
|
||||||
|
print("--- Вывод платформы ---")
|
||||||
|
print(text)
|
||||||
|
print("--- End ---")
|
||||||
|
|
||||||
|
|
||||||
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
||||||
"""Run an ibcmd command non-interactively.
|
"""Run an ibcmd command non-interactively.
|
||||||
|
|
||||||
@@ -96,7 +354,10 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
|||||||
if warn_no_user and os.name == "nt" and not has_username:
|
if warn_no_user and os.name == "nt" and not has_username:
|
||||||
sys.stderr.write(IBCMD_NOUSER_HINT)
|
sys.stderr.write(IBCMD_NOUSER_HINT)
|
||||||
sys.stderr.flush()
|
sys.stderr.flush()
|
||||||
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
|
r = subprocess.run(cmd, input=b"", capture_output=True)
|
||||||
|
r.stdout = decode_platform_bytes(r.stdout)
|
||||||
|
r.stderr = decode_platform_bytes(r.stderr)
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
def dir_nonempty(path):
|
def dir_nonempty(path):
|
||||||
@@ -135,12 +396,36 @@ def main():
|
|||||||
choices=["Hierarchical", "Plain"],
|
choices=["Hierarchical", "Plain"],
|
||||||
help="Dump format (default: Hierarchical)",
|
help="Dump format (default: Hierarchical)",
|
||||||
)
|
)
|
||||||
args = parser.parse_args()
|
parser.add_argument("-AdditionalV8Arguments", nargs="*", default=[],
|
||||||
|
help="Extra 1cv8 arguments, e.g. /UseHwLicenses+")
|
||||||
|
parser.add_argument("-AdditionalIbcmdArguments", nargs="*", default=[],
|
||||||
|
help="Extra ibcmd arguments in --key=value form")
|
||||||
|
known_opts = {s.lower() for a in parser._actions for s in a.option_strings}
|
||||||
|
argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts)
|
||||||
|
args = ci_parse_args(parser, argv)
|
||||||
|
|
||||||
|
args.V8Path = clean_path(args.V8Path, "-V8Path")
|
||||||
|
args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath")
|
||||||
|
assert_infobase_exists(args.InfoBasePath)
|
||||||
|
args.InputFile = clean_path(args.InputFile, "-InputFile")
|
||||||
|
args.OutputDir = clean_path(args.OutputDir, "-OutputDir")
|
||||||
|
|
||||||
# --- Resolve V8Path ---
|
# --- Resolve V8Path ---
|
||||||
v8path = resolve_v8path(args.V8Path)
|
v8path = resolve_v8path(args.V8Path)
|
||||||
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
|
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
|
||||||
|
|
||||||
|
# --- Resolve additional arguments for the selected engine ---
|
||||||
|
arg_hints = {
|
||||||
|
"/F": "-InfoBasePath",
|
||||||
|
"/S": "-InfoBaseServer + -InfoBaseRef",
|
||||||
|
"/N": "-UserName",
|
||||||
|
"/P": "-Password",
|
||||||
|
"--db-path": "-InfoBasePath",
|
||||||
|
"--user": "-UserName",
|
||||||
|
"--password": "-Password",
|
||||||
|
}
|
||||||
|
extra_args = resolve_extra_args(engine, v8_extra, ibcmd_extra, arg_hints)
|
||||||
|
|
||||||
# --- Validate database connection ---
|
# --- Validate database connection ---
|
||||||
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
||||||
print("Error: database connection required. Specify -InfoBasePath or -InfoBaseServer/-InfoBaseRef", file=sys.stderr)
|
print("Error: database connection required. Specify -InfoBasePath or -InfoBaseServer/-InfoBaseRef", file=sys.stderr)
|
||||||
@@ -178,7 +463,8 @@ def main():
|
|||||||
if args.Password:
|
if args.Password:
|
||||||
arguments.append(f"--password={args.Password}")
|
arguments.append(f"--password={args.Password}")
|
||||||
arguments.append(f"--data={ib_data}")
|
arguments.append(f"--data={ib_data}")
|
||||||
print(f"Running: ibcmd {_redact(' '.join(arguments), args.Password, args.UserName)}")
|
arguments.extend(extra_args)
|
||||||
|
print(f"Running: ibcmd {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
|
||||||
result = run_ibcmd([v8path] + arguments, warn_no_user=False)
|
result = run_ibcmd([v8path] + arguments, warn_no_user=False)
|
||||||
exit_code = result.returncode
|
exit_code = result.returncode
|
||||||
out_missing = exit_code == 0 and not dir_nonempty(args.OutputDir)
|
out_missing = exit_code == 0 and not dir_nonempty(args.OutputDir)
|
||||||
@@ -190,40 +476,33 @@ def main():
|
|||||||
print(f"Error: exit code 0 but no files under {args.OutputDir} — dump produced no output", file=sys.stderr)
|
print(f"Error: exit code 0 but no files under {args.OutputDir} — dump produced no output", file=sys.stderr)
|
||||||
else:
|
else:
|
||||||
print(f"Error dumping external data processor/report (code: {exit_code})", file=sys.stderr)
|
print(f"Error dumping external data processor/report (code: {exit_code})", file=sys.stderr)
|
||||||
if result.stdout:
|
|
||||||
print(result.stdout)
|
|
||||||
if result.stderr:
|
|
||||||
print(result.stderr, file=sys.stderr)
|
|
||||||
sys.exit(exit_code)
|
sys.exit(exit_code)
|
||||||
|
|
||||||
# --- Build arguments ---
|
# --- Build arguments ---
|
||||||
arguments = ["DESIGNER"]
|
arguments = ["DESIGNER"]
|
||||||
|
|
||||||
if args.InfoBaseServer and args.InfoBaseRef:
|
if args.InfoBaseServer and args.InfoBaseRef:
|
||||||
arguments += ["/S", f"{args.InfoBaseServer}/{args.InfoBaseRef}"]
|
arguments += ["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"']
|
||||||
else:
|
else:
|
||||||
arguments += ["/F", args.InfoBasePath]
|
arguments += ["/F", f'"{args.InfoBasePath}"']
|
||||||
|
|
||||||
if args.UserName:
|
if args.UserName:
|
||||||
arguments.append(f"/N{args.UserName}")
|
arguments.append(f'/N"{args.UserName}"')
|
||||||
if args.Password:
|
if args.Password:
|
||||||
arguments.append(f"/P{args.Password}")
|
arguments.append(f'/P"{args.Password}"')
|
||||||
|
|
||||||
arguments += ["/DumpExternalDataProcessorOrReportToFiles", args.OutputDir, args.InputFile]
|
arguments += ["/DumpExternalDataProcessorOrReportToFiles", f'"{args.OutputDir}"', f'"{args.InputFile}"']
|
||||||
arguments += ["-Format", args.Format]
|
arguments += ["-Format", args.Format]
|
||||||
|
|
||||||
# --- Output ---
|
# --- Output ---
|
||||||
out_file = os.path.join(temp_dir, "dump_log.txt")
|
out_file = os.path.join(temp_dir, "dump_log.txt")
|
||||||
arguments += ["/Out", out_file]
|
arguments += ["/Out", f'"{out_file}"']
|
||||||
arguments.append("/DisableStartupDialogs")
|
arguments.append("/DisableStartupDialogs")
|
||||||
|
arguments.extend(quote_if_needed(a) for a in extra_args)
|
||||||
|
|
||||||
# --- Execute ---
|
# --- Execute ---
|
||||||
print(f"Running: 1cv8.exe {_redact(' '.join(arguments), args.Password, args.UserName)}")
|
print(f"Running: 1cv8.exe {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
|
||||||
result = subprocess.run(
|
result = run_v8(v8path, arguments)
|
||||||
[v8path] + arguments,
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
)
|
|
||||||
exit_code = result.returncode
|
exit_code = result.returncode
|
||||||
|
|
||||||
# --- Result ---
|
# --- Result ---
|
||||||
@@ -249,6 +528,7 @@ def main():
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
print_platform_output(result)
|
||||||
sys.exit(exit_code)
|
sys.exit(exit_code)
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
|
|||||||
@@ -18,19 +18,20 @@ allowed-tools:
|
|||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
```
|
```
|
||||||
/epf-init <Name> [Synonym] [SrcDir]
|
/epf-init <Name> [Synonym] [SrcDir] [FormatVersion]
|
||||||
```
|
```
|
||||||
|
|
||||||
| Параметр | Обязательный | По умолчанию | Описание |
|
| Параметр | Обязательный | По умолчанию | Описание |
|
||||||
|-----------|:------------:|--------------|-------------------------------------|
|
|---------------|:------------:|--------------|------------------------------------------------|
|
||||||
| Name | да | — | Имя обработки (латиница/кириллица) |
|
| Name | да | — | Имя обработки (латиница/кириллица) |
|
||||||
| Synonym | нет | = Name | Синоним (отображаемое имя) |
|
| Synonym | нет | = Name | Синоним (отображаемое имя) |
|
||||||
| SrcDir | нет | `src` | Каталог исходников относительно CWD |
|
| SrcDir | нет | `src` | Каталог исходников относительно CWD |
|
||||||
|
| FormatVersion | нет | `2.17` | Версия формата: 2.20 — платформа 8.3.27, 2.21 — 8.5. Дефолт открывается любой платформой |
|
||||||
|
|
||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/init.ps1" -Name "<Name>" [-Synonym "<Synonym>"] [-SrcDir "<SrcDir>"]
|
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/init.ps1" -Name "<Name>" [-Synonym "<Synonym>"] [-SrcDir "<SrcDir>"] [-FormatVersion "<2.17|2.18|2.19|2.20|2.21>"]
|
||||||
```
|
```
|
||||||
|
|
||||||
## Дальнейшие шаги
|
## Дальнейшие шаги
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# epf-init v1.1 — Init 1C external data processor scaffold
|
# epf-init v1.7 — Init 1C external data processor scaffold (+write_xml_file/write_utf8_bom: общий эталон записи)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory)]
|
||||||
@@ -6,10 +6,22 @@ param(
|
|||||||
|
|
||||||
[string]$Synonym = $Name,
|
[string]$Synonym = $Name,
|
||||||
|
|
||||||
[string]$SrcDir = "src"
|
[string]$SrcDir = "src",
|
||||||
|
|
||||||
|
# Версия формата выгрузки. Своей конфигурации у автономной обработки нет, наследовать
|
||||||
|
# версию неоткуда — поэтому её задают явно. Формы, макеты и справку внутри обработки
|
||||||
|
# навыки берут уже отсюда: их детектор читает version из корня этого файла.
|
||||||
|
[ValidateSet("2.17", "2.18", "2.19", "2.20", "2.21")]
|
||||||
|
[string]$FormatVersion = "2.17"
|
||||||
)
|
)
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
function Esc-XmlText {
|
||||||
|
param([string]$s)
|
||||||
|
# Эскейп ТЕКСТА элемента: только & < > — кавычку и апостроф платформа держит сырыми.
|
||||||
|
return $s.Replace('&','&').Replace('<','<').Replace('>','>')
|
||||||
|
}
|
||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
[Console]::InputEncoding = [System.Text.Encoding]::UTF8
|
[Console]::InputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
|
||||||
@@ -18,9 +30,17 @@ $uuid2 = [guid]::NewGuid().ToString()
|
|||||||
$uuid3 = [guid]::NewGuid().ToString()
|
$uuid3 = [guid]::NewGuid().ToString()
|
||||||
$uuid4 = [guid]::NewGuid().ToString()
|
$uuid4 = [guid]::NewGuid().ToString()
|
||||||
|
|
||||||
|
# Объявления пространств имён — одной переменной: места эмиссии её только интерполируют.
|
||||||
|
$xmlnsDecl = 'xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
|
||||||
|
# 2.21 (8.5) добавила в шапку пространство палитры. Вставляем НА МЕСТО (после lf, перед style):
|
||||||
|
# платформа держит объявления по алфавиту, дописать в конец нельзя.
|
||||||
|
if (($FormatVersion -match '^(\d+)\.(\d+)$') -and ([int]$Matches[1] * 100 + [int]$Matches[2]) -ge 221) {
|
||||||
|
$xmlnsDecl = $xmlnsDecl -replace ' xmlns:style=', ' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette" xmlns:style='
|
||||||
|
}
|
||||||
|
|
||||||
$xml = @"
|
$xml = @"
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
|
<MetaDataObject $xmlnsDecl version="$FormatVersion">
|
||||||
<ExternalDataProcessor uuid="$uuid1">
|
<ExternalDataProcessor uuid="$uuid1">
|
||||||
<InternalInfo>
|
<InternalInfo>
|
||||||
<xr:ContainedObject>
|
<xr:ContainedObject>
|
||||||
@@ -33,11 +53,11 @@ $xml = @"
|
|||||||
</xr:GeneratedType>
|
</xr:GeneratedType>
|
||||||
</InternalInfo>
|
</InternalInfo>
|
||||||
<Properties>
|
<Properties>
|
||||||
<Name>$Name</Name>
|
<Name>$(Esc-XmlText $Name)</Name>
|
||||||
<Synonym>
|
<Synonym>
|
||||||
<v8:item>
|
<v8:item>
|
||||||
<v8:lang>ru</v8:lang>
|
<v8:lang>ru</v8:lang>
|
||||||
<v8:content>$Synonym</v8:content>
|
<v8:content>$(Esc-XmlText $Synonym)</v8:content>
|
||||||
</v8:item>
|
</v8:item>
|
||||||
</Synonym>
|
</Synonym>
|
||||||
<Comment/>
|
<Comment/>
|
||||||
@@ -64,7 +84,16 @@ $extDir = Join-Path $processorDir "Ext"
|
|||||||
New-Item -ItemType Directory -Path $extDir -Force | Out-Null
|
New-Item -ItemType Directory -Path $extDir -Force | Out-Null
|
||||||
|
|
||||||
$enc = New-Object System.Text.UTF8Encoding($true)
|
$enc = New-Object System.Text.UTF8Encoding($true)
|
||||||
[System.IO.File]::WriteAllText((Resolve-Path $SrcDir | Join-Path -ChildPath "$Name.xml"), $xml, $enc)
|
# XML в каноне выгрузки Конфигуратора: CRLF в разделителях, без перевода в конце.
|
||||||
|
#
|
||||||
|
# Копия этой функции есть в каждом навыке-эмиттере (навыки автономны). Держать
|
||||||
|
# копии одинаковыми — сознательно: разошедшиеся копии сводят на нет весь смысл.
|
||||||
|
function Write-XmlFile([string]$path, [string]$text, $encoding) {
|
||||||
|
$t = ($text -replace "`r`n", "`n") -replace "`n", "`r`n"
|
||||||
|
[System.IO.File]::WriteAllText($path, $t.TrimEnd("`r", "`n"), $encoding)
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-XmlFile (Resolve-Path $SrcDir | Join-Path -ChildPath "$Name.xml") $xml $enc
|
||||||
|
|
||||||
# --- Модуль объекта ---
|
# --- Модуль объекта ---
|
||||||
|
|
||||||
@@ -83,6 +112,11 @@ $moduleBsl = @"
|
|||||||
"@
|
"@
|
||||||
|
|
||||||
$modulePath = Join-Path $extDir "ObjectModule.bsl"
|
$modulePath = Join-Path $extDir "ObjectModule.bsl"
|
||||||
|
# Модуль пишем в каноне платформы: CRLF в разделителях строк (корпус: 2643 CRLF,
|
||||||
|
# чисто-LF 0 из 3001). Хвостовой перевод НЕ навязываем — у платформы он
|
||||||
|
# неканоничен (1235 модулей с ним, 766 без). Шаблон берёт переводы строк из
|
||||||
|
# самого скрипта, а он в репозитории хранится с LF.
|
||||||
|
$moduleBsl = ($moduleBsl -replace "`r`n", "`n") -replace "`n", "`r`n"
|
||||||
[System.IO.File]::WriteAllText($modulePath, $moduleBsl, $enc)
|
[System.IO.File]::WriteAllText($modulePath, $moduleBsl, $enc)
|
||||||
|
|
||||||
Write-Host "[OK] Создана обработка: $rootFile"
|
Write-Host "[OK] Создана обработка: $rootFile"
|
||||||
|
|||||||
@@ -1,19 +1,61 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# epf-init v1.1 — Init 1C external data processor scaffold
|
# epf-init v1.7 — Init 1C external data processor scaffold (+write_xml_file/write_utf8_bom: общий эталон записи)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
"""Generates minimal XML source files for a 1C external data processor."""
|
"""Generates minimal XML source files for a 1C external data processor."""
|
||||||
import sys, os, argparse, uuid
|
import sys, os, re, argparse, uuid
|
||||||
|
|
||||||
def esc_xml(s):
|
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
|
||||||
return s.replace('&','&').replace('<','<').replace('>','>').replace('"','"')
|
# регистр не различают, в 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)
|
||||||
|
|
||||||
|
|
||||||
|
def esc_xml_text(s):
|
||||||
|
# Эскейп ТЕКСТА элемента: только & < > — кавычку и апостроф платформа держит сырыми.
|
||||||
|
return s.replace('&', '&').replace('<', '<').replace('>', '>')
|
||||||
|
|
||||||
def new_uuid():
|
def new_uuid():
|
||||||
return str(uuid.uuid4())
|
return str(uuid.uuid4())
|
||||||
|
|
||||||
def write_utf8_bom(path, content):
|
def write_utf8_bom(path, content):
|
||||||
|
# newline='' — без трансляции: иначе текстовый режим Python дал бы CRLF на Windows
|
||||||
|
# и LF на macOS, то есть вывод навыка зависел бы от ОС.
|
||||||
with open(path, 'w', encoding='utf-8-sig', newline='') as f:
|
with open(path, 'w', encoding='utf-8-sig', newline='') as f:
|
||||||
f.write(content)
|
f.write(content)
|
||||||
|
|
||||||
|
|
||||||
|
def write_xml_file(path, content):
|
||||||
|
"""XML в каноне выгрузки Конфигуратора: CRLF в разделителях, без перевода в конце.
|
||||||
|
|
||||||
|
Копия этой функции есть в каждом навыке-эмиттере (навыки автономны). Держать
|
||||||
|
копии одинаковыми — сознательно: разошедшиеся копии сводят на нет весь смысл.
|
||||||
|
"""
|
||||||
|
text = content.replace('\r\n', '\n').replace('\n', '\r\n').rstrip('\r\n')
|
||||||
|
write_utf8_bom(path, text)
|
||||||
|
|
||||||
|
|
||||||
|
def format_rank(ver):
|
||||||
|
""""2.20" → 220, "2.9" → 209. Строковое сравнение неверно ("2.9" > "2.17")."""
|
||||||
|
m = re.match(r'^(\d+)\.(\d+)$', ver or '')
|
||||||
|
return int(m.group(1)) * 100 + int(m.group(2)) if m else 0
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
sys.stdout.reconfigure(encoding="utf-8")
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
sys.stderr.reconfigure(encoding="utf-8")
|
||||||
@@ -21,7 +63,12 @@ def main():
|
|||||||
parser.add_argument('-Name', dest='Name', required=True)
|
parser.add_argument('-Name', dest='Name', required=True)
|
||||||
parser.add_argument('-Synonym', dest='Synonym', default=None)
|
parser.add_argument('-Synonym', dest='Synonym', default=None)
|
||||||
parser.add_argument('-SrcDir', dest='SrcDir', default='src')
|
parser.add_argument('-SrcDir', dest='SrcDir', default='src')
|
||||||
args = parser.parse_args()
|
# Версия формата выгрузки. Своей конфигурации у автономного объекта нет, наследовать
|
||||||
|
# версию неоткуда — поэтому её задают явно. Формы, макеты и справку внутри объекта
|
||||||
|
# навыки берут уже отсюда: их детектор читает version из корня этого файла.
|
||||||
|
parser.add_argument('-FormatVersion', dest='FormatVersion', default='2.17',
|
||||||
|
choices=['2.17', '2.18', '2.19', '2.20', '2.21'])
|
||||||
|
args = ci_parse_args(parser)
|
||||||
|
|
||||||
name = args.Name
|
name = args.Name
|
||||||
synonym = args.Synonym if args.Synonym else name
|
synonym = args.Synonym if args.Synonym else name
|
||||||
@@ -32,8 +79,36 @@ def main():
|
|||||||
uuid3 = new_uuid()
|
uuid3 = new_uuid()
|
||||||
uuid4 = new_uuid()
|
uuid4 = new_uuid()
|
||||||
|
|
||||||
|
# Объявления пространств имён — одной переменной: места эмиссии её только подставляют.
|
||||||
|
xmlns_decl = (
|
||||||
|
'xmlns="http://v8.1c.ru/8.3/MDClasses"'
|
||||||
|
' xmlns:app="http://v8.1c.ru/8.2/managed-application/core"'
|
||||||
|
' xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config"'
|
||||||
|
' xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi"'
|
||||||
|
' xmlns:ent="http://v8.1c.ru/8.1/data/enterprise"'
|
||||||
|
' xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform"'
|
||||||
|
' xmlns:style="http://v8.1c.ru/8.1/data/ui/style"'
|
||||||
|
' xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system"'
|
||||||
|
' xmlns:v8="http://v8.1c.ru/8.1/data/core"'
|
||||||
|
' xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"'
|
||||||
|
' xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web"'
|
||||||
|
' xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows"'
|
||||||
|
' xmlns:xen="http://v8.1c.ru/8.3/xcf/enums"'
|
||||||
|
' xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef"'
|
||||||
|
' xmlns:xr="http://v8.1c.ru/8.3/xcf/readable"'
|
||||||
|
' xmlns:xs="http://www.w3.org/2001/XMLSchema"'
|
||||||
|
' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
|
||||||
|
)
|
||||||
|
format_version = args.FormatVersion
|
||||||
|
# 2.21 (8.5) добавила в шапку пространство палитры. Вставляем НА МЕСТО (после lf, перед
|
||||||
|
# style): платформа держит объявления по алфавиту, дописать в конец нельзя.
|
||||||
|
if format_rank(format_version) >= 221:
|
||||||
|
xmlns_decl = xmlns_decl.replace(
|
||||||
|
' xmlns:style=',
|
||||||
|
' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette" xmlns:style=')
|
||||||
|
|
||||||
xml = f'''<?xml version="1.0" encoding="UTF-8"?>
|
xml = f'''<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
|
<MetaDataObject {xmlns_decl} version="{format_version}">
|
||||||
\t<ExternalDataProcessor uuid="{uuid1}">
|
\t<ExternalDataProcessor uuid="{uuid1}">
|
||||||
\t\t<InternalInfo>
|
\t\t<InternalInfo>
|
||||||
\t\t\t<xr:ContainedObject>
|
\t\t\t<xr:ContainedObject>
|
||||||
@@ -46,11 +121,11 @@ def main():
|
|||||||
\t\t\t</xr:GeneratedType>
|
\t\t\t</xr:GeneratedType>
|
||||||
\t\t</InternalInfo>
|
\t\t</InternalInfo>
|
||||||
\t\t<Properties>
|
\t\t<Properties>
|
||||||
\t\t\t<Name>{esc_xml(name)}</Name>
|
\t\t\t<Name>{esc_xml_text(name)}</Name>
|
||||||
\t\t\t<Synonym>
|
\t\t\t<Synonym>
|
||||||
\t\t\t\t<v8:item>
|
\t\t\t\t<v8:item>
|
||||||
\t\t\t\t\t<v8:lang>ru</v8:lang>
|
\t\t\t\t\t<v8:lang>ru</v8:lang>
|
||||||
\t\t\t\t\t<v8:content>{esc_xml(synonym)}</v8:content>
|
\t\t\t\t\t<v8:content>{esc_xml_text(synonym)}</v8:content>
|
||||||
\t\t\t\t</v8:item>
|
\t\t\t\t</v8:item>
|
||||||
\t\t\t</Synonym>
|
\t\t\t</Synonym>
|
||||||
\t\t\t<Comment/>
|
\t\t\t<Comment/>
|
||||||
@@ -72,7 +147,7 @@ def main():
|
|||||||
ext_dir = os.path.join(processor_dir, "Ext")
|
ext_dir = os.path.join(processor_dir, "Ext")
|
||||||
os.makedirs(ext_dir, exist_ok=True)
|
os.makedirs(ext_dir, exist_ok=True)
|
||||||
|
|
||||||
write_utf8_bom(os.path.join(os.path.abspath(src_dir), f"{name}.xml"), xml)
|
write_xml_file(os.path.join(os.path.abspath(src_dir), f"{name}.xml"), xml)
|
||||||
|
|
||||||
# --- Модуль объекта ---
|
# --- Модуль объекта ---
|
||||||
module_bsl = """\
|
module_bsl = """\
|
||||||
@@ -89,7 +164,10 @@ def main():
|
|||||||
#КонецОбласти"""
|
#КонецОбласти"""
|
||||||
|
|
||||||
module_path = os.path.join(ext_dir, "ObjectModule.bsl")
|
module_path = os.path.join(ext_dir, "ObjectModule.bsl")
|
||||||
write_utf8_bom(module_path, module_bsl)
|
# Модуль пишем в каноне платформы: CRLF в разделителях строк (корпус: 2643 CRLF,
|
||||||
|
# чисто-LF 0 из 3001). Хвостовой перевод НЕ навязываем — у платформы он
|
||||||
|
# неканоничен (1235 модулей с ним, 766 без).
|
||||||
|
write_utf8_bom(module_path, module_bsl.replace('\r\n', '\n').replace('\n', '\r\n'))
|
||||||
|
|
||||||
print(f"[OK] Создана обработка: {root_file}")
|
print(f"[OK] Создана обработка: {root_file}")
|
||||||
print(f" Каталог: {processor_dir}")
|
print(f" Каталог: {processor_dir}")
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# epf-validate v1.2 — Validate 1C external data processor / report structure
|
# epf-validate v1.4 — Validate 1C external data processor / report structure
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
# Works for both EPF (ExternalDataProcessor) and ERF (ExternalReport) — auto-detects
|
# Works for both EPF (ExternalDataProcessor) and ERF (ExternalReport) — auto-detects
|
||||||
param(
|
param(
|
||||||
@@ -185,8 +185,9 @@ if ($root.NamespaceURI -ne $expectedNs) {
|
|||||||
$version = $root.GetAttribute("version")
|
$version = $root.GetAttribute("version")
|
||||||
if (-not $version) {
|
if (-not $version) {
|
||||||
Report-Warn "1. Missing version attribute on MetaDataObject"
|
Report-Warn "1. Missing version attribute on MetaDataObject"
|
||||||
} elseif ($version -ne "2.17" -and $version -ne "2.20" -and $version -ne "2.21") {
|
} elseif ($version -notin @("2.17", "2.18", "2.19", "2.20", "2.21")) {
|
||||||
Report-Warn "1. Unusual version '$version' (expected 2.17, 2.20 or 2.21)"
|
# Лестница версий формата: 2.17 (8.3.20-8.3.24), 2.18 (8.3.25), 2.19 (8.3.26), 2.20 (8.3.27).
|
||||||
|
Report-Warn "1. Unusual version '$version' (expected 2.17-2.20 or 2.21)"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Detect type: ExternalDataProcessor or ExternalReport
|
# Detect type: ExternalDataProcessor or ExternalReport
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# epf-validate v1.2 — Validate 1C external data processor / report structure
|
# epf-validate v1.4 — Validate 1C external data processor / report structure
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
# Works for both EPF (ExternalDataProcessor) and ERF (ExternalReport) — auto-detects
|
# Works for both EPF (ExternalDataProcessor) and ERF (ExternalReport) — auto-detects
|
||||||
|
|
||||||
@@ -10,6 +10,28 @@ import sys
|
|||||||
from io import StringIO
|
from io import StringIO
|
||||||
from lxml import etree
|
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)
|
||||||
|
|
||||||
|
|
||||||
MD_NS = "http://v8.1c.ru/8.3/MDClasses"
|
MD_NS = "http://v8.1c.ru/8.3/MDClasses"
|
||||||
V8_NS = "http://v8.1c.ru/8.1/data/core"
|
V8_NS = "http://v8.1c.ru/8.1/data/core"
|
||||||
XR_NS = "http://v8.1c.ru/8.3/xcf/readable"
|
XR_NS = "http://v8.1c.ru/8.3/xcf/readable"
|
||||||
@@ -50,7 +72,7 @@ def main():
|
|||||||
parser.add_argument("-Detailed", action="store_true")
|
parser.add_argument("-Detailed", action="store_true")
|
||||||
parser.add_argument("-MaxErrors", type=int, default=30)
|
parser.add_argument("-MaxErrors", type=int, default=30)
|
||||||
parser.add_argument("-OutFile", default=None)
|
parser.add_argument("-OutFile", default=None)
|
||||||
args = parser.parse_args()
|
args = ci_parse_args(parser)
|
||||||
|
|
||||||
max_errors = args.MaxErrors
|
max_errors = args.MaxErrors
|
||||||
|
|
||||||
@@ -165,8 +187,9 @@ def main():
|
|||||||
version = root.get("version", "")
|
version = root.get("version", "")
|
||||||
if not version:
|
if not version:
|
||||||
report_warn("1. Missing version attribute on MetaDataObject")
|
report_warn("1. Missing version attribute on MetaDataObject")
|
||||||
elif version not in ("2.17", "2.20", "2.21"):
|
elif version not in ("2.17", "2.18", "2.19", "2.20", "2.21"):
|
||||||
report_warn(f"1. Unusual version '{version}' (expected 2.17, 2.20 or 2.21)")
|
# Лестница версий формата: 2.17 (8.3.20-8.3.24), 2.18 (8.3.25), 2.19 (8.3.26), 2.20 (8.3.27).
|
||||||
|
report_warn(f"1. Unusual version '{version}' (expected 2.17-2.20 or 2.21)")
|
||||||
|
|
||||||
# Detect type
|
# Detect type
|
||||||
child_elements = []
|
child_elements = []
|
||||||
|
|||||||
@@ -57,6 +57,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-build/scripts/epf-bu
|
|||||||
| `-Password <пароль>` | нет | Пароль |
|
| `-Password <пароль>` | нет | Пароль |
|
||||||
| `-SourceFile <путь>` | да | Путь к корневому XML-файлу исходников |
|
| `-SourceFile <путь>` | да | Путь к корневому XML-файлу исходников |
|
||||||
| `-OutputFile <путь>` | да | Путь к выходному ERF-файлу |
|
| `-OutputFile <путь>` | да | Путь к выходному ERF-файлу |
|
||||||
|
| `-AdditionalV8Arguments <список>` | нет | Доп. аргументы запуска `1cv8.exe` через запятую, напр. `/UseHwLicenses+` |
|
||||||
|
| `-AdditionalIbcmdArguments <список>` | нет | Доп. аргументы `ibcmd` через запятую, в форме `--ключ=значение` |
|
||||||
|
|
||||||
> `*` — опционально. Если не указано — автоматически создаётся временная база со заглушками метаданных
|
> `*` — опционально. Если не указано — автоматически создаётся временная база со заглушками метаданных
|
||||||
|
|
||||||
|
|||||||
@@ -57,6 +57,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-dump/scripts/epf-dum
|
|||||||
| `-InputFile <путь>` | да | Путь к ERF-файлу |
|
| `-InputFile <путь>` | да | Путь к ERF-файлу |
|
||||||
| `-OutputDir <путь>` | да | Каталог для выгрузки исходников |
|
| `-OutputDir <путь>` | да | Каталог для выгрузки исходников |
|
||||||
| `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` |
|
| `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` |
|
||||||
|
| `-AdditionalV8Arguments <список>` | нет | Доп. аргументы запуска `1cv8.exe` через запятую, напр. `/UseHwLicenses+` |
|
||||||
|
| `-AdditionalIbcmdArguments <список>` | нет | Доп. аргументы `ibcmd` через запятую, в форме `--ключ=значение` |
|
||||||
|
|
||||||
> `*` — обязательно хотя бы одно подключение. Без базы скрипт завершится с ошибкой (dump в пустой базе безвозвратно теряет ссылочные типы)
|
> `*` — обязательно хотя бы одно подключение. Без базы скрипт завершится с ошибкой (dump в пустой базе безвозвратно теряет ссылочные типы)
|
||||||
|
|
||||||
|
|||||||
@@ -18,20 +18,21 @@ allowed-tools:
|
|||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
```
|
```
|
||||||
/erf-init <Name> [Synonym] [SrcDir] [--with-skd]
|
/erf-init <Name> [Synonym] [SrcDir] [FormatVersion] [--with-skd]
|
||||||
```
|
```
|
||||||
|
|
||||||
| Параметр | Обязательный | По умолчанию | Описание |
|
| Параметр | Обязательный | По умолчанию | Описание |
|
||||||
|-----------|:------------:|--------------|---------------------------------------|
|
|---------------|:------------:|--------------|---------------------------------------|
|
||||||
| Name | да | — | Имя отчёта (латиница/кириллица) |
|
| Name | да | — | Имя отчёта (латиница/кириллица) |
|
||||||
| Synonym | нет | = Name | Синоним (отображаемое имя) |
|
| Synonym | нет | = Name | Синоним (отображаемое имя) |
|
||||||
| SrcDir | нет | `src` | Каталог исходников относительно CWD |
|
| SrcDir | нет | `src` | Каталог исходников относительно CWD |
|
||||||
| --WithSKD | нет | — | Создать пустую СКД и привязать к MainDataCompositionSchema |
|
| FormatVersion | нет | `2.17` | Версия формата: 2.20 — платформа 8.3.27, 2.21 — 8.5. Дефолт открывается любой платформой |
|
||||||
|
| --WithSKD | нет | — | Создать пустую СКД и привязать к MainDataCompositionSchema |
|
||||||
|
|
||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/init.ps1" -Name "<Name>" [-Synonym "<Synonym>"] [-SrcDir "<SrcDir>"] [-WithSKD]
|
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/init.ps1" -Name "<Name>" [-Synonym "<Synonym>"] [-SrcDir "<SrcDir>"] [-FormatVersion "<2.17|2.18|2.19|2.20|2.21>"] [-WithSKD]
|
||||||
```
|
```
|
||||||
|
|
||||||
## Дальнейшие шаги
|
## Дальнейшие шаги
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# erf-init v1.1 — Init 1C external report scaffold
|
# erf-init v1.7 — Init 1C external report scaffold (+write_xml_file/write_utf8_bom: общий эталон записи)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory)]
|
||||||
@@ -8,10 +8,22 @@ param(
|
|||||||
|
|
||||||
[string]$SrcDir = "src",
|
[string]$SrcDir = "src",
|
||||||
|
|
||||||
[switch]$WithSKD
|
[switch]$WithSKD,
|
||||||
|
|
||||||
|
# Версия формата выгрузки. Своей конфигурации у автономного отчёта нет, наследовать
|
||||||
|
# версию неоткуда — поэтому её задают явно. Формы, макеты и справку внутри отчёта
|
||||||
|
# навыки берут уже отсюда: их детектор читает version из корня этого файла.
|
||||||
|
[ValidateSet("2.17", "2.18", "2.19", "2.20", "2.21")]
|
||||||
|
[string]$FormatVersion = "2.17"
|
||||||
)
|
)
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
function Esc-XmlText {
|
||||||
|
param([string]$s)
|
||||||
|
# Эскейп ТЕКСТА элемента: только & < > — кавычку и апостроф платформа держит сырыми.
|
||||||
|
return $s.Replace('&','&').Replace('<','<').Replace('>','>')
|
||||||
|
}
|
||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
[Console]::InputEncoding = [System.Text.Encoding]::UTF8
|
[Console]::InputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
|
||||||
@@ -20,6 +32,14 @@ $uuid2 = [guid]::NewGuid().ToString()
|
|||||||
$uuid3 = [guid]::NewGuid().ToString()
|
$uuid3 = [guid]::NewGuid().ToString()
|
||||||
$uuid4 = [guid]::NewGuid().ToString()
|
$uuid4 = [guid]::NewGuid().ToString()
|
||||||
|
|
||||||
|
# Объявления пространств имён — одной переменной: места эмиссии её только интерполируют.
|
||||||
|
$xmlnsDecl = 'xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
|
||||||
|
# 2.21 (8.5) добавила в шапку пространство палитры. Вставляем НА МЕСТО (после lf, перед style):
|
||||||
|
# платформа держит объявления по алфавиту, дописать в конец нельзя.
|
||||||
|
if (($FormatVersion -match '^(\d+)\.(\d+)$') -and ([int]$Matches[1] * 100 + [int]$Matches[2]) -ge 221) {
|
||||||
|
$xmlnsDecl = $xmlnsDecl -replace ' xmlns:style=', ' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette" xmlns:style='
|
||||||
|
}
|
||||||
|
|
||||||
# --- Формируем Properties ---
|
# --- Формируем Properties ---
|
||||||
|
|
||||||
$mainDCSValue = ""
|
$mainDCSValue = ""
|
||||||
@@ -48,7 +68,7 @@ $childObjectsXml = if ($childObjectsContent) {
|
|||||||
|
|
||||||
$xml = @"
|
$xml = @"
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
|
<MetaDataObject $xmlnsDecl version="$FormatVersion">
|
||||||
<ExternalReport uuid="$uuid1">
|
<ExternalReport uuid="$uuid1">
|
||||||
<InternalInfo>
|
<InternalInfo>
|
||||||
<xr:ContainedObject>
|
<xr:ContainedObject>
|
||||||
@@ -61,11 +81,11 @@ $xml = @"
|
|||||||
</xr:GeneratedType>
|
</xr:GeneratedType>
|
||||||
</InternalInfo>
|
</InternalInfo>
|
||||||
<Properties>
|
<Properties>
|
||||||
<Name>$Name</Name>
|
<Name>$(Esc-XmlText $Name)</Name>
|
||||||
<Synonym>
|
<Synonym>
|
||||||
<v8:item>
|
<v8:item>
|
||||||
<v8:lang>ru</v8:lang>
|
<v8:lang>ru</v8:lang>
|
||||||
<v8:content>$Synonym</v8:content>
|
<v8:content>$(Esc-XmlText $Synonym)</v8:content>
|
||||||
</v8:item>
|
</v8:item>
|
||||||
</Synonym>
|
</Synonym>
|
||||||
<Comment/>
|
<Comment/>
|
||||||
@@ -98,7 +118,16 @@ $extDir = Join-Path $reportDir "Ext"
|
|||||||
New-Item -ItemType Directory -Path $extDir -Force | Out-Null
|
New-Item -ItemType Directory -Path $extDir -Force | Out-Null
|
||||||
|
|
||||||
$enc = New-Object System.Text.UTF8Encoding($true)
|
$enc = New-Object System.Text.UTF8Encoding($true)
|
||||||
[System.IO.File]::WriteAllText((Resolve-Path $SrcDir | Join-Path -ChildPath "$Name.xml"), $xml, $enc)
|
# XML в каноне выгрузки Конфигуратора: CRLF в разделителях, без перевода в конце.
|
||||||
|
#
|
||||||
|
# Копия этой функции есть в каждом навыке-эмиттере (навыки автономны). Держать
|
||||||
|
# копии одинаковыми — сознательно: разошедшиеся копии сводят на нет весь смысл.
|
||||||
|
function Write-XmlFile([string]$path, [string]$text, $encoding) {
|
||||||
|
$t = ($text -replace "`r`n", "`n") -replace "`n", "`r`n"
|
||||||
|
[System.IO.File]::WriteAllText($path, $t.TrimEnd("`r", "`n"), $encoding)
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-XmlFile (Resolve-Path $SrcDir | Join-Path -ChildPath "$Name.xml") $xml $enc
|
||||||
|
|
||||||
# --- Модуль объекта ---
|
# --- Модуль объекта ---
|
||||||
|
|
||||||
@@ -117,6 +146,11 @@ $moduleBsl = @"
|
|||||||
"@
|
"@
|
||||||
|
|
||||||
$modulePath = Join-Path $extDir "ObjectModule.bsl"
|
$modulePath = Join-Path $extDir "ObjectModule.bsl"
|
||||||
|
# Модуль пишем в каноне платформы: CRLF в разделителях строк (корпус: 2643 CRLF,
|
||||||
|
# чисто-LF 0 из 3001). Хвостовой перевод НЕ навязываем — у платформы он
|
||||||
|
# неканоничен (1235 модулей с ним, 766 без). Шаблон берёт переводы строк из
|
||||||
|
# самого скрипта, а он в репозитории хранится с LF.
|
||||||
|
$moduleBsl = ($moduleBsl -replace "`r`n", "`n") -replace "`n", "`r`n"
|
||||||
[System.IO.File]::WriteAllText($modulePath, $moduleBsl, $enc)
|
[System.IO.File]::WriteAllText($modulePath, $moduleBsl, $enc)
|
||||||
|
|
||||||
Write-Host "[OK] Создан отчёт: $rootFile"
|
Write-Host "[OK] Создан отчёт: $rootFile"
|
||||||
@@ -136,7 +170,7 @@ if ($WithSKD) {
|
|||||||
|
|
||||||
$skdMetaXml = @"
|
$skdMetaXml = @"
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
|
<MetaDataObject $xmlnsDecl version="$FormatVersion">
|
||||||
<Template uuid="$skdUuid">
|
<Template uuid="$skdUuid">
|
||||||
<Properties>
|
<Properties>
|
||||||
<Name>$skdName</Name>
|
<Name>$skdName</Name>
|
||||||
@@ -153,7 +187,7 @@ if ($WithSKD) {
|
|||||||
</MetaDataObject>
|
</MetaDataObject>
|
||||||
"@
|
"@
|
||||||
|
|
||||||
[System.IO.File]::WriteAllText($skdMetaPath, $skdMetaXml, $enc)
|
Write-XmlFile $skdMetaPath $skdMetaXml $enc
|
||||||
|
|
||||||
$skdContent = @"
|
$skdContent = @"
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
@@ -173,7 +207,7 @@ if ($WithSKD) {
|
|||||||
"@
|
"@
|
||||||
|
|
||||||
$skdFilePath = Join-Path $skdExtDir "Template.xml"
|
$skdFilePath = Join-Path $skdExtDir "Template.xml"
|
||||||
[System.IO.File]::WriteAllText($skdFilePath, $skdContent, $enc)
|
Write-XmlFile $skdFilePath $skdContent $enc
|
||||||
|
|
||||||
Write-Host " СКД: $skdMetaPath"
|
Write-Host " СКД: $skdMetaPath"
|
||||||
Write-Host " Тело: $skdFilePath"
|
Write-Host " Тело: $skdFilePath"
|
||||||
|
|||||||
@@ -1,19 +1,61 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# erf-init v1.1 — Init 1C external report scaffold
|
# erf-init v1.7 — Init 1C external report scaffold (+write_xml_file/write_utf8_bom: общий эталон записи)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
"""Generates minimal XML source files for a 1C external report."""
|
"""Generates minimal XML source files for a 1C external report."""
|
||||||
import sys, os, argparse, uuid
|
import sys, os, re, argparse, uuid
|
||||||
|
|
||||||
def esc_xml(s):
|
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
|
||||||
return s.replace('&','&').replace('<','<').replace('>','>').replace('"','"')
|
# регистр не различают, в 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)
|
||||||
|
|
||||||
|
|
||||||
|
def esc_xml_text(s):
|
||||||
|
# Эскейп ТЕКСТА элемента: только & < > — кавычку и апостроф платформа держит сырыми.
|
||||||
|
return s.replace('&', '&').replace('<', '<').replace('>', '>')
|
||||||
|
|
||||||
def new_uuid():
|
def new_uuid():
|
||||||
return str(uuid.uuid4())
|
return str(uuid.uuid4())
|
||||||
|
|
||||||
def write_utf8_bom(path, content):
|
def write_utf8_bom(path, content):
|
||||||
|
# newline='' — без трансляции: иначе текстовый режим Python дал бы CRLF на Windows
|
||||||
|
# и LF на macOS, то есть вывод навыка зависел бы от ОС.
|
||||||
with open(path, 'w', encoding='utf-8-sig', newline='') as f:
|
with open(path, 'w', encoding='utf-8-sig', newline='') as f:
|
||||||
f.write(content)
|
f.write(content)
|
||||||
|
|
||||||
|
|
||||||
|
def write_xml_file(path, content):
|
||||||
|
"""XML в каноне выгрузки Конфигуратора: CRLF в разделителях, без перевода в конце.
|
||||||
|
|
||||||
|
Копия этой функции есть в каждом навыке-эмиттере (навыки автономны). Держать
|
||||||
|
копии одинаковыми — сознательно: разошедшиеся копии сводят на нет весь смысл.
|
||||||
|
"""
|
||||||
|
text = content.replace('\r\n', '\n').replace('\n', '\r\n').rstrip('\r\n')
|
||||||
|
write_utf8_bom(path, text)
|
||||||
|
|
||||||
|
|
||||||
|
def format_rank(ver):
|
||||||
|
""""2.20" → 220, "2.9" → 209. Строковое сравнение неверно ("2.9" > "2.17")."""
|
||||||
|
m = re.match(r'^(\d+)\.(\d+)$', ver or '')
|
||||||
|
return int(m.group(1)) * 100 + int(m.group(2)) if m else 0
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
sys.stdout.reconfigure(encoding="utf-8")
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
sys.stderr.reconfigure(encoding="utf-8")
|
||||||
@@ -21,8 +63,13 @@ def main():
|
|||||||
parser.add_argument('-Name', dest='Name', required=True)
|
parser.add_argument('-Name', dest='Name', required=True)
|
||||||
parser.add_argument('-Synonym', dest='Synonym', default=None)
|
parser.add_argument('-Synonym', dest='Synonym', default=None)
|
||||||
parser.add_argument('-SrcDir', dest='SrcDir', default='src')
|
parser.add_argument('-SrcDir', dest='SrcDir', default='src')
|
||||||
|
# Версия формата выгрузки. Своей конфигурации у автономного объекта нет, наследовать
|
||||||
|
# версию неоткуда — поэтому её задают явно. Формы, макеты и справку внутри объекта
|
||||||
|
# навыки берут уже отсюда: их детектор читает version из корня этого файла.
|
||||||
|
parser.add_argument('-FormatVersion', dest='FormatVersion', default='2.17',
|
||||||
|
choices=['2.17', '2.18', '2.19', '2.20', '2.21'])
|
||||||
parser.add_argument('-WithSKD', dest='WithSKD', action='store_true')
|
parser.add_argument('-WithSKD', dest='WithSKD', action='store_true')
|
||||||
args = parser.parse_args()
|
args = ci_parse_args(parser)
|
||||||
|
|
||||||
name = args.Name
|
name = args.Name
|
||||||
synonym = args.Synonym if args.Synonym else name
|
synonym = args.Synonym if args.Synonym else name
|
||||||
@@ -33,6 +80,34 @@ def main():
|
|||||||
uuid3 = new_uuid()
|
uuid3 = new_uuid()
|
||||||
uuid4 = new_uuid()
|
uuid4 = new_uuid()
|
||||||
|
|
||||||
|
# Объявления пространств имён — одной переменной: места эмиссии её только подставляют.
|
||||||
|
xmlns_decl = (
|
||||||
|
'xmlns="http://v8.1c.ru/8.3/MDClasses"'
|
||||||
|
' xmlns:app="http://v8.1c.ru/8.2/managed-application/core"'
|
||||||
|
' xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config"'
|
||||||
|
' xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi"'
|
||||||
|
' xmlns:ent="http://v8.1c.ru/8.1/data/enterprise"'
|
||||||
|
' xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform"'
|
||||||
|
' xmlns:style="http://v8.1c.ru/8.1/data/ui/style"'
|
||||||
|
' xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system"'
|
||||||
|
' xmlns:v8="http://v8.1c.ru/8.1/data/core"'
|
||||||
|
' xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"'
|
||||||
|
' xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web"'
|
||||||
|
' xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows"'
|
||||||
|
' xmlns:xen="http://v8.1c.ru/8.3/xcf/enums"'
|
||||||
|
' xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef"'
|
||||||
|
' xmlns:xr="http://v8.1c.ru/8.3/xcf/readable"'
|
||||||
|
' xmlns:xs="http://www.w3.org/2001/XMLSchema"'
|
||||||
|
' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
|
||||||
|
)
|
||||||
|
format_version = args.FormatVersion
|
||||||
|
# 2.21 (8.5) добавила в шапку пространство палитры. Вставляем НА МЕСТО (после lf, перед
|
||||||
|
# style): платформа держит объявления по алфавиту, дописать в конец нельзя.
|
||||||
|
if format_rank(format_version) >= 221:
|
||||||
|
xmlns_decl = xmlns_decl.replace(
|
||||||
|
' xmlns:style=',
|
||||||
|
' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette" xmlns:style=')
|
||||||
|
|
||||||
# --- Properties ---
|
# --- Properties ---
|
||||||
main_dcs_value = ""
|
main_dcs_value = ""
|
||||||
child_objects_content = ""
|
child_objects_content = ""
|
||||||
@@ -45,7 +120,7 @@ def main():
|
|||||||
child_objects_xml = f"<ChildObjects>{child_objects_content}\t\t</ChildObjects>" if child_objects_content else "<ChildObjects/>"
|
child_objects_xml = f"<ChildObjects>{child_objects_content}\t\t</ChildObjects>" if child_objects_content else "<ChildObjects/>"
|
||||||
|
|
||||||
xml = f'''<?xml version="1.0" encoding="UTF-8"?>
|
xml = f'''<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
|
<MetaDataObject {xmlns_decl} version="{format_version}">
|
||||||
\t<ExternalReport uuid="{uuid1}">
|
\t<ExternalReport uuid="{uuid1}">
|
||||||
\t\t<InternalInfo>
|
\t\t<InternalInfo>
|
||||||
\t\t\t<xr:ContainedObject>
|
\t\t\t<xr:ContainedObject>
|
||||||
@@ -58,11 +133,11 @@ def main():
|
|||||||
\t\t\t</xr:GeneratedType>
|
\t\t\t</xr:GeneratedType>
|
||||||
\t\t</InternalInfo>
|
\t\t</InternalInfo>
|
||||||
\t\t<Properties>
|
\t\t<Properties>
|
||||||
\t\t\t<Name>{esc_xml(name)}</Name>
|
\t\t\t<Name>{esc_xml_text(name)}</Name>
|
||||||
\t\t\t<Synonym>
|
\t\t\t<Synonym>
|
||||||
\t\t\t\t<v8:item>
|
\t\t\t\t<v8:item>
|
||||||
\t\t\t\t\t<v8:lang>ru</v8:lang>
|
\t\t\t\t\t<v8:lang>ru</v8:lang>
|
||||||
\t\t\t\t\t<v8:content>{esc_xml(synonym)}</v8:content>
|
\t\t\t\t\t<v8:content>{esc_xml_text(synonym)}</v8:content>
|
||||||
\t\t\t\t</v8:item>
|
\t\t\t\t</v8:item>
|
||||||
\t\t\t</Synonym>
|
\t\t\t</Synonym>
|
||||||
\t\t\t<Comment/>
|
\t\t\t<Comment/>
|
||||||
@@ -90,7 +165,7 @@ def main():
|
|||||||
ext_dir = os.path.join(report_dir, "Ext")
|
ext_dir = os.path.join(report_dir, "Ext")
|
||||||
os.makedirs(ext_dir, exist_ok=True)
|
os.makedirs(ext_dir, exist_ok=True)
|
||||||
|
|
||||||
write_utf8_bom(os.path.join(os.path.abspath(src_dir), f"{name}.xml"), xml)
|
write_xml_file(os.path.join(os.path.abspath(src_dir), f"{name}.xml"), xml)
|
||||||
|
|
||||||
# --- Модуль объекта ---
|
# --- Модуль объекта ---
|
||||||
module_bsl = """\
|
module_bsl = """\
|
||||||
@@ -107,7 +182,10 @@ def main():
|
|||||||
#КонецОбласти"""
|
#КонецОбласти"""
|
||||||
|
|
||||||
module_path = os.path.join(ext_dir, "ObjectModule.bsl")
|
module_path = os.path.join(ext_dir, "ObjectModule.bsl")
|
||||||
write_utf8_bom(module_path, module_bsl)
|
# Модуль пишем в каноне платформы: CRLF в разделителях строк (корпус: 2643 CRLF,
|
||||||
|
# чисто-LF 0 из 3001). Хвостовой перевод НЕ навязываем — у платформы он
|
||||||
|
# неканоничен (1235 модулей с ним, 766 без).
|
||||||
|
write_utf8_bom(module_path, module_bsl.replace('\r\n', '\n').replace('\n', '\r\n'))
|
||||||
|
|
||||||
print(f"[OK] Создан отчёт: {root_file}")
|
print(f"[OK] Создан отчёт: {root_file}")
|
||||||
print(f" Каталог: {report_dir}")
|
print(f" Каталог: {report_dir}")
|
||||||
@@ -124,7 +202,7 @@ def main():
|
|||||||
skd_uuid = new_uuid()
|
skd_uuid = new_uuid()
|
||||||
|
|
||||||
skd_meta_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
|
skd_meta_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
|
<MetaDataObject {xmlns_decl} version="{format_version}">
|
||||||
\t<Template uuid="{skd_uuid}">
|
\t<Template uuid="{skd_uuid}">
|
||||||
\t\t<Properties>
|
\t\t<Properties>
|
||||||
\t\t\t<Name>{skd_name}</Name>
|
\t\t\t<Name>{skd_name}</Name>
|
||||||
@@ -140,7 +218,7 @@ def main():
|
|||||||
\t</Template>
|
\t</Template>
|
||||||
</MetaDataObject>'''
|
</MetaDataObject>'''
|
||||||
|
|
||||||
write_utf8_bom(skd_meta_path, skd_meta_xml)
|
write_xml_file(skd_meta_path, skd_meta_xml)
|
||||||
|
|
||||||
skd_content = '''<?xml version="1.0" encoding="UTF-8"?>
|
skd_content = '''<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema"
|
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema"
|
||||||
@@ -158,7 +236,7 @@ def main():
|
|||||||
</DataCompositionSchema>'''
|
</DataCompositionSchema>'''
|
||||||
|
|
||||||
skd_file_path = os.path.join(skd_ext_dir, "Template.xml")
|
skd_file_path = os.path.join(skd_ext_dir, "Template.xml")
|
||||||
write_utf8_bom(skd_file_path, skd_content)
|
write_xml_file(skd_file_path, skd_content)
|
||||||
|
|
||||||
print(f" СКД: {skd_meta_path}")
|
print(f" СКД: {skd_meta_path}")
|
||||||
print(f" Тело: {skd_file_path}")
|
print(f" Тело: {skd_file_path}")
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# form-add v1.11 — Add managed form to 1C config object
|
# form-add v1.25 — Add managed form to 1C config object (+write_xml_file/write_utf8_bom: общий эталон записи)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory)]
|
||||||
@@ -154,9 +154,20 @@ function Assert-EditAllowed([string]$targetPath, [string]$require) {
|
|||||||
function Detect-FormatVersion([string]$dir) {
|
function Detect-FormatVersion([string]$dir) {
|
||||||
$d = $dir
|
$d = $dir
|
||||||
while ($d) {
|
while ($d) {
|
||||||
|
# Автономная внешняя обработка/отчёт: своего Configuration.xml у неё нет, версию несёт
|
||||||
|
# корень самой обработки. Без этого форма и макет внутри обработки 2.21 писались бы 2.17.
|
||||||
|
$extPath = "$d.xml"
|
||||||
|
if (Test-Path $extPath) {
|
||||||
|
$extText = [System.IO.File]::ReadAllText($extPath, [System.Text.Encoding]::UTF8)
|
||||||
|
$extHead = $extText.Substring(0, [Math]::Min(2000, $extText.Length))
|
||||||
|
if ($extHead -match '<(ExternalDataProcessor|ExternalReport)[ >]' -and $extHead -match '<MetaDataObject[^>]+version="(\d+\.\d+)"') { return $Matches[1] }
|
||||||
|
}
|
||||||
$cfgPath = Join-Path $d "Configuration.xml"
|
$cfgPath = Join-Path $d "Configuration.xml"
|
||||||
if (Test-Path $cfgPath) {
|
if (Test-Path $cfgPath) {
|
||||||
$head = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8).Substring(0, [Math]::Min(2000, (Get-Item $cfgPath).Length))
|
$cfgText = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8)
|
||||||
|
# Длину среза берём по СТРОКЕ, а не по размеру файла: размер в БАЙТАХ, Substring считает
|
||||||
|
# СИМВОЛЫ, и на кириллице байт больше — короткий Configuration.xml ронял навык исключением.
|
||||||
|
$head = $cfgText.Substring(0, [Math]::Min(2000, $cfgText.Length))
|
||||||
if ($head -match '<MetaDataObject[^>]+version="(\d+\.\d+)"') { return $Matches[1] }
|
if ($head -match '<MetaDataObject[^>]+version="(\d+\.\d+)"') { return $Matches[1] }
|
||||||
}
|
}
|
||||||
$parent = Split-Path $d -Parent
|
$parent = Split-Path $d -Parent
|
||||||
@@ -166,6 +177,13 @@ function Detect-FormatVersion([string]$dir) {
|
|||||||
return "2.17"
|
return "2.17"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Версия формата как число для сравнений: "2.20" → 220, "2.9" → 209.
|
||||||
|
# Строковое сравнение здесь неверно ("2.9" > "2.17" лексикографически) — известная ловушка.
|
||||||
|
function Get-FormatRank([string]$ver) {
|
||||||
|
if ($ver -match '^(\d+)\.(\d+)$') { return [int]$Matches[1] * 100 + [int]$Matches[2] }
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
# --- Фаза 1: Определение типа объекта ---
|
# --- Фаза 1: Определение типа объекта ---
|
||||||
|
|
||||||
# Resolve ObjectPath (directory → .xml)
|
# Resolve ObjectPath (directory → .xml)
|
||||||
@@ -187,7 +205,26 @@ if (-not (Test-Path $ObjectPath)) {
|
|||||||
|
|
||||||
$objectXmlFull = Resolve-Path $ObjectPath
|
$objectXmlFull = Resolve-Path $ObjectPath
|
||||||
Assert-EditAllowed $objectXmlFull.Path 'editable'
|
Assert-EditAllowed $objectXmlFull.Path 'editable'
|
||||||
$script:formatVersion = Detect-FormatVersion (Split-Path $objectXmlFull.Path -Parent)
|
# Версию берём прежде всего из корня самого объекта — он её несёт всегда, а у автономной
|
||||||
|
# внешней обработки/отчёта подниматься к Configuration.xml просто некуда.
|
||||||
|
$script:formatVersion = $null
|
||||||
|
$objHead = [System.IO.File]::ReadAllText($objectXmlFull.Path, [System.Text.Encoding]::UTF8)
|
||||||
|
$objHead = $objHead.Substring(0, [Math]::Min(2000, $objHead.Length))
|
||||||
|
if ($objHead -match '<MetaDataObject[^>]+version="(\d+\.\d+)"') { $script:formatVersion = $Matches[1] }
|
||||||
|
if (-not $script:formatVersion) { $script:formatVersion = Detect-FormatVersion (Split-Path $objectXmlFull.Path -Parent) }
|
||||||
|
|
||||||
|
# Объявления пространств имён — одной переменной на корень: места эмиссии их только
|
||||||
|
# интерполируют. Правки шапки (как xmlns:pal в формате 2.21) делаются здесь, в одном месте.
|
||||||
|
$script:xmlnsDecl = 'xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
|
||||||
|
$script:formNsDecl = 'xmlns="http://v8.1c.ru/8.3/xcf/logform" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core" xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
|
||||||
|
|
||||||
|
# 2.21 (8.5) добавила в шапку пространство палитры — ради <Color> у значений перечисления.
|
||||||
|
# Вставляем НА МЕСТО (после lf, перед style): платформа держит объявления по алфавиту,
|
||||||
|
# дописать в конец нельзя.
|
||||||
|
if ((Get-FormatRank $script:formatVersion) -ge 221) {
|
||||||
|
$script:xmlnsDecl = $script:xmlnsDecl -replace ' xmlns:style=', ' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette" xmlns:style='
|
||||||
|
$script:formNsDecl = $script:formNsDecl -replace ' xmlns:style=', ' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette" xmlns:style='
|
||||||
|
}
|
||||||
|
|
||||||
$xmlDoc = New-Object System.Xml.XmlDocument
|
$xmlDoc = New-Object System.Xml.XmlDocument
|
||||||
$xmlDoc.PreserveWhitespace = $true
|
$xmlDoc.PreserveWhitespace = $true
|
||||||
@@ -310,9 +347,16 @@ if ($objectType -in $processorLikeTypes) {
|
|||||||
$extPresentationLine = "`n`t`t`t<ExtendedPresentation/>"
|
$extPresentationLine = "`n`t`t`t<ExtendedPresentation/>"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Использование в режиме совместимости интерфейса — свойство формата 2.21 (8.5),
|
||||||
|
# сразу после UsePurposes (проверено по выгрузке 8.5, до ExtendedPresentation).
|
||||||
|
$useInIfcLine = ""
|
||||||
|
if ((Get-FormatRank $script:formatVersion) -ge 221) {
|
||||||
|
$useInIfcLine = "`n`t`t`t<UseInInterfaceCompatibilityMode>Any</UseInInterfaceCompatibilityMode>"
|
||||||
|
}
|
||||||
|
|
||||||
$formMetaXml = @"
|
$formMetaXml = @"
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="$($script:formatVersion)">
|
<MetaDataObject $($script:xmlnsDecl) version="$($script:formatVersion)">
|
||||||
<Form uuid="$formUuid">
|
<Form uuid="$formUuid">
|
||||||
<Properties>
|
<Properties>
|
||||||
<Name>$FormName</Name>
|
<Name>$FormName</Name>
|
||||||
@@ -328,20 +372,29 @@ $formMetaXml = @"
|
|||||||
<UsePurposes>
|
<UsePurposes>
|
||||||
<v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value>
|
<v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value>
|
||||||
<v8:Value xsi:type="app:ApplicationUsePurpose">MobilePlatformApplication</v8:Value>
|
<v8:Value xsi:type="app:ApplicationUsePurpose">MobilePlatformApplication</v8:Value>
|
||||||
</UsePurposes>$extPresentationLine
|
</UsePurposes>$useInIfcLine$extPresentationLine
|
||||||
</Properties>
|
</Properties>
|
||||||
</Form>
|
</Form>
|
||||||
</MetaDataObject>
|
</MetaDataObject>
|
||||||
"@
|
"@
|
||||||
|
|
||||||
[System.IO.File]::WriteAllText($formMetaPath, $formMetaXml, $encBom)
|
# XML в каноне выгрузки Конфигуратора: CRLF в разделителях, без перевода в конце.
|
||||||
|
#
|
||||||
|
# Копия этой функции есть в каждом навыке-эмиттере (навыки автономны). Держать
|
||||||
|
# копии одинаковыми — сознательно: разошедшиеся копии сводят на нет весь смысл.
|
||||||
|
#
|
||||||
|
# Модуль .bsl сюда НЕ идёт — он пишется отдельно.
|
||||||
|
function Write-XmlFile([string]$path, [string]$text, $encoding) {
|
||||||
|
$t = ($text -replace "`r`n", "`n") -replace "`n", "`r`n"
|
||||||
|
[System.IO.File]::WriteAllText($path, $t.TrimEnd("`r", "`n"), $encoding)
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-XmlFile $formMetaPath $formMetaXml $encBom
|
||||||
|
|
||||||
# --- 3b. Form.xml ---
|
# --- 3b. Form.xml ---
|
||||||
|
|
||||||
$formXmlPath = Join-Path $formExtDir "Form.xml"
|
$formXmlPath = Join-Path $formExtDir "Form.xml"
|
||||||
|
|
||||||
$formNsDecl = 'xmlns="http://v8.1c.ru/8.3/xcf/logform" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core" xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
|
|
||||||
|
|
||||||
if ($Purpose -eq "List" -or $Purpose -eq "Choice") {
|
if ($Purpose -eq "List" -or $Purpose -eq "Choice") {
|
||||||
# Динамический список
|
# Динамический список
|
||||||
# MainTable: тип.имя
|
# MainTable: тип.имя
|
||||||
@@ -349,7 +402,7 @@ if ($Purpose -eq "List" -or $Purpose -eq "Choice") {
|
|||||||
|
|
||||||
$formXml = @"
|
$formXml = @"
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<Form $formNsDecl version="$($script:formatVersion)">
|
<Form $($script:formNsDecl) version="$($script:formatVersion)">
|
||||||
<AutoCommandBar name="ФормаКоманднаяПанель" id="-1">
|
<AutoCommandBar name="ФормаКоманднаяПанель" id="-1">
|
||||||
<Autofill>true</Autofill>
|
<Autofill>true</Autofill>
|
||||||
</AutoCommandBar>
|
</AutoCommandBar>
|
||||||
@@ -374,7 +427,7 @@ if ($Purpose -eq "List" -or $Purpose -eq "Choice") {
|
|||||||
|
|
||||||
$formXml = @"
|
$formXml = @"
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<Form $formNsDecl version="$($script:formatVersion)">
|
<Form $($script:formNsDecl) version="$($script:formatVersion)">
|
||||||
<AutoCommandBar name="ФормаКоманднаяПанель" id="-1">
|
<AutoCommandBar name="ФормаКоманднаяПанель" id="-1">
|
||||||
<Autofill>true</Autofill>
|
<Autofill>true</Autofill>
|
||||||
</AutoCommandBar>
|
</AutoCommandBar>
|
||||||
@@ -421,7 +474,7 @@ if ($Purpose -eq "List" -or $Purpose -eq "Choice") {
|
|||||||
|
|
||||||
$formXml = @"
|
$formXml = @"
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<Form $formNsDecl version="$($script:formatVersion)">
|
<Form $($script:formNsDecl) version="$($script:formatVersion)">
|
||||||
<AutoCommandBar name="ФормаКоманднаяПанель" id="-1">
|
<AutoCommandBar name="ФормаКоманднаяПанель" id="-1">
|
||||||
<Autofill>true</Autofill>
|
<Autofill>true</Autofill>
|
||||||
</AutoCommandBar>
|
</AutoCommandBar>
|
||||||
@@ -441,7 +494,7 @@ if ($Purpose -eq "List" -or $Purpose -eq "Choice") {
|
|||||||
if (Test-Path $formXmlPath) {
|
if (Test-Path $formXmlPath) {
|
||||||
Write-Host "[SKIP] Form.xml already exists: $formXmlPath — not overwriting"
|
Write-Host "[SKIP] Form.xml already exists: $formXmlPath — not overwriting"
|
||||||
} else {
|
} else {
|
||||||
[System.IO.File]::WriteAllText($formXmlPath, $formXml, $encBom)
|
Write-XmlFile $formXmlPath $formXml $encBom
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- 3c. Module.bsl ---
|
# --- 3c. Module.bsl ---
|
||||||
@@ -473,6 +526,11 @@ $moduleBsl = @"
|
|||||||
if (Test-Path $modulePath) {
|
if (Test-Path $modulePath) {
|
||||||
Write-Host "[SKIP] Module.bsl already exists: $modulePath — not overwriting"
|
Write-Host "[SKIP] Module.bsl already exists: $modulePath — not overwriting"
|
||||||
} else {
|
} else {
|
||||||
|
# Модуль пишем в каноне платформы: CRLF в разделителях строк (корпус: 2643 CRLF,
|
||||||
|
# чисто-LF 0 из 3001). Хвостовой перевод НЕ навязываем — у платформы он
|
||||||
|
# неканоничен (1235 модулей с ним, 766 без). Шаблон берёт переводы строк из
|
||||||
|
# самого скрипта, а он в репозитории хранится с LF.
|
||||||
|
$moduleBsl = ($moduleBsl -replace "`r`n", "`n") -replace "`n", "`r`n"
|
||||||
[System.IO.File]::WriteAllText($modulePath, $moduleBsl, $encBom)
|
[System.IO.File]::WriteAllText($modulePath, $moduleBsl, $encBom)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -582,12 +640,27 @@ if ($SetDefault -or $isFirstFormForPurpose) {
|
|||||||
$settings = New-Object System.Xml.XmlWriterSettings
|
$settings = New-Object System.Xml.XmlWriterSettings
|
||||||
$settings.Encoding = $encBom
|
$settings.Encoding = $encBom
|
||||||
$settings.Indent = $false
|
$settings.Indent = $false
|
||||||
|
$settings.NewLineHandling = [System.Xml.NewLineHandling]::None
|
||||||
|
|
||||||
$stream = New-Object System.IO.FileStream($objectXmlFull.Path, [System.IO.FileMode]::Create)
|
# Через MemoryStream, а не прямо в файл: нужен шаг пост-обработки строки.
|
||||||
$writer = [System.Xml.XmlWriter]::Create($stream, $settings)
|
$memStream = New-Object System.IO.MemoryStream
|
||||||
|
$writer = [System.Xml.XmlWriter]::Create($memStream, $settings)
|
||||||
$xmlDoc.Save($writer)
|
$xmlDoc.Save($writer)
|
||||||
$writer.Close()
|
$writer.Flush(); $writer.Close()
|
||||||
$stream.Close()
|
|
||||||
|
$xmlText = [System.Text.Encoding]::UTF8.GetString($memStream.ToArray())
|
||||||
|
$memStream.Close()
|
||||||
|
if ($xmlText.Length -gt 0 -and $xmlText[0] -eq [char]0xFEFF) { $xmlText = $xmlText.Substring(1) }
|
||||||
|
$xmlText = $xmlText.Replace('encoding="utf-8"', 'encoding="UTF-8"')
|
||||||
|
# Пустой элемент: XmlWriter отдаёт `<a />`, Конфигуратор пишет `<a/>`. Внутри
|
||||||
|
# CDATA/комментария ` />` может быть содержимым (там `>` не экранируется),
|
||||||
|
# поэтому они идут первыми ветками альтернации и возвращаются как есть.
|
||||||
|
$xmlText = [regex]::Replace($xmlText, '(?s)<!\[CDATA\[.*?\]\]>|<!--.*?-->|(?<=\S) />', { param($m) if ($m.Value -eq ' />') { '/>' } else { $m.Value } })
|
||||||
|
# Целевой перевод строки: стиль файла-назначения — правка наследует его (#44/#46/#47),
|
||||||
|
# новый файл получает канон выгрузки CRLF. Зеркало _detect_xml_style в py-порту.
|
||||||
|
$targetEol = if ((Test-Path -LiteralPath $objectXmlFull.Path) -and ([System.IO.File]::ReadAllText($objectXmlFull.Path) -notmatch "`r`n")) { "`n" } else { "`r`n" }
|
||||||
|
$xmlText = ($xmlText -replace "`r`n", "`n") -replace "`n", $targetEol
|
||||||
|
[System.IO.File]::WriteAllText($objectXmlFull.Path, $xmlText, $encBom)
|
||||||
|
|
||||||
# --- Фаза 5: Вывод ---
|
# --- Фаза 5: Вывод ---
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# form-add v1.11 — Add managed form to 1C config object
|
# form-add v1.25 — Add managed form to 1C config object (+write_xml_file/write_utf8_bom: общий эталон записи)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -11,6 +11,28 @@ import uuid
|
|||||||
|
|
||||||
from lxml import etree
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# Support guard (Ext/ParentConfigurations.bin) — see docs/1c-support-state-spec.md
|
# Support guard (Ext/ParentConfigurations.bin) — see docs/1c-support-state-spec.md
|
||||||
@@ -196,6 +218,16 @@ NSMAP = {
|
|||||||
|
|
||||||
def detect_format_version(d):
|
def detect_format_version(d):
|
||||||
while d:
|
while d:
|
||||||
|
# Автономная внешняя обработка/отчёт: своего Configuration.xml у неё нет, версию несёт
|
||||||
|
# корень самой обработки. Без этого форма и макет внутри обработки 2.21 писались бы 2.17.
|
||||||
|
ext_path = d + ".xml"
|
||||||
|
if os.path.isfile(ext_path):
|
||||||
|
with open(ext_path, "r", encoding="utf-8-sig") as f:
|
||||||
|
ext_head = f.read(2000)
|
||||||
|
if re.search(r'<(ExternalDataProcessor|ExternalReport)[ >]', ext_head):
|
||||||
|
m = re.search(r'<MetaDataObject[^>]+version="(\d+\.\d+)"', ext_head)
|
||||||
|
if m:
|
||||||
|
return m.group(1)
|
||||||
cfg_path = os.path.join(d, "Configuration.xml")
|
cfg_path = os.path.join(d, "Configuration.xml")
|
||||||
if os.path.isfile(cfg_path):
|
if os.path.isfile(cfg_path):
|
||||||
with open(cfg_path, "r", encoding="utf-8-sig") as f:
|
with open(cfg_path, "r", encoding="utf-8-sig") as f:
|
||||||
@@ -210,6 +242,12 @@ def detect_format_version(d):
|
|||||||
return "2.17"
|
return "2.17"
|
||||||
|
|
||||||
|
|
||||||
|
def format_rank(ver):
|
||||||
|
""""2.20" → 220, "2.9" → 209. Строковое сравнение неверно ("2.9" > "2.17")."""
|
||||||
|
m = re.match(r'^(\d+)\.(\d+)$', ver or '')
|
||||||
|
return int(m.group(1)) * 100 + int(m.group(2)) if m else 0
|
||||||
|
|
||||||
|
|
||||||
def _detect_xml_style(path):
|
def _detect_xml_style(path):
|
||||||
"""Стиль существующего файла для round-trip-сохранения: BOM / EOL / регистр encoding /
|
"""Стиль существующего файла для round-trip-сохранения: BOM / EOL / регистр encoding /
|
||||||
финальный перенос. None → файл новый (сохранить текущее поведение)."""
|
финальный перенос. None → файл новый (сохранить текущее поведение)."""
|
||||||
@@ -227,21 +265,22 @@ def _detect_xml_style(path):
|
|||||||
|
|
||||||
|
|
||||||
def _finalize_xml_bytes(xml_bytes, style):
|
def _finalize_xml_bytes(xml_bytes, style):
|
||||||
"""Привести сериализованные байты к стилю оригинала (или к дефолту, если style is None)."""
|
"""Привести байты к стилю оригинала; для НОВОГО файла (style is None) — к канону
|
||||||
enc_decl = style["enc"] if style else "utf-8"
|
выгрузки Конфигуратора: encoding="UTF-8", CRLF в разделителях, без перевода в конце."""
|
||||||
|
enc_decl = style["enc"] if style else "UTF-8"
|
||||||
xml_bytes = xml_bytes.replace(
|
xml_bytes = xml_bytes.replace(
|
||||||
b"<?xml version='1.0' encoding='UTF-8'?>",
|
b"<?xml version='1.0' encoding='UTF-8'?>",
|
||||||
b'<?xml version="1.0" encoding="' + enc_decl.encode("ascii") + b'"?>')
|
b'<?xml version="1.0" encoding="' + enc_decl.encode("ascii") + b'"?>')
|
||||||
# Канонизировать переносы к LF (убирает от \r в tail'ах)
|
# Канонизировать переносы к LF (убирает от \r в tail'ах)
|
||||||
xml_bytes = (xml_bytes.replace(b" \n", b"\n").replace(b" ", b"")
|
xml_bytes = (xml_bytes.replace(b" \n", b"\n").replace(b" ", b"")
|
||||||
.replace(b"\r\n", b"\n").replace(b"\r", b"\n"))
|
.replace(b"\r\n", b"\n").replace(b"\r", b"\n"))
|
||||||
# Финальный перенос — как в оригинале (новый файл → есть)
|
# Финальный перенос — как в оригинале (новый файл → нет, канон #57)
|
||||||
want_final_nl = style["final_nl"] if style else True
|
want_final_nl = style["final_nl"] if style else False
|
||||||
xml_bytes = xml_bytes.rstrip(b"\n")
|
xml_bytes = xml_bytes.rstrip(b"\n")
|
||||||
if want_final_nl:
|
if want_final_nl:
|
||||||
xml_bytes += b"\n"
|
xml_bytes += b"\n"
|
||||||
# EOL — как в оригинале (новый файл → LF, текущее поведение)
|
# EOL — как в оригинале (новый файл → CRLF, канон #57)
|
||||||
if style and style["crlf"]:
|
if (style["crlf"] if style else True):
|
||||||
xml_bytes = xml_bytes.replace(b"\n", b"\r\n")
|
xml_bytes = xml_bytes.replace(b"\n", b"\r\n")
|
||||||
return xml_bytes
|
return xml_bytes
|
||||||
|
|
||||||
@@ -257,10 +296,24 @@ def save_xml_with_bom(tree, path):
|
|||||||
f.write(xml_bytes)
|
f.write(xml_bytes)
|
||||||
|
|
||||||
|
|
||||||
def write_text_with_bom(path, text):
|
def write_utf8_bom(path, content):
|
||||||
"""Write text to file with UTF-8 BOM."""
|
# newline='' — без трансляции: иначе текстовый режим Python дал бы CRLF на Windows
|
||||||
with open(path, "w", encoding="utf-8-sig") as f:
|
# и LF на macOS, то есть вывод навыка зависел бы от ОС.
|
||||||
f.write(text)
|
with open(path, 'w', encoding='utf-8-sig', newline='') as f:
|
||||||
|
f.write(content)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def write_xml_file(path, content):
|
||||||
|
"""XML в каноне выгрузки Конфигуратора: CRLF в разделителях, без перевода в конце.
|
||||||
|
|
||||||
|
Копия этой функции есть в каждом навыке-эмиттере (навыки автономны). Держать
|
||||||
|
копии одинаковыми — сознательно: разошедшиеся копии сводят на нет весь смысл.
|
||||||
|
|
||||||
|
Модуль .bsl сюда НЕ идёт — он пишется отдельно.
|
||||||
|
"""
|
||||||
|
text = content.replace('\r\n', '\n').replace('\n', '\r\n').rstrip('\r\n')
|
||||||
|
write_utf8_bom(path, text)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
@@ -272,7 +325,7 @@ def main():
|
|||||||
parser.add_argument("-Synonym", default=None)
|
parser.add_argument("-Synonym", default=None)
|
||||||
parser.add_argument("-Purpose", default="Object")
|
parser.add_argument("-Purpose", default="Object")
|
||||||
parser.add_argument("-SetDefault", action="store_true")
|
parser.add_argument("-SetDefault", action="store_true")
|
||||||
args = parser.parse_args()
|
args = ci_parse_args(parser)
|
||||||
|
|
||||||
object_path = args.ObjectPath
|
object_path = args.ObjectPath
|
||||||
form_name = args.FormName
|
form_name = args.FormName
|
||||||
@@ -299,7 +352,64 @@ def main():
|
|||||||
|
|
||||||
object_xml_full = os.path.abspath(object_path)
|
object_xml_full = os.path.abspath(object_path)
|
||||||
assert_edit_allowed(object_xml_full, "editable")
|
assert_edit_allowed(object_xml_full, "editable")
|
||||||
format_version = detect_format_version(os.path.dirname(object_xml_full))
|
# Версию берём прежде всего из корня самого объекта — он её несёт всегда, а у автономной
|
||||||
|
# внешней обработки/отчёта подниматься к Configuration.xml просто некуда.
|
||||||
|
format_version = None
|
||||||
|
with open(object_xml_full, "r", encoding="utf-8-sig") as f:
|
||||||
|
obj_head = f.read(2000)
|
||||||
|
m_ver = re.search(r'<MetaDataObject[^>]+version="(\d+\.\d+)"', obj_head)
|
||||||
|
if m_ver:
|
||||||
|
format_version = m_ver.group(1)
|
||||||
|
if not format_version:
|
||||||
|
format_version = detect_format_version(os.path.dirname(object_xml_full))
|
||||||
|
|
||||||
|
# Объявления пространств имён — одной переменной на корень: места эмиссии их только
|
||||||
|
# подставляют. Правки шапки (как xmlns:pal в формате 2.21) делаются здесь, в одном месте.
|
||||||
|
xmlns_decl = (
|
||||||
|
'xmlns="http://v8.1c.ru/8.3/MDClasses"'
|
||||||
|
' xmlns:app="http://v8.1c.ru/8.2/managed-application/core"'
|
||||||
|
' xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config"'
|
||||||
|
' xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi"'
|
||||||
|
' xmlns:ent="http://v8.1c.ru/8.1/data/enterprise"'
|
||||||
|
' xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform"'
|
||||||
|
' xmlns:style="http://v8.1c.ru/8.1/data/ui/style"'
|
||||||
|
' xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system"'
|
||||||
|
' xmlns:v8="http://v8.1c.ru/8.1/data/core"'
|
||||||
|
' xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"'
|
||||||
|
' xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web"'
|
||||||
|
' xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows"'
|
||||||
|
' xmlns:xen="http://v8.1c.ru/8.3/xcf/enums"'
|
||||||
|
' xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef"'
|
||||||
|
' xmlns:xr="http://v8.1c.ru/8.3/xcf/readable"'
|
||||||
|
' xmlns:xs="http://www.w3.org/2001/XMLSchema"'
|
||||||
|
' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
|
||||||
|
)
|
||||||
|
form_ns_decl = (
|
||||||
|
'xmlns="http://v8.1c.ru/8.3/xcf/logform"'
|
||||||
|
' xmlns:app="http://v8.1c.ru/8.2/managed-application/core"'
|
||||||
|
' xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config"'
|
||||||
|
' xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core"'
|
||||||
|
' xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings"'
|
||||||
|
' xmlns:ent="http://v8.1c.ru/8.1/data/enterprise"'
|
||||||
|
' xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform"'
|
||||||
|
' xmlns:style="http://v8.1c.ru/8.1/data/ui/style"'
|
||||||
|
' xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system"'
|
||||||
|
' xmlns:v8="http://v8.1c.ru/8.1/data/core"'
|
||||||
|
' xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"'
|
||||||
|
' xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web"'
|
||||||
|
' xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows"'
|
||||||
|
' xmlns:xr="http://v8.1c.ru/8.3/xcf/readable"'
|
||||||
|
' xmlns:xs="http://www.w3.org/2001/XMLSchema"'
|
||||||
|
' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
|
||||||
|
)
|
||||||
|
|
||||||
|
# 2.21 (8.5) добавила в шапку пространство палитры — ради <Color> у значений перечисления.
|
||||||
|
# Вставляем НА МЕСТО (после lf, перед style): платформа держит объявления по алфавиту,
|
||||||
|
# дописать в конец нельзя.
|
||||||
|
if format_rank(format_version) >= 221:
|
||||||
|
pal = ' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette" xmlns:style='
|
||||||
|
xmlns_decl = xmlns_decl.replace(' xmlns:style=', pal)
|
||||||
|
form_ns_decl = form_ns_decl.replace(' xmlns:style=', pal)
|
||||||
|
|
||||||
parser_xml = etree.XMLParser(remove_blank_text=False)
|
parser_xml = etree.XMLParser(remove_blank_text=False)
|
||||||
tree = etree.parse(object_xml_full, parser_xml)
|
tree = etree.parse(object_xml_full, parser_xml)
|
||||||
@@ -388,24 +498,7 @@ def main():
|
|||||||
|
|
||||||
form_meta_xml = (
|
form_meta_xml = (
|
||||||
'<?xml version="1.0" encoding="UTF-8"?>\n'
|
'<?xml version="1.0" encoding="UTF-8"?>\n'
|
||||||
'<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses"'
|
f'<MetaDataObject {xmlns_decl} version="{format_version}">\n'
|
||||||
' xmlns:app="http://v8.1c.ru/8.2/managed-application/core"'
|
|
||||||
' xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config"'
|
|
||||||
' xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi"'
|
|
||||||
' xmlns:ent="http://v8.1c.ru/8.1/data/enterprise"'
|
|
||||||
' xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform"'
|
|
||||||
' xmlns:style="http://v8.1c.ru/8.1/data/ui/style"'
|
|
||||||
' xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system"'
|
|
||||||
' xmlns:v8="http://v8.1c.ru/8.1/data/core"'
|
|
||||||
' xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"'
|
|
||||||
' xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web"'
|
|
||||||
' xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows"'
|
|
||||||
' xmlns:xen="http://v8.1c.ru/8.3/xcf/enums"'
|
|
||||||
' xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef"'
|
|
||||||
' xmlns:xr="http://v8.1c.ru/8.3/xcf/readable"'
|
|
||||||
' xmlns:xs="http://www.w3.org/2001/XMLSchema"'
|
|
||||||
' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
|
|
||||||
f' version="{format_version}">\n'
|
|
||||||
f'\t<Form uuid="{form_uuid}">\n'
|
f'\t<Form uuid="{form_uuid}">\n'
|
||||||
'\t\t<Properties>\n'
|
'\t\t<Properties>\n'
|
||||||
f'\t\t\t<Name>{form_name}</Name>\n'
|
f'\t\t\t<Name>{form_name}</Name>\n'
|
||||||
@@ -422,37 +515,22 @@ def main():
|
|||||||
'\t\t\t\t<v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value>\n'
|
'\t\t\t\t<v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value>\n'
|
||||||
'\t\t\t\t<v8:Value xsi:type="app:ApplicationUsePurpose">MobilePlatformApplication</v8:Value>\n'
|
'\t\t\t\t<v8:Value xsi:type="app:ApplicationUsePurpose">MobilePlatformApplication</v8:Value>\n'
|
||||||
'\t\t\t</UsePurposes>\n'
|
'\t\t\t</UsePurposes>\n'
|
||||||
|
# Использование в режиме совместимости интерфейса — свойство формата 2.21 (8.5),
|
||||||
|
# сразу после UsePurposes (проверено по выгрузке 8.5, до ExtendedPresentation).
|
||||||
|
+ ('\t\t\t<UseInInterfaceCompatibilityMode>Any</UseInInterfaceCompatibilityMode>\n'
|
||||||
|
if format_rank(format_version) >= 221 else '')
|
||||||
+ ('\t\t\t<ExtendedPresentation/>\n' if object_type in processor_like_types else '')
|
+ ('\t\t\t<ExtendedPresentation/>\n' if object_type in processor_like_types else '')
|
||||||
+ '\t\t</Properties>\n'
|
+ '\t\t</Properties>\n'
|
||||||
'\t</Form>\n'
|
'\t</Form>\n'
|
||||||
'</MetaDataObject>'
|
'</MetaDataObject>'
|
||||||
)
|
)
|
||||||
|
|
||||||
write_text_with_bom(form_meta_path, form_meta_xml)
|
write_xml_file(form_meta_path, form_meta_xml)
|
||||||
|
|
||||||
# --- 3b. Form.xml ---
|
# --- 3b. Form.xml ---
|
||||||
|
|
||||||
form_xml_path = os.path.join(form_ext_dir, "Form.xml")
|
form_xml_path = os.path.join(form_ext_dir, "Form.xml")
|
||||||
|
|
||||||
form_ns_decl = (
|
|
||||||
'xmlns="http://v8.1c.ru/8.3/xcf/logform"'
|
|
||||||
' xmlns:app="http://v8.1c.ru/8.2/managed-application/core"'
|
|
||||||
' xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config"'
|
|
||||||
' xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core"'
|
|
||||||
' xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings"'
|
|
||||||
' xmlns:ent="http://v8.1c.ru/8.1/data/enterprise"'
|
|
||||||
' xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform"'
|
|
||||||
' xmlns:style="http://v8.1c.ru/8.1/data/ui/style"'
|
|
||||||
' xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system"'
|
|
||||||
' xmlns:v8="http://v8.1c.ru/8.1/data/core"'
|
|
||||||
' xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"'
|
|
||||||
' xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web"'
|
|
||||||
' xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows"'
|
|
||||||
' xmlns:xr="http://v8.1c.ru/8.3/xcf/readable"'
|
|
||||||
' xmlns:xs="http://www.w3.org/2001/XMLSchema"'
|
|
||||||
' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
|
|
||||||
)
|
|
||||||
|
|
||||||
if purpose in ("List", "Choice"):
|
if purpose in ("List", "Choice"):
|
||||||
# Dynamic list
|
# Dynamic list
|
||||||
main_table = f"{object_type}.{object_name}"
|
main_table = f"{object_type}.{object_name}"
|
||||||
@@ -551,7 +629,7 @@ def main():
|
|||||||
if os.path.exists(form_xml_path):
|
if os.path.exists(form_xml_path):
|
||||||
print(f"[SKIP] Form.xml already exists: {form_xml_path} — not overwriting")
|
print(f"[SKIP] Form.xml already exists: {form_xml_path} — not overwriting")
|
||||||
else:
|
else:
|
||||||
write_text_with_bom(form_xml_path, form_xml)
|
write_xml_file(form_xml_path, form_xml)
|
||||||
|
|
||||||
# --- 3c. Module.bsl ---
|
# --- 3c. Module.bsl ---
|
||||||
|
|
||||||
@@ -582,7 +660,10 @@ def main():
|
|||||||
if os.path.exists(module_path):
|
if os.path.exists(module_path):
|
||||||
print(f"[SKIP] Module.bsl already exists: {module_path} — not overwriting")
|
print(f"[SKIP] Module.bsl already exists: {module_path} — not overwriting")
|
||||||
else:
|
else:
|
||||||
write_text_with_bom(module_path, module_bsl)
|
# Модуль пишем в каноне платформы: CRLF в разделителях строк (корпус: 2643 CRLF,
|
||||||
|
# чисто-LF 0 из 3001). Хвостовой перевод НЕ навязываем — у платформы он
|
||||||
|
# неканоничен (1235 модулей с ним, 766 без).
|
||||||
|
write_utf8_bom(module_path, module_bsl.replace('\r\n', '\n').replace('\n', '\r\n'))
|
||||||
|
|
||||||
# --- Phase 4: Register in parent object ---
|
# --- Phase 4: Register in parent object ---
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# form-compile v1.175 — Compile 1C managed form from JSON or object metadata
|
# form-compile v1.192 — Compile 1C managed form from JSON or object metadata (гвард на группу additionalColumns без ключа columns)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[string]$JsonPath,
|
[string]$JsonPath,
|
||||||
@@ -1335,9 +1335,20 @@ function Generate-ChartOfAccountsChoiceDSL($meta, [hashtable]$presetData) {
|
|||||||
function Detect-FormatVersion([string]$dir) {
|
function Detect-FormatVersion([string]$dir) {
|
||||||
$d = $dir
|
$d = $dir
|
||||||
while ($d) {
|
while ($d) {
|
||||||
|
# Автономная внешняя обработка/отчёт: своего Configuration.xml у неё нет, версию несёт
|
||||||
|
# корень самой обработки. Без этого форма и макет внутри обработки 2.21 писались бы 2.17.
|
||||||
|
$extPath = "$d.xml"
|
||||||
|
if (Test-Path $extPath) {
|
||||||
|
$extText = [System.IO.File]::ReadAllText($extPath, [System.Text.Encoding]::UTF8)
|
||||||
|
$extHead = $extText.Substring(0, [Math]::Min(2000, $extText.Length))
|
||||||
|
if ($extHead -match '<(ExternalDataProcessor|ExternalReport)[ >]' -and $extHead -match '<MetaDataObject[^>]+version="(\d+\.\d+)"') { return $Matches[1] }
|
||||||
|
}
|
||||||
$cfgPath = Join-Path $d "Configuration.xml"
|
$cfgPath = Join-Path $d "Configuration.xml"
|
||||||
if (Test-Path $cfgPath) {
|
if (Test-Path $cfgPath) {
|
||||||
$head = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8).Substring(0, [Math]::Min(2000, (Get-Item $cfgPath).Length))
|
$cfgText = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8)
|
||||||
|
# Длину среза берём по СТРОКЕ, а не по размеру файла: размер в БАЙТАХ, Substring считает
|
||||||
|
# СИМВОЛЫ, и на кириллице байт больше — короткий Configuration.xml ронял навык исключением.
|
||||||
|
$head = $cfgText.Substring(0, [Math]::Min(2000, $cfgText.Length))
|
||||||
if ($head -match '<MetaDataObject[^>]+version="(\d+\.\d+)"') { return $Matches[1] }
|
if ($head -match '<MetaDataObject[^>]+version="(\d+\.\d+)"') { return $Matches[1] }
|
||||||
}
|
}
|
||||||
$parent = Split-Path $d -Parent
|
$parent = Split-Path $d -Parent
|
||||||
@@ -1347,6 +1358,13 @@ function Detect-FormatVersion([string]$dir) {
|
|||||||
return "2.17"
|
return "2.17"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Версия формата как число для сравнений: "2.20" → 220, "2.9" → 209.
|
||||||
|
# Строковое сравнение здесь неверно ("2.9" > "2.17" лексикографически) — известная ловушка.
|
||||||
|
function Get-FormatRank([string]$ver) {
|
||||||
|
if ($ver -match '^(\d+)\.(\d+)$') { return [int]$Matches[1] * 100 + [int]$Matches[2] }
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
# --- Support guard (Ext/ParentConfigurations.bin) ---
|
# --- Support guard (Ext/ParentConfigurations.bin) ---
|
||||||
# See docs/1c-support-state-spec.md. Blocks edits of vendor objects "на замке" /
|
# See docs/1c-support-state-spec.md. Blocks edits of vendor objects "на замке" /
|
||||||
# read-only configs unless allowed. Trigger = bin present; reaction from
|
# read-only configs unless allowed. Trigger = bin present; reaction from
|
||||||
@@ -1482,6 +1500,17 @@ $script:outPathResolved = if ([System.IO.Path]::IsPathRooted($OutputPath)) { $Ou
|
|||||||
Assert-EditAllowed $script:outPathResolved 'editable'
|
Assert-EditAllowed $script:outPathResolved 'editable'
|
||||||
$script:formatVersion = Detect-FormatVersion ([System.IO.Path]::GetDirectoryName($script:outPathResolved))
|
$script:formatVersion = Detect-FormatVersion ([System.IO.Path]::GetDirectoryName($script:outPathResolved))
|
||||||
|
|
||||||
|
# Объявления пространств имён — одной переменной: место эмиссии её только интерполирует.
|
||||||
|
# Правки шапки (как xmlns:pal в формате 2.21) делаются здесь, в одном месте.
|
||||||
|
$script:formNsDecl = 'xmlns="http://v8.1c.ru/8.3/xcf/logform" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core" xmlns:dcssch="http://v8.1c.ru/8.1/data-composition-system/schema" xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
|
||||||
|
|
||||||
|
# 2.21 (8.5) добавила в шапку пространство палитры — ради <Color> у значений перечисления.
|
||||||
|
# Вставляем НА МЕСТО (после lf, перед style): платформа держит объявления по алфавиту,
|
||||||
|
# дописать в конец нельзя.
|
||||||
|
if ((Get-FormatRank $script:formatVersion) -ge 221) {
|
||||||
|
$script:formNsDecl = $script:formNsDecl -replace ' xmlns:style=', ' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette" xmlns:style='
|
||||||
|
}
|
||||||
|
|
||||||
# --- 0. Path normalization and mode dispatch ---
|
# --- 0. Path normalization and mode dispatch ---
|
||||||
|
|
||||||
# Form name → purpose mapping
|
# Form name → purpose mapping
|
||||||
@@ -1676,6 +1705,12 @@ function X {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function Esc-Xml {
|
function Esc-Xml {
|
||||||
|
param([string]$s)
|
||||||
|
# Эскейп ЗНАЧЕНИЯ АТРИБУТА: & < > и кавычка — внутри "..." литеральная " невалидна.
|
||||||
|
return $s.Replace('&','&').Replace('<','<').Replace('>','>').Replace('"','"')
|
||||||
|
}
|
||||||
|
|
||||||
|
function Esc-XmlText {
|
||||||
# Экранирование ТЕКСТА элемента (<v8:content>, <Value>): только & < > .
|
# Экранирование ТЕКСТА элемента (<v8:content>, <Value>): только & < > .
|
||||||
# Кавычки/апострофы в тексте экранировать НЕ нужно (1С их не экранирует — пишет литерально);
|
# Кавычки/апострофы в тексте экранировать НЕ нужно (1С их не экранирует — пишет литерально);
|
||||||
# " ломал бы раундтрип. Кавычки спецсимвольны лишь в значениях атрибутов.
|
# " ломал бы раундтрип. Кавычки спецсимвольны лишь в значениях атрибутов.
|
||||||
@@ -1690,14 +1725,14 @@ function Emit-MLItems {
|
|||||||
param($val, [string]$indent)
|
param($val, [string]$indent)
|
||||||
if ($val -is [System.Collections.IDictionary]) {
|
if ($val -is [System.Collections.IDictionary]) {
|
||||||
foreach ($k in $val.Keys) {
|
foreach ($k in $val.Keys) {
|
||||||
X "$indent<v8:item>"; X "$indent`t<v8:lang>$k</v8:lang>"; X "$indent`t<v8:content>$(Esc-Xml "$($val[$k])")</v8:content>"; X "$indent</v8:item>"
|
X "$indent<v8:item>"; X "$indent`t<v8:lang>$k</v8:lang>"; X "$indent`t<v8:content>$(Esc-XmlText "$($val[$k])")</v8:content>"; X "$indent</v8:item>"
|
||||||
}
|
}
|
||||||
} elseif ($val -is [System.Management.Automation.PSCustomObject]) {
|
} elseif ($val -is [System.Management.Automation.PSCustomObject]) {
|
||||||
foreach ($p in $val.PSObject.Properties) {
|
foreach ($p in $val.PSObject.Properties) {
|
||||||
X "$indent<v8:item>"; X "$indent`t<v8:lang>$($p.Name)</v8:lang>"; X "$indent`t<v8:content>$(Esc-Xml "$($p.Value)")</v8:content>"; X "$indent</v8:item>"
|
X "$indent<v8:item>"; X "$indent`t<v8:lang>$($p.Name)</v8:lang>"; X "$indent`t<v8:content>$(Esc-XmlText "$($p.Value)")</v8:content>"; X "$indent</v8:item>"
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
X "$indent<v8:item>"; X "$indent`t<v8:lang>ru</v8:lang>"; X "$indent`t<v8:content>$(Esc-Xml "$val")</v8:content>"; X "$indent</v8:item>"
|
X "$indent<v8:item>"; X "$indent`t<v8:lang>ru</v8:lang>"; X "$indent`t<v8:content>$(Esc-XmlText "$val")</v8:content>"; X "$indent</v8:item>"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1716,7 +1751,7 @@ function Emit-USPresentation {
|
|||||||
param($val, [string]$tag, [string]$indent)
|
param($val, [string]$tag, [string]$indent)
|
||||||
if ($null -eq $val) { return }
|
if ($null -eq $val) { return }
|
||||||
if ($val -is [string]) {
|
if ($val -is [string]) {
|
||||||
X "$indent<$tag xsi:type=`"xs:string`">$(Esc-Xml $val)</$tag>"
|
X "$indent<$tag xsi:type=`"xs:string`">$(Esc-XmlText $val)</$tag>"
|
||||||
} else {
|
} else {
|
||||||
Emit-MLText -tag $tag -text $val -indent $indent -xsiType "v8:LocalStringType"
|
Emit-MLText -tag $tag -text $val -indent $indent -xsiType "v8:LocalStringType"
|
||||||
}
|
}
|
||||||
@@ -1845,10 +1880,10 @@ function Emit-FilterItem {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ($item.presentation) { Emit-USPresentation -val $item.presentation -tag "dcsset:presentation" -indent "$indent`t" }
|
if ($item.presentation) { Emit-USPresentation -val $item.presentation -tag "dcsset:presentation" -indent "$indent`t" }
|
||||||
if ($item.viewMode) { X "$indent`t<dcsset:viewMode>$(Esc-Xml "$($item.viewMode)")</dcsset:viewMode>" }
|
if ($item.viewMode) { X "$indent`t<dcsset:viewMode>$(Esc-XmlText "$($item.viewMode)")</dcsset:viewMode>" }
|
||||||
if ($item.userSettingID) {
|
if ($item.userSettingID) {
|
||||||
$guid = if ("$($item.userSettingID)" -eq "auto") { New-Guid-String } else { "$($item.userSettingID)" }
|
$guid = if ("$($item.userSettingID)" -eq "auto") { New-Guid-String } else { "$($item.userSettingID)" }
|
||||||
X "$indent`t<dcsset:userSettingID>$(Esc-Xml $guid)</dcsset:userSettingID>"
|
X "$indent`t<dcsset:userSettingID>$(Esc-XmlText $guid)</dcsset:userSettingID>"
|
||||||
}
|
}
|
||||||
if ($item.userSettingPresentation) { Emit-USPresentation -val $item.userSettingPresentation -tag "dcsset:userSettingPresentation" -indent "$indent`t" }
|
if ($item.userSettingPresentation) { Emit-USPresentation -val $item.userSettingPresentation -tag "dcsset:userSettingPresentation" -indent "$indent`t" }
|
||||||
X "$indent</dcsset:item>"
|
X "$indent</dcsset:item>"
|
||||||
@@ -1856,10 +1891,10 @@ function Emit-FilterItem {
|
|||||||
}
|
}
|
||||||
X "$indent<dcsset:item xsi:type=`"dcsset:FilterItemComparison`">"
|
X "$indent<dcsset:item xsi:type=`"dcsset:FilterItemComparison`">"
|
||||||
if ($item.use -eq $false) { X "$indent`t<dcsset:use>false</dcsset:use>" }
|
if ($item.use -eq $false) { X "$indent`t<dcsset:use>false</dcsset:use>" }
|
||||||
X "$indent`t<dcsset:left xsi:type=`"dcscor:Field`">$(Esc-Xml "$($item.field)")</dcsset:left>"
|
X "$indent`t<dcsset:left xsi:type=`"dcscor:Field`">$(Esc-XmlText "$($item.field)")</dcsset:left>"
|
||||||
$compType = $script:comparisonTypes["$($item.op)"]
|
$compType = $script:comparisonTypes["$($item.op)"]
|
||||||
if (-not $compType) { $compType = "$($item.op)" }
|
if (-not $compType) { $compType = "$($item.op)" }
|
||||||
X "$indent`t<dcsset:comparisonType>$(Esc-Xml $compType)</dcsset:comparisonType>"
|
X "$indent`t<dcsset:comparisonType>$(Esc-XmlText $compType)</dcsset:comparisonType>"
|
||||||
$valIsArray = ($item.value -is [array]) -or ($item.value -is [System.Collections.IList] -and $item.value -isnot [string])
|
$valIsArray = ($item.value -is [array]) -or ($item.value -is [System.Collections.IList] -and $item.value -isnot [string])
|
||||||
if ($valIsArray) {
|
if ($valIsArray) {
|
||||||
if (@($item.value).Count -eq 0) {
|
if (@($item.value).Count -eq 0) {
|
||||||
@@ -1878,7 +1913,7 @@ function Emit-FilterItem {
|
|||||||
elseif ("$v" -match '^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена|Catalog|Enum|Document|ChartOfAccounts|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.') { $vt = 'dcscor:DesignTimeValue' }
|
elseif ("$v" -match '^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена|Catalog|Enum|Document|ChartOfAccounts|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.') { $vt = 'dcscor:DesignTimeValue' }
|
||||||
else { $vt = 'xs:string' }
|
else { $vt = 'xs:string' }
|
||||||
}
|
}
|
||||||
$vStr = if ($v -is [bool]) { "$v".ToLower() } else { Esc-Xml "$v" }
|
$vStr = if ($v -is [bool]) { "$v".ToLower() } else { Esc-XmlText "$v" }
|
||||||
$nsAttr = Get-ValueTypeNsAttr -valueType $vt -value "$v"
|
$nsAttr = Get-ValueTypeNsAttr -valueType $vt -value "$v"
|
||||||
X "$indent`t<dcsset:right$nsAttr xsi:type=`"$vt`">$vStr</dcsset:right>"
|
X "$indent`t<dcsset:right$nsAttr xsi:type=`"$vt`">$vStr</dcsset:right>"
|
||||||
}
|
}
|
||||||
@@ -1904,8 +1939,8 @@ function Emit-FilterItem {
|
|||||||
$variant = "$sv"; $hasDate = $false; $dateV = $null
|
$variant = "$sv"; $hasDate = $false; $dateV = $null
|
||||||
}
|
}
|
||||||
X "$indent`t<dcsset:right xsi:type=`"v8:$sdType`">"
|
X "$indent`t<dcsset:right xsi:type=`"v8:$sdType`">"
|
||||||
X "$indent`t`t<v8:variant xsi:type=`"v8:${sdType}Variant`">$(Esc-Xml $variant)</v8:variant>"
|
X "$indent`t`t<v8:variant xsi:type=`"v8:${sdType}Variant`">$(Esc-XmlText $variant)</v8:variant>"
|
||||||
if ($hasDate) { X "$indent`t`t<v8:date>$(Esc-Xml $dateV)</v8:date>" }
|
if ($hasDate) { X "$indent`t`t<v8:date>$(Esc-XmlText $dateV)</v8:date>" }
|
||||||
X "$indent`t</dcsset:right>"
|
X "$indent`t</dcsset:right>"
|
||||||
} elseif ("$($item.value)" -eq '_') {
|
} elseif ("$($item.value)" -eq '_') {
|
||||||
# "_" — маркер пустого значения: платформа эмитит пустой self-closing <dcsset:right>
|
# "_" — маркер пустого значения: платформа эмитит пустой self-closing <dcsset:right>
|
||||||
@@ -1923,15 +1958,15 @@ function Emit-FilterItem {
|
|||||||
elseif ("$v" -match '^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена|Catalog|Enum|Document|ChartOfAccounts|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.') { $vt = "dcscor:DesignTimeValue" }
|
elseif ("$v" -match '^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена|Catalog|Enum|Document|ChartOfAccounts|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.') { $vt = "dcscor:DesignTimeValue" }
|
||||||
else { $vt = "xs:string" }
|
else { $vt = "xs:string" }
|
||||||
}
|
}
|
||||||
$vStr = if ($item.value -is [bool]) { "$($item.value)".ToLower() } else { Esc-Xml "$($item.value)" }
|
$vStr = if ($item.value -is [bool]) { "$($item.value)".ToLower() } else { Esc-XmlText "$($item.value)" }
|
||||||
$nsAttr = Get-ValueTypeNsAttr -valueType $vt -value "$($item.value)"
|
$nsAttr = Get-ValueTypeNsAttr -valueType $vt -value "$($item.value)"
|
||||||
X "$indent`t<dcsset:right$nsAttr xsi:type=`"$vt`">$vStr</dcsset:right>"
|
X "$indent`t<dcsset:right$nsAttr xsi:type=`"$vt`">$vStr</dcsset:right>"
|
||||||
}
|
}
|
||||||
if ($item.presentation) { Emit-USPresentation -val $item.presentation -tag "dcsset:presentation" -indent "$indent`t" }
|
if ($item.presentation) { Emit-USPresentation -val $item.presentation -tag "dcsset:presentation" -indent "$indent`t" }
|
||||||
if ($item.viewMode) { X "$indent`t<dcsset:viewMode>$(Esc-Xml "$($item.viewMode)")</dcsset:viewMode>" }
|
if ($item.viewMode) { X "$indent`t<dcsset:viewMode>$(Esc-XmlText "$($item.viewMode)")</dcsset:viewMode>" }
|
||||||
if ($item.userSettingID) {
|
if ($item.userSettingID) {
|
||||||
$uid = if ("$($item.userSettingID)" -eq "auto") { New-Guid-String } else { "$($item.userSettingID)" }
|
$uid = if ("$($item.userSettingID)" -eq "auto") { New-Guid-String } else { "$($item.userSettingID)" }
|
||||||
X "$indent`t<dcsset:userSettingID>$(Esc-Xml $uid)</dcsset:userSettingID>"
|
X "$indent`t<dcsset:userSettingID>$(Esc-XmlText $uid)</dcsset:userSettingID>"
|
||||||
}
|
}
|
||||||
if ($item.userSettingPresentation) { Emit-USPresentation -val $item.userSettingPresentation -tag "dcsset:userSettingPresentation" -indent "$indent`t" }
|
if ($item.userSettingPresentation) { Emit-USPresentation -val $item.userSettingPresentation -tag "dcsset:userSettingPresentation" -indent "$indent`t" }
|
||||||
X "$indent</dcsset:item>"
|
X "$indent</dcsset:item>"
|
||||||
@@ -1955,10 +1990,10 @@ function Emit-Filter {
|
|||||||
Emit-FilterItem -item ([pscustomobject]$obj) -indent "$indent`t"
|
Emit-FilterItem -item ([pscustomobject]$obj) -indent "$indent`t"
|
||||||
} else { Emit-FilterItem -item $item -indent "$indent`t" }
|
} else { Emit-FilterItem -item $item -indent "$indent`t" }
|
||||||
}
|
}
|
||||||
if ($null -ne $blockViewMode) { X "$indent`t<dcsset:viewMode>$(Esc-Xml "$blockViewMode")</dcsset:viewMode>" }
|
if ($null -ne $blockViewMode) { X "$indent`t<dcsset:viewMode>$(Esc-XmlText "$blockViewMode")</dcsset:viewMode>" }
|
||||||
if ($null -ne $blockUserSettingID) {
|
if ($null -ne $blockUserSettingID) {
|
||||||
$uid = if ("$blockUserSettingID" -eq 'auto') { New-Guid-String } else { "$blockUserSettingID" }
|
$uid = if ("$blockUserSettingID" -eq 'auto') { New-Guid-String } else { "$blockUserSettingID" }
|
||||||
X "$indent`t<dcsset:userSettingID>$(Esc-Xml $uid)</dcsset:userSettingID>"
|
X "$indent`t<dcsset:userSettingID>$(Esc-XmlText $uid)</dcsset:userSettingID>"
|
||||||
}
|
}
|
||||||
if ($null -ne $blockUserSettingPresentation) { Emit-USPresentation -val $blockUserSettingPresentation -tag "dcsset:userSettingPresentation" -indent "$indent`t" }
|
if ($null -ne $blockUserSettingPresentation) { Emit-USPresentation -val $blockUserSettingPresentation -tag "dcsset:userSettingPresentation" -indent "$indent`t" }
|
||||||
X "$indent</dcsset:filter>"
|
X "$indent</dcsset:filter>"
|
||||||
@@ -1980,7 +2015,7 @@ function Emit-Order {
|
|||||||
if ($parts.Count -gt 1 -and $parts[1] -match '^(?i)(desc|убыв)') { $dir = "Desc" }
|
if ($parts.Count -gt 1 -and $parts[1] -match '^(?i)(desc|убыв)') { $dir = "Desc" }
|
||||||
elseif ($parts.Count -gt 1 -and $parts[1] -match '^(?i)(asc|возр)') { $dir = "Asc" }
|
elseif ($parts.Count -gt 1 -and $parts[1] -match '^(?i)(asc|возр)') { $dir = "Asc" }
|
||||||
X "$indent`t<dcsset:item xsi:type=`"dcsset:OrderItemField`">"
|
X "$indent`t<dcsset:item xsi:type=`"dcsset:OrderItemField`">"
|
||||||
X "$indent`t`t<dcsset:field>$(Esc-Xml $field)</dcsset:field>"
|
X "$indent`t`t<dcsset:field>$(Esc-XmlText $field)</dcsset:field>"
|
||||||
X "$indent`t`t<dcsset:orderType>$dir</dcsset:orderType>"
|
X "$indent`t`t<dcsset:orderType>$dir</dcsset:orderType>"
|
||||||
X "$indent`t</dcsset:item>"
|
X "$indent`t</dcsset:item>"
|
||||||
}
|
}
|
||||||
@@ -1990,16 +2025,16 @@ function Emit-Order {
|
|||||||
if ($dir -match '^(?i)(desc|убыв)') { $dir = "Desc" } elseif ($dir -match '^(?i)(asc|возр)') { $dir = "Asc" }
|
if ($dir -match '^(?i)(desc|убыв)') { $dir = "Desc" } elseif ($dir -match '^(?i)(asc|возр)') { $dir = "Asc" }
|
||||||
X "$indent`t<dcsset:item xsi:type=`"dcsset:OrderItemField`">"
|
X "$indent`t<dcsset:item xsi:type=`"dcsset:OrderItemField`">"
|
||||||
if ($item.use -eq $false) { X "$indent`t`t<dcsset:use>false</dcsset:use>" }
|
if ($item.use -eq $false) { X "$indent`t`t<dcsset:use>false</dcsset:use>" }
|
||||||
X "$indent`t`t<dcsset:field>$(Esc-Xml "$($item.field)")</dcsset:field>"
|
X "$indent`t`t<dcsset:field>$(Esc-XmlText "$($item.field)")</dcsset:field>"
|
||||||
X "$indent`t`t<dcsset:orderType>$dir</dcsset:orderType>"
|
X "$indent`t`t<dcsset:orderType>$dir</dcsset:orderType>"
|
||||||
if ($item.viewMode) { X "$indent`t`t<dcsset:viewMode>$(Esc-Xml "$($item.viewMode)")</dcsset:viewMode>" }
|
if ($item.viewMode) { X "$indent`t`t<dcsset:viewMode>$(Esc-XmlText "$($item.viewMode)")</dcsset:viewMode>" }
|
||||||
X "$indent`t</dcsset:item>"
|
X "$indent`t</dcsset:item>"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ($null -ne $blockViewMode) { X "$indent`t<dcsset:viewMode>$(Esc-Xml "$blockViewMode")</dcsset:viewMode>" }
|
if ($null -ne $blockViewMode) { X "$indent`t<dcsset:viewMode>$(Esc-XmlText "$blockViewMode")</dcsset:viewMode>" }
|
||||||
if ($null -ne $blockUserSettingID) {
|
if ($null -ne $blockUserSettingID) {
|
||||||
$uid = if ("$blockUserSettingID" -eq 'auto') { New-Guid-String } else { "$blockUserSettingID" }
|
$uid = if ("$blockUserSettingID" -eq 'auto') { New-Guid-String } else { "$blockUserSettingID" }
|
||||||
X "$indent`t<dcsset:userSettingID>$(Esc-Xml $uid)</dcsset:userSettingID>"
|
X "$indent`t<dcsset:userSettingID>$(Esc-XmlText $uid)</dcsset:userSettingID>"
|
||||||
}
|
}
|
||||||
if ($null -ne $blockUserSettingPresentation) { Emit-USPresentation -val $blockUserSettingPresentation -tag "dcsset:userSettingPresentation" -indent "$indent`t" }
|
if ($null -ne $blockUserSettingPresentation) { Emit-USPresentation -val $blockUserSettingPresentation -tag "dcsset:userSettingPresentation" -indent "$indent`t" }
|
||||||
X "$indent</dcsset:order>"
|
X "$indent</dcsset:order>"
|
||||||
@@ -2031,7 +2066,7 @@ function Emit-AppearanceValue {
|
|||||||
if (_HasKey $val 'items') { $nestedItems = (_Get $val 'items') }
|
if (_HasKey $val 'items') { $nestedItems = (_Get $val 'items') }
|
||||||
}
|
}
|
||||||
if ($useWrapper) { X "$indent`t<dcscor:use>false</dcscor:use>" }
|
if ($useWrapper) { X "$indent`t<dcscor:use>false</dcscor:use>" }
|
||||||
X "$indent`t<dcscor:parameter>$(Esc-Xml $key)</dcscor:parameter>"
|
X "$indent`t<dcscor:parameter>$(Esc-XmlText $key)</dcscor:parameter>"
|
||||||
$isFontDict = $false
|
$isFontDict = $false
|
||||||
if ($innerVal -is [PSCustomObject]) {
|
if ($innerVal -is [PSCustomObject]) {
|
||||||
$tProp = $innerVal.PSObject.Properties['@type']
|
$tProp = $innerVal.PSObject.Properties['@type']
|
||||||
@@ -2047,7 +2082,7 @@ function Emit-AppearanceValue {
|
|||||||
$lg = if (_HasKey $innerVal 'gap') { if ((_Get $innerVal 'gap')) { 'true' } else { 'false' } } else { 'false' }
|
$lg = if (_HasKey $innerVal 'gap') { if ((_Get $innerVal 'gap')) { 'true' } else { 'false' } } else { 'false' }
|
||||||
$ls = if (_HasKey $innerVal 'style') { "$(_Get $innerVal 'style')" } else { 'None' }
|
$ls = if (_HasKey $innerVal 'style') { "$(_Get $innerVal 'style')" } else { 'None' }
|
||||||
X "$indent`t<dcscor:value xsi:type=`"v8ui:Line`" width=`"$lw`" gap=`"$lg`">"
|
X "$indent`t<dcscor:value xsi:type=`"v8ui:Line`" width=`"$lw`" gap=`"$lg`">"
|
||||||
X "$indent`t`t<v8ui:style xsi:type=`"v8ui:SpreadsheetDocumentCellLineType`">$(Esc-Xml $ls)</v8ui:style>"
|
X "$indent`t`t<v8ui:style xsi:type=`"v8ui:SpreadsheetDocumentCellLineType`">$(Esc-XmlText $ls)</v8ui:style>"
|
||||||
X "$indent`t</dcscor:value>"
|
X "$indent`t</dcscor:value>"
|
||||||
} elseif ($isFontDict) {
|
} elseif ($isFontDict) {
|
||||||
$attrParts = @()
|
$attrParts = @()
|
||||||
@@ -2060,7 +2095,7 @@ function Emit-AppearanceValue {
|
|||||||
X "$indent`t<dcscor:value xsi:type=`"v8ui:Font`" $($attrParts -join ' ')/>"
|
X "$indent`t<dcscor:value xsi:type=`"v8ui:Font`" $($attrParts -join ' ')/>"
|
||||||
} elseif ($isDict -and (_HasKey $innerVal 'field')) {
|
} elseif ($isDict -and (_HasKey $innerVal 'field')) {
|
||||||
# Ссылка на поле (dcscor:Field) — значение параметра оформления = поле компоновки
|
# Ссылка на поле (dcscor:Field) — значение параметра оформления = поле компоновки
|
||||||
X "$indent`t<dcscor:value xsi:type=`"dcscor:Field`">$(Esc-Xml "$(_Get $innerVal 'field')")</dcscor:value>"
|
X "$indent`t<dcscor:value xsi:type=`"dcscor:Field`">$(Esc-XmlText "$(_Get $innerVal 'field')")</dcscor:value>"
|
||||||
} elseif ($isDict) {
|
} elseif ($isDict) {
|
||||||
# Локализуемый текст параметра оформления: платформа объявляет xsi:type на dcscor:value
|
# Локализуемый текст параметра оформления: платформа объявляет xsi:type на dcscor:value
|
||||||
Emit-MLText -tag "dcscor:value" -text $innerVal -indent "$indent`t" -xsiType "v8:LocalStringType"
|
Emit-MLText -tag "dcscor:value" -text $innerVal -indent "$indent`t" -xsiType "v8:LocalStringType"
|
||||||
@@ -2075,19 +2110,19 @@ function Emit-AppearanceValue {
|
|||||||
'ТипМакета' = 'dcsset:DataCompositionGroupTemplateType'
|
'ТипМакета' = 'dcsset:DataCompositionGroupTemplateType'
|
||||||
}
|
}
|
||||||
$keyType = $keyTypeMap[$key]
|
$keyType = $keyTypeMap[$key]
|
||||||
if ($keyType) { X "$indent`t<dcscor:value xsi:type=`"$keyType`">$(Esc-Xml $actualVal)</dcscor:value>" }
|
if ($keyType) { X "$indent`t<dcscor:value xsi:type=`"$keyType`">$(Esc-XmlText $actualVal)</dcscor:value>" }
|
||||||
elseif ($actualVal -match '^(style|web|win):') { X "$indent`t<dcscor:value xsi:type=`"v8ui:Color`">$(Esc-Xml $actualVal)</dcscor:value>" }
|
elseif ($actualVal -match '^(style|web|win):') { X "$indent`t<dcscor:value xsi:type=`"v8ui:Color`">$(Esc-XmlText $actualVal)</dcscor:value>" }
|
||||||
elseif ($actualVal -eq "true" -or $actualVal -eq "false") { X "$indent`t<dcscor:value xsi:type=`"xs:boolean`">$actualVal</dcscor:value>" }
|
elseif ($actualVal -eq "true" -or $actualVal -eq "false") { X "$indent`t<dcscor:value xsi:type=`"xs:boolean`">$actualVal</dcscor:value>" }
|
||||||
elseif ($key -eq "Текст" -or $key -eq "Заголовок" -or $key -eq "Формат") {
|
elseif ($key -eq "Текст" -or $key -eq "Заголовок" -or $key -eq "Формат") {
|
||||||
# Текст/Заголовок/Формат: голая строка = плоский xs:string (так платформа хранит
|
# Текст/Заголовок/Формат: голая строка = плоский xs:string (так платформа хранит
|
||||||
# нелокализованный литерал). Локализуемый текст → объект {ru,en} (ветка isDict выше).
|
# нелокализованный литерал). Локализуемый текст → объект {ru,en} (ветка isDict выше).
|
||||||
# Пустая строка → самозакрывающийся тег (как у платформы).
|
# Пустая строка → самозакрывающийся тег (как у платформы).
|
||||||
if ($actualVal -eq '') { X "$indent`t<dcscor:value xsi:type=`"xs:string`"/>" }
|
if ($actualVal -eq '') { X "$indent`t<dcscor:value xsi:type=`"xs:string`"/>" }
|
||||||
else { X "$indent`t<dcscor:value xsi:type=`"xs:string`">$(Esc-Xml $actualVal)</dcscor:value>" }
|
else { X "$indent`t<dcscor:value xsi:type=`"xs:string`">$(Esc-XmlText $actualVal)</dcscor:value>" }
|
||||||
}
|
}
|
||||||
elseif ($actualVal -match '^-?\d+(\.\d+)?$') { X "$indent`t<dcscor:value xsi:type=`"xs:decimal`">$actualVal</dcscor:value>" }
|
elseif ($actualVal -match '^-?\d+(\.\d+)?$') { X "$indent`t<dcscor:value xsi:type=`"xs:decimal`">$actualVal</dcscor:value>" }
|
||||||
elseif ($key -eq 'ЦветТекста' -or $key -eq 'ЦветФона' -or $key -eq 'ЦветГраницы') { X "$indent`t<dcscor:value xsi:type=`"v8ui:Color`">$(Esc-Xml $actualVal)</dcscor:value>" }
|
elseif ($key -eq 'ЦветТекста' -or $key -eq 'ЦветФона' -or $key -eq 'ЦветГраницы') { X "$indent`t<dcscor:value xsi:type=`"v8ui:Color`">$(Esc-XmlText $actualVal)</dcscor:value>" }
|
||||||
else { X "$indent`t<dcscor:value xsi:type=`"xs:string`">$(Esc-Xml $actualVal)</dcscor:value>" }
|
else { X "$indent`t<dcscor:value xsi:type=`"xs:string`">$(Esc-XmlText $actualVal)</dcscor:value>" }
|
||||||
}
|
}
|
||||||
if ($nestedItems) {
|
if ($nestedItems) {
|
||||||
$niProps = if ($nestedItems -is [PSCustomObject]) { $nestedItems.PSObject.Properties } else { $null }
|
$niProps = if ($nestedItems -is [PSCustomObject]) { $nestedItems.PSObject.Properties } else { $null }
|
||||||
@@ -2110,7 +2145,7 @@ function Emit-ConditionalAppearance {
|
|||||||
X "$indent`t`t<dcsset:selection>"
|
X "$indent`t`t<dcsset:selection>"
|
||||||
foreach ($sel in $ca.selection) {
|
foreach ($sel in $ca.selection) {
|
||||||
X "$indent`t`t`t<dcsset:item>"
|
X "$indent`t`t`t<dcsset:item>"
|
||||||
X "$indent`t`t`t`t<dcsset:field>$(Esc-Xml "$sel")</dcsset:field>"
|
X "$indent`t`t`t`t<dcsset:field>$(Esc-XmlText "$sel")</dcsset:field>"
|
||||||
X "$indent`t`t`t</dcsset:item>"
|
X "$indent`t`t`t</dcsset:item>"
|
||||||
}
|
}
|
||||||
X "$indent`t`t</dcsset:selection>"
|
X "$indent`t`t</dcsset:selection>"
|
||||||
@@ -2129,12 +2164,12 @@ function Emit-ConditionalAppearance {
|
|||||||
Emit-MLItems -val $ca.presentation -indent "$indent`t`t`t"
|
Emit-MLItems -val $ca.presentation -indent "$indent`t`t`t"
|
||||||
X "$indent`t`t</dcsset:presentation>"
|
X "$indent`t`t</dcsset:presentation>"
|
||||||
}
|
}
|
||||||
else { X "$indent`t`t<dcsset:presentation xsi:type=`"xs:string`">$(Esc-Xml "$($ca.presentation)")</dcsset:presentation>" }
|
else { X "$indent`t`t<dcsset:presentation xsi:type=`"xs:string`">$(Esc-XmlText "$($ca.presentation)")</dcsset:presentation>" }
|
||||||
}
|
}
|
||||||
if ($ca.viewMode) { X "$indent`t`t<dcsset:viewMode>$(Esc-Xml "$($ca.viewMode)")</dcsset:viewMode>" }
|
if ($ca.viewMode) { X "$indent`t`t<dcsset:viewMode>$(Esc-XmlText "$($ca.viewMode)")</dcsset:viewMode>" }
|
||||||
if ($ca.userSettingID) {
|
if ($ca.userSettingID) {
|
||||||
$uid = if ("$($ca.userSettingID)" -eq "auto") { New-Guid-String } else { "$($ca.userSettingID)" }
|
$uid = if ("$($ca.userSettingID)" -eq "auto") { New-Guid-String } else { "$($ca.userSettingID)" }
|
||||||
X "$indent`t`t<dcsset:userSettingID>$(Esc-Xml $uid)</dcsset:userSettingID>"
|
X "$indent`t`t<dcsset:userSettingID>$(Esc-XmlText $uid)</dcsset:userSettingID>"
|
||||||
}
|
}
|
||||||
if ($ca.userSettingPresentation) { Emit-USPresentation -val $ca.userSettingPresentation -tag "dcsset:userSettingPresentation" -indent "$indent`t`t" }
|
if ($ca.userSettingPresentation) { Emit-USPresentation -val $ca.userSettingPresentation -tag "dcsset:userSettingPresentation" -indent "$indent`t`t" }
|
||||||
if ($ca.useInDontUse -and $ca.useInDontUse.Count -gt 0) {
|
if ($ca.useInDontUse -and $ca.useInDontUse.Count -gt 0) {
|
||||||
@@ -2150,10 +2185,10 @@ function Emit-ConditionalAppearance {
|
|||||||
}
|
}
|
||||||
X "$indent`t</dcsset:item>"
|
X "$indent`t</dcsset:item>"
|
||||||
}
|
}
|
||||||
if ($null -ne $blockViewMode) { X "$indent`t<dcsset:viewMode>$(Esc-Xml "$blockViewMode")</dcsset:viewMode>" }
|
if ($null -ne $blockViewMode) { X "$indent`t<dcsset:viewMode>$(Esc-XmlText "$blockViewMode")</dcsset:viewMode>" }
|
||||||
if ($null -ne $blockUserSettingID) {
|
if ($null -ne $blockUserSettingID) {
|
||||||
$uid = if ("$blockUserSettingID" -eq 'auto') { New-Guid-String } else { "$blockUserSettingID" }
|
$uid = if ("$blockUserSettingID" -eq 'auto') { New-Guid-String } else { "$blockUserSettingID" }
|
||||||
X "$indent`t<dcsset:userSettingID>$(Esc-Xml $uid)</dcsset:userSettingID>"
|
X "$indent`t<dcsset:userSettingID>$(Esc-XmlText $uid)</dcsset:userSettingID>"
|
||||||
}
|
}
|
||||||
if ($null -ne $blockUserSettingPresentation) { Emit-USPresentation -val $blockUserSettingPresentation -tag "dcsset:userSettingPresentation" -indent "$indent`t" }
|
if ($null -ne $blockUserSettingPresentation) { Emit-USPresentation -val $blockUserSettingPresentation -tag "dcsset:userSettingPresentation" -indent "$indent`t" }
|
||||||
X "$indent</$wrapTag>"
|
X "$indent</$wrapTag>"
|
||||||
@@ -2192,14 +2227,14 @@ function Emit-GroupItemField {
|
|||||||
$pae = if ($level.periodAdditionEnd) { "$($level.periodAdditionEnd)" } else { '0001-01-01T00:00:00' }
|
$pae = if ($level.periodAdditionEnd) { "$($level.periodAdditionEnd)" } else { '0001-01-01T00:00:00' }
|
||||||
}
|
}
|
||||||
X "$indent<dcsset:item xsi:type=`"dcsset:GroupItemField`">"
|
X "$indent<dcsset:item xsi:type=`"dcsset:GroupItemField`">"
|
||||||
X "$indent`t<dcsset:field>$(Esc-Xml $field)</dcsset:field>"
|
X "$indent`t<dcsset:field>$(Esc-XmlText $field)</dcsset:field>"
|
||||||
X "$indent`t<dcsset:groupType>$(Esc-Xml $gt)</dcsset:groupType>"
|
X "$indent`t<dcsset:groupType>$(Esc-XmlText $gt)</dcsset:groupType>"
|
||||||
X "$indent`t<dcsset:periodAdditionType>$(Esc-Xml $pat)</dcsset:periodAdditionType>"
|
X "$indent`t<dcsset:periodAdditionType>$(Esc-XmlText $pat)</dcsset:periodAdditionType>"
|
||||||
# Авто-детект: ISO-дата → xs:dateTime, иначе путь → dcscor:Field.
|
# Авто-детект: ISO-дата → xs:dateTime, иначе путь → dcscor:Field.
|
||||||
$pabT = if ($pab -match '^\d{4}-\d{2}-\d{2}T') { 'xs:dateTime' } else { 'dcscor:Field' }
|
$pabT = if ($pab -match '^\d{4}-\d{2}-\d{2}T') { 'xs:dateTime' } else { 'dcscor:Field' }
|
||||||
$paeT = if ($pae -match '^\d{4}-\d{2}-\d{2}T') { 'xs:dateTime' } else { 'dcscor:Field' }
|
$paeT = if ($pae -match '^\d{4}-\d{2}-\d{2}T') { 'xs:dateTime' } else { 'dcscor:Field' }
|
||||||
X "$indent`t<dcsset:periodAdditionBegin xsi:type=`"$pabT`">$(Esc-Xml $pab)</dcsset:periodAdditionBegin>"
|
X "$indent`t<dcsset:periodAdditionBegin xsi:type=`"$pabT`">$(Esc-XmlText $pab)</dcsset:periodAdditionBegin>"
|
||||||
X "$indent`t<dcsset:periodAdditionEnd xsi:type=`"$paeT`">$(Esc-Xml $pae)</dcsset:periodAdditionEnd>"
|
X "$indent`t<dcsset:periodAdditionEnd xsi:type=`"$paeT`">$(Esc-XmlText $pae)</dcsset:periodAdditionEnd>"
|
||||||
X "$indent</dcsset:item>"
|
X "$indent</dcsset:item>"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2268,22 +2303,22 @@ function Emit-CalcFields {
|
|||||||
}
|
}
|
||||||
$ci = "$indent`t"
|
$ci = "$indent`t"
|
||||||
X "$indent<CalculatedField>"
|
X "$indent<CalculatedField>"
|
||||||
X "$ci<dcssch:dataPath>$(Esc-Xml $dataPath)</dcssch:dataPath>"
|
X "$ci<dcssch:dataPath>$(Esc-XmlText $dataPath)</dcssch:dataPath>"
|
||||||
X "$ci<dcssch:expression>$(Esc-Xml $expression)</dcssch:expression>"
|
X "$ci<dcssch:expression>$(Esc-XmlText $expression)</dcssch:expression>"
|
||||||
if ($title) { Emit-MLText -tag 'dcssch:title' -text $title -indent $ci -xsiType 'v8:LocalStringType' }
|
if ($title) { Emit-MLText -tag 'dcssch:title' -text $title -indent $ci -xsiType 'v8:LocalStringType' }
|
||||||
if ($restrict.Count -gt 0) {
|
if ($restrict.Count -gt 0) {
|
||||||
X "$ci<dcssch:useRestriction>"
|
X "$ci<dcssch:useRestriction>"
|
||||||
foreach ($r in @('field','condition','group','order')) { if ($restrict -contains $r) { X "$ci`t<dcssch:$r>true</dcssch:$r>" } }
|
foreach ($r in @('field','condition','group','order')) { if ($restrict -contains $r) { X "$ci`t<dcssch:$r>true</dcssch:$r>" } }
|
||||||
X "$ci</dcssch:useRestriction>"
|
X "$ci</dcssch:useRestriction>"
|
||||||
}
|
}
|
||||||
if ($pres) { X "$ci<dcssch:presentationExpression>$(Esc-Xml "$pres")</dcssch:presentationExpression>" }
|
if ($pres) { X "$ci<dcssch:presentationExpression>$(Esc-XmlText "$pres")</dcssch:presentationExpression>" }
|
||||||
if ($orderExpr) {
|
if ($orderExpr) {
|
||||||
$oeList = if ($orderExpr -is [System.Collections.IList]) { $orderExpr } else { @($orderExpr) }
|
$oeList = if ($orderExpr -is [System.Collections.IList]) { $orderExpr } else { @($orderExpr) }
|
||||||
foreach ($oe in $oeList) {
|
foreach ($oe in $oeList) {
|
||||||
if ($oe -is [string]) { $exprV = $oe; $oType = 'Asc'; $auto = 'false' }
|
if ($oe -is [string]) { $exprV = $oe; $oType = 'Asc'; $auto = 'false' }
|
||||||
else { $exprV = "$($oe.expression)"; $oType = if ($oe.orderType) { "$($oe.orderType)" } else { 'Asc' }; $auto = if ($oe.autoOrder) { 'true' } else { 'false' } }
|
else { $exprV = "$($oe.expression)"; $oType = if ($oe.orderType) { "$($oe.orderType)" } else { 'Asc' }; $auto = if ($oe.autoOrder) { 'true' } else { 'false' } }
|
||||||
X "$ci<dcssch:orderExpression>"
|
X "$ci<dcssch:orderExpression>"
|
||||||
X "$ci`t<expression xmlns=`"$($script:dcsCommonNs)`">$(Esc-Xml $exprV)</expression>"
|
X "$ci`t<expression xmlns=`"$($script:dcsCommonNs)`">$(Esc-XmlText $exprV)</expression>"
|
||||||
X "$ci`t<orderType xmlns=`"$($script:dcsCommonNs)`">$oType</orderType>"
|
X "$ci`t<orderType xmlns=`"$($script:dcsCommonNs)`">$oType</orderType>"
|
||||||
X "$ci`t<autoOrder xmlns=`"$($script:dcsCommonNs)`">$auto</autoOrder>"
|
X "$ci`t<autoOrder xmlns=`"$($script:dcsCommonNs)`">$auto</autoOrder>"
|
||||||
X "$ci</dcssch:orderExpression>"
|
X "$ci</dcssch:orderExpression>"
|
||||||
@@ -2368,26 +2403,49 @@ $script:knownInvalidTypes = @{
|
|||||||
"FormTable" = "UI element type, not a data type"
|
"FormTable" = "UI element type, not a data type"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Алиас на локальный словарь: тело Resolve-TypeStr ниже — общая реализация,
|
||||||
|
# одинаковая во всех навыках (реестр в tests/skills/check-inline-drift.mjs).
|
||||||
|
$script:typeSynonyms = $script:formTypeSynonyms
|
||||||
|
|
||||||
function Resolve-TypeStr {
|
function Resolve-TypeStr {
|
||||||
param([string]$typeStr)
|
param([string]$typeStr)
|
||||||
if (-not $typeStr) { return $typeStr }
|
if (-not $typeStr) { return $typeStr }
|
||||||
# Lenient: strip leading cfg: prefix if user passed it (canonical form is without prefix)
|
|
||||||
if ($typeStr -match '^cfg:(.+)$') { $typeStr = $Matches[1] }
|
# Прощающий ввод: ведущий префикс приходит копипастой из выгрузки. Без срезания он ломает
|
||||||
|
# поиск в словаре — русское имя типа остаётся непереведённым, и платформа отвечает
|
||||||
|
# «Неизвестное имя типа». cfg: снимаем всегда — он однозначно означает текущую конфигурацию.
|
||||||
|
# Сгенерированный dNpM: (в корпусе на этом URI встречаются d4p1, d5p1, d6p1 — имя префикса
|
||||||
|
# платформа выдаёт по порядку объявления) снимаем ТОЛЬКО у ссылочных типов, с точкой:
|
||||||
|
# сам по себе префикс многозначен — в формах d5p1:Chart, d5p1:TextDocument,
|
||||||
|
# d5p1:GeographicalSchema адресуют чужие пространства имён, и там он часть значения.
|
||||||
|
if ($typeStr.StartsWith('cfg:')) {
|
||||||
|
$typeStr = $typeStr.Substring(4)
|
||||||
|
} elseif ($typeStr.Contains('.') -and $typeStr -match '^d\d+p\d+:') {
|
||||||
|
$typeStr = $typeStr.Substring($typeStr.IndexOf(':') + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Параметризованные типы: Number(15,2), Строка(100)
|
||||||
if ($typeStr -match '^([^(]+)\((.+)\)$') {
|
if ($typeStr -match '^([^(]+)\((.+)\)$') {
|
||||||
$base = $Matches[1].Trim(); $params = $Matches[2]
|
$baseName = $Matches[1].Trim()
|
||||||
$r = $script:formTypeSynonyms[$base.ToLower()]
|
$params = $Matches[2]
|
||||||
if ($r) { return "$r($params)" }
|
$resolved = $script:typeSynonyms[$baseName.ToLower()]
|
||||||
|
if ($resolved) { return "$resolved($params)" }
|
||||||
return $typeStr
|
return $typeStr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Ссылочные типы: СправочникСсылка.Организации → CatalogRef.Организации
|
||||||
if ($typeStr.Contains('.')) {
|
if ($typeStr.Contains('.')) {
|
||||||
$i = $typeStr.IndexOf('.')
|
$dotIdx = $typeStr.IndexOf('.')
|
||||||
$prefix = $typeStr.Substring(0, $i); $suffix = $typeStr.Substring($i)
|
$prefix = $typeStr.Substring(0, $dotIdx)
|
||||||
$r = $script:formTypeSynonyms[$prefix.ToLower()]
|
$suffix = $typeStr.Substring($dotIdx) # includes the dot
|
||||||
if ($r) { return "$r$suffix" }
|
$resolved = $script:typeSynonyms[$prefix.ToLower()]
|
||||||
|
if ($resolved) { return "$resolved$suffix" }
|
||||||
return $typeStr
|
return $typeStr
|
||||||
}
|
}
|
||||||
$r = $script:formTypeSynonyms[$typeStr.ToLower()]
|
|
||||||
if ($r) { return $r }
|
# Простое имя
|
||||||
|
$resolved = $script:typeSynonyms[$typeStr.ToLower()]
|
||||||
|
if ($resolved) { return $resolved }
|
||||||
return $typeStr
|
return $typeStr
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3207,7 +3265,7 @@ function Emit-CommonElementProps {
|
|||||||
if ($null -ne $el.($p[0])) { X "$indent<$($p[1])>$(if ($el.($p[0])){'true'}else{'false'})</$($p[1])>" }
|
if ($null -ne $el.($p[0])) { X "$indent<$($p[1])>$(if ($el.($p[0])){'true'}else{'false'})</$($p[1])>" }
|
||||||
}
|
}
|
||||||
# Динамический заголовок колонки-группы из данных (HeaderDataPath) — перед HeaderHorizontalAlign (порядок XSD)
|
# Динамический заголовок колонки-группы из данных (HeaderDataPath) — перед HeaderHorizontalAlign (порядок XSD)
|
||||||
if ($el.headerDataPath) { X "$indent<HeaderDataPath>$(Esc-Xml "$($el.headerDataPath)")</HeaderDataPath>" }
|
if ($el.headerDataPath) { X "$indent<HeaderDataPath>$(Esc-XmlText "$($el.headerDataPath)")</HeaderDataPath>" }
|
||||||
if ($el.footerHorizontalAlign) { X "$indent<FooterHorizontalAlign>$($el.footerHorizontalAlign)</FooterHorizontalAlign>" }
|
if ($el.footerHorizontalAlign) { X "$indent<FooterHorizontalAlign>$($el.footerHorizontalAlign)</FooterHorizontalAlign>" }
|
||||||
if ($el.headerHorizontalAlign) { X "$indent<HeaderHorizontalAlign>$($el.headerHorizontalAlign)</HeaderHorizontalAlign>" }
|
if ($el.headerHorizontalAlign) { X "$indent<HeaderHorizontalAlign>$($el.headerHorizontalAlign)</HeaderHorizontalAlign>" }
|
||||||
# Формат заголовка колонки-группы (ML-текст) — после HeaderHorizontalAlign (порядок XSD)
|
# Формат заголовка колонки-группы (ML-текст) — после HeaderHorizontalAlign (порядок XSD)
|
||||||
@@ -3227,8 +3285,8 @@ function Emit-PictureRef {
|
|||||||
if (-not $src) { return }
|
if (-not $src) { return }
|
||||||
$srcStr = "$src"
|
$srcStr = "$src"
|
||||||
X "$indent<$picTag>"
|
X "$indent<$picTag>"
|
||||||
if ($srcStr -match '^abs:(.*)$') { X "$indent`t<xr:Abs>$(Esc-Xml $matches[1])</xr:Abs>" }
|
if ($srcStr -match '^abs:(.*)$') { X "$indent`t<xr:Abs>$(Esc-XmlText $matches[1])</xr:Abs>" }
|
||||||
else { X "$indent`t<xr:Ref>$(Esc-Xml $srcStr)</xr:Ref>" }
|
else { X "$indent`t<xr:Ref>$(Esc-XmlText $srcStr)</xr:Ref>" }
|
||||||
X "$indent`t<xr:LoadTransparent>$(if ($lt) { 'true' } else { 'false' })</xr:LoadTransparent>"
|
X "$indent`t<xr:LoadTransparent>$(if ($lt) { 'true' } else { 'false' })</xr:LoadTransparent>"
|
||||||
if ($tpx) { X "$indent`t<xr:TransparentPixel x=`"$($tpx.x)`" y=`"$($tpx.y)`"/>" }
|
if ($tpx) { X "$indent`t<xr:TransparentPixel x=`"$($tpx.x)`" y=`"$($tpx.y)`"/>" }
|
||||||
X "$indent</$picTag>"
|
X "$indent</$picTag>"
|
||||||
@@ -3257,8 +3315,8 @@ function Emit-CommandPicture {
|
|||||||
if ($null -eq $lt -and $null -ne $elemLt) { $lt = [bool]$elemLt }
|
if ($null -eq $lt -and $null -ne $elemLt) { $lt = [bool]$elemLt }
|
||||||
$srcStr = "$src"
|
$srcStr = "$src"
|
||||||
X "$indent<Picture>"
|
X "$indent<Picture>"
|
||||||
if ($srcStr -match '^abs:(.*)$') { X "$indent`t<xr:Abs>$(Esc-Xml $matches[1])</xr:Abs>" }
|
if ($srcStr -match '^abs:(.*)$') { X "$indent`t<xr:Abs>$(Esc-XmlText $matches[1])</xr:Abs>" }
|
||||||
else { X "$indent`t<xr:Ref>$(Esc-Xml $srcStr)</xr:Ref>" }
|
else { X "$indent`t<xr:Ref>$(Esc-XmlText $srcStr)</xr:Ref>" }
|
||||||
X "$indent`t<xr:LoadTransparent>$(if ($lt -eq $false) { 'false' } else { 'true' })</xr:LoadTransparent>"
|
X "$indent`t<xr:LoadTransparent>$(if ($lt -eq $false) { 'false' } else { 'true' })</xr:LoadTransparent>"
|
||||||
if ($tpx) { X "$indent`t<xr:TransparentPixel x=`"$($tpx.x)`" y=`"$($tpx.y)`"/>" }
|
if ($tpx) { X "$indent`t<xr:TransparentPixel x=`"$($tpx.x)`" y=`"$($tpx.y)`"/>" }
|
||||||
X "$indent</Picture>"
|
X "$indent</Picture>"
|
||||||
@@ -3420,7 +3478,7 @@ function Emit-GenericScalars {
|
|||||||
X "$indent<$($s.Tag)>$(if ($p.Value){'true'}else{'false'})</$($s.Tag)>"
|
X "$indent<$($s.Tag)>$(if ($p.Value){'true'}else{'false'})</$($s.Tag)>"
|
||||||
} else {
|
} else {
|
||||||
$v = "$($p.Value)"; if ($v -eq '') { continue }
|
$v = "$($p.Value)"; if ($v -eq '') { continue }
|
||||||
X "$indent<$($s.Tag)>$(Esc-Xml $v)</$($s.Tag)>"
|
X "$indent<$($s.Tag)>$(Esc-XmlText $v)</$($s.Tag)>"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3467,7 +3525,7 @@ function Emit-BorderTag {
|
|||||||
$width = if ($val.PSObject.Properties['width'] -and $null -ne $val.width) { $val.width } else { 1 }
|
$width = if ($val.PSObject.Properties['width'] -and $null -ne $val.width) { $val.width } else { 1 }
|
||||||
$style = if ($val.PSObject.Properties['style']) { "$($val.style)" } else { $null }
|
$style = if ($val.PSObject.Properties['style']) { "$($val.style)" } else { $null }
|
||||||
X "$indent<Border width=`"$width`">"
|
X "$indent<Border width=`"$width`">"
|
||||||
if ($style) { X "$indent`t<v8ui:style xsi:type=`"v8ui:ControlBorderType`">$(Esc-Xml $style)</v8ui:style>" }
|
if ($style) { X "$indent`t<v8ui:style xsi:type=`"v8ui:ControlBorderType`">$(Esc-XmlText $style)</v8ui:style>" }
|
||||||
X "$indent</Border>"
|
X "$indent</Border>"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3494,13 +3552,13 @@ function PL-Bool {
|
|||||||
}
|
}
|
||||||
function Emit-PlannerColor {
|
function Emit-PlannerColor {
|
||||||
param([string]$tag, $o, [string]$key, [string]$ind)
|
param([string]$tag, $o, [string]$key, [string]$ind)
|
||||||
X "$ind<pl:$tag>$(Esc-Xml "$(PL-Get $o $key 'auto')")</pl:$tag>"
|
X "$ind<pl:$tag>$(Esc-XmlText "$(PL-Get $o $key 'auto')")</pl:$tag>"
|
||||||
}
|
}
|
||||||
# <pl:text>/<pl:tooltip>… — пустое → самозакрывающийся тег (как в выгрузке платформы).
|
# <pl:text>/<pl:tooltip>… — пустое → самозакрывающийся тег (как в выгрузке платформы).
|
||||||
function Emit-PlannerText {
|
function Emit-PlannerText {
|
||||||
param([string]$tag, $v, [string]$ind)
|
param([string]$tag, $v, [string]$ind)
|
||||||
if ([string]::IsNullOrEmpty("$v")) { X "$ind<pl:$tag/>" }
|
if ([string]::IsNullOrEmpty("$v")) { X "$ind<pl:$tag/>" }
|
||||||
else { X "$ind<pl:$tag>$(Esc-Xml "$v")</pl:$tag>" }
|
else { X "$ind<pl:$tag>$(Esc-XmlText "$v")</pl:$tag>" }
|
||||||
}
|
}
|
||||||
# Признак ссылочного значения (объект разреза/элемент-ссылка) → xsi:type="xr:DesignTimeRef";
|
# Признак ссылочного значения (объект разреза/элемент-ссылка) → xsi:type="xr:DesignTimeRef";
|
||||||
# иначе xs:string. Покрывает англ. (Enum.X.EnumValue.Y) и рус. (Справочник.X) метатипы.
|
# иначе xs:string. Покрывает англ. (Enum.X.EnumValue.Y) и рус. (Справочник.X) метатипы.
|
||||||
@@ -3515,7 +3573,7 @@ function Emit-PlannerValue {
|
|||||||
param($v, [string]$ind)
|
param($v, [string]$ind)
|
||||||
if ($null -eq $v -or "$v" -eq '') { X "$ind<pl:value xsi:nil=`"true`"/>"; return }
|
if ($null -eq $v -or "$v" -eq '') { X "$ind<pl:value xsi:nil=`"true`"/>"; return }
|
||||||
$t = if (Test-PlannerRef "$v") { 'xr:DesignTimeRef' } else { 'xs:string' }
|
$t = if (Test-PlannerRef "$v") { 'xr:DesignTimeRef' } else { 'xs:string' }
|
||||||
X "$ind<pl:value xsi:type=`"$t`">$(Esc-Xml "$v")</pl:value>"
|
X "$ind<pl:value xsi:type=`"$t`">$(Esc-XmlText "$v")</pl:value>"
|
||||||
}
|
}
|
||||||
function Emit-PlannerFont {
|
function Emit-PlannerFont {
|
||||||
param($o, [string]$ind)
|
param($o, [string]$ind)
|
||||||
@@ -3529,14 +3587,14 @@ function Emit-PlannerBorder {
|
|||||||
$bw = if ($b) { PL-Get $b 'width' 1 } else { 1 }
|
$bw = if ($b) { PL-Get $b 'width' 1 } else { 1 }
|
||||||
$bs = if ($b) { PL-Get $b 'style' 'Single' } else { 'Single' }
|
$bs = if ($b) { PL-Get $b 'style' 'Single' } else { 'Single' }
|
||||||
X "$ind<pl:border width=`"$bw`">"
|
X "$ind<pl:border width=`"$bw`">"
|
||||||
X "$ind`t<v8ui:style xsi:type=`"v8ui:ControlBorderType`">$(Esc-Xml "$bs")</v8ui:style>"
|
X "$ind`t<v8ui:style xsi:type=`"v8ui:ControlBorderType`">$(Esc-XmlText "$bs")</v8ui:style>"
|
||||||
X "$ind</pl:border>"
|
X "$ind</pl:border>"
|
||||||
}
|
}
|
||||||
function Emit-PlannerLevel {
|
function Emit-PlannerLevel {
|
||||||
param($lv, [string]$cns, [string]$ind)
|
param($lv, [string]$cns, [string]$ind)
|
||||||
$li = "$ind`t"
|
$li = "$ind`t"
|
||||||
X "$ind<level xmlns=`"$cns`">"
|
X "$ind<level xmlns=`"$cns`">"
|
||||||
X "$li<measure>$(Esc-Xml "$(PL-Get $lv 'measure' 'Hour')")</measure>"
|
X "$li<measure>$(Esc-XmlText "$(PL-Get $lv 'measure' 'Hour')")</measure>"
|
||||||
X "$li<interval>$(PL-Get $lv 'interval' 1)</interval>"
|
X "$li<interval>$(PL-Get $lv 'interval' 1)</interval>"
|
||||||
X "$li<show>$(PL-Bool (PL-Get $lv 'show' $true))</show>"
|
X "$li<show>$(PL-Bool (PL-Get $lv 'show' $true))</show>"
|
||||||
$line = PL-Get $lv 'line' $null
|
$line = PL-Get $lv 'line' $null
|
||||||
@@ -3544,10 +3602,10 @@ function Emit-PlannerLevel {
|
|||||||
$lg = if ($line) { PL-Get $line 'gap' $false } else { $false }
|
$lg = if ($line) { PL-Get $line 'gap' $false } else { $false }
|
||||||
$lst = if ($line) { PL-Get $line 'style' 'Solid' } else { 'Solid' }
|
$lst = if ($line) { PL-Get $line 'style' 'Solid' } else { 'Solid' }
|
||||||
X "$li<line width=`"$lw`" gap=`"$(PL-Bool $lg)`">"
|
X "$li<line width=`"$lw`" gap=`"$(PL-Bool $lg)`">"
|
||||||
X "$li`t<v8ui:style xsi:type=`"v8ui:ChartLineType`">$(Esc-Xml "$lst")</v8ui:style>"
|
X "$li`t<v8ui:style xsi:type=`"v8ui:ChartLineType`">$(Esc-XmlText "$lst")</v8ui:style>"
|
||||||
X "$li</line>"
|
X "$li</line>"
|
||||||
X "$li<scaleColor>$(Esc-Xml "$(PL-Get $lv 'scaleColor' 'auto')")</scaleColor>"
|
X "$li<scaleColor>$(Esc-XmlText "$(PL-Get $lv 'scaleColor' 'auto')")</scaleColor>"
|
||||||
X "$li<dayFormatRule>$(Esc-Xml "$(PL-Get $lv 'dayFormatRule' 'MonthDayWeekDay')")</dayFormatRule>"
|
X "$li<dayFormatRule>$(Esc-XmlText "$(PL-Get $lv 'dayFormatRule' 'MonthDayWeekDay')")</dayFormatRule>"
|
||||||
$fmt = PL-Get $lv 'format' $null
|
$fmt = PL-Get $lv 'format' $null
|
||||||
if ($null -eq $fmt) { $fmt = [ordered]@{ '#' = 'DF="HH:mm"'; 'ru' = 'DF="HH:mm"' } }
|
if ($null -eq $fmt) { $fmt = [ordered]@{ '#' = 'DF="HH:mm"'; 'ru' = 'DF="HH:mm"' } }
|
||||||
X "$li<format>"
|
X "$li<format>"
|
||||||
@@ -3558,8 +3616,8 @@ function Emit-PlannerLevel {
|
|||||||
X "$li<labels>"
|
X "$li<labels>"
|
||||||
X "$li`t<ticks>$ticks</ticks>"
|
X "$li`t<ticks>$ticks</ticks>"
|
||||||
X "$li</labels>"
|
X "$li</labels>"
|
||||||
X "$li<backColor>$(Esc-Xml "$(PL-Get $lv 'backColor' 'auto')")</backColor>"
|
X "$li<backColor>$(Esc-XmlText "$(PL-Get $lv 'backColor' 'auto')")</backColor>"
|
||||||
X "$li<textColor>$(Esc-Xml "$(PL-Get $lv 'textColor' 'auto')")</textColor>"
|
X "$li<textColor>$(Esc-XmlText "$(PL-Get $lv 'textColor' 'auto')")</textColor>"
|
||||||
X "$li<showPereodicalLabels>$(PL-Bool (PL-Get $lv 'showPereodicalLabels' $true))</showPereodicalLabels>"
|
X "$li<showPereodicalLabels>$(PL-Bool (PL-Get $lv 'showPereodicalLabels' $true))</showPereodicalLabels>"
|
||||||
X "$ind</level>"
|
X "$ind</level>"
|
||||||
}
|
}
|
||||||
@@ -3568,14 +3626,14 @@ function Emit-PlannerTimeScale {
|
|||||||
$cns = $script:CHART_NS
|
$cns = $script:CHART_NS
|
||||||
$ci = "$ind`t"
|
$ci = "$ind`t"
|
||||||
X "$ind<pl:timeScale>"
|
X "$ind<pl:timeScale>"
|
||||||
X "$ci<placement xmlns=`"$cns`">$(Esc-Xml "$(if ($ts) { PL-Get $ts 'placement' 'Left' } else { 'Left' })")</placement>"
|
X "$ci<placement xmlns=`"$cns`">$(Esc-XmlText "$(if ($ts) { PL-Get $ts 'placement' 'Left' } else { 'Left' })")</placement>"
|
||||||
$levels = if ($ts) { @(PL-Get $ts 'levels' @()) } else { @() }
|
$levels = if ($ts) { @(PL-Get $ts 'levels' @()) } else { @() }
|
||||||
if (@($levels).Count -eq 0) { $levels = @($null) } # один уровень-дефолт
|
if (@($levels).Count -eq 0) { $levels = @($null) } # один уровень-дефолт
|
||||||
foreach ($lv in $levels) { Emit-PlannerLevel $lv $cns $ci }
|
foreach ($lv in $levels) { Emit-PlannerLevel $lv $cns $ci }
|
||||||
$transp = if ($ts) { PL-Get $ts 'transparent' $false } else { $false }
|
$transp = if ($ts) { PL-Get $ts 'transparent' $false } else { $false }
|
||||||
X "$ci<transparent xmlns=`"$cns`">$(PL-Bool $transp)</transparent>"
|
X "$ci<transparent xmlns=`"$cns`">$(PL-Bool $transp)</transparent>"
|
||||||
X "$ci<backColor xmlns=`"$cns`">$(Esc-Xml "$(if ($ts) { PL-Get $ts 'backColor' 'auto' } else { 'auto' })")</backColor>"
|
X "$ci<backColor xmlns=`"$cns`">$(Esc-XmlText "$(if ($ts) { PL-Get $ts 'backColor' 'auto' } else { 'auto' })")</backColor>"
|
||||||
X "$ci<textColor xmlns=`"$cns`">$(Esc-Xml "$(if ($ts) { PL-Get $ts 'textColor' 'auto' } else { 'auto' })")</textColor>"
|
X "$ci<textColor xmlns=`"$cns`">$(Esc-XmlText "$(if ($ts) { PL-Get $ts 'textColor' 'auto' } else { 'auto' })")</textColor>"
|
||||||
X "$ci<currentLevel xmlns=`"$cns`">$(if ($ts) { PL-Get $ts 'currentLevel' 0 } else { 0 })</currentLevel>"
|
X "$ci<currentLevel xmlns=`"$cns`">$(if ($ts) { PL-Get $ts 'currentLevel' 0 } else { 0 })</currentLevel>"
|
||||||
X "$ind</pl:timeScale>"
|
X "$ind</pl:timeScale>"
|
||||||
}
|
}
|
||||||
@@ -3600,7 +3658,7 @@ function Emit-PlannerItem {
|
|||||||
X "$ii<pl:id>$id</pl:id>"
|
X "$ii<pl:id>$id</pl:id>"
|
||||||
X "$ii<pl:textFormatted>$(PL-Bool (PL-Get $it 'textFormatted' $false))</pl:textFormatted>"
|
X "$ii<pl:textFormatted>$(PL-Bool (PL-Get $it 'textFormatted' $false))</pl:textFormatted>"
|
||||||
Emit-PlannerBorder $it $ii 'border'
|
Emit-PlannerBorder $it $ii 'border'
|
||||||
X "$ii<pl:editMode>$(Esc-Xml "$(PL-Get $it 'editMode' 'EnableEdit')")</pl:editMode>"
|
X "$ii<pl:editMode>$(Esc-XmlText "$(PL-Get $it 'editMode' 'EnableEdit')")</pl:editMode>"
|
||||||
X "$ind</pl:item>"
|
X "$ind</pl:item>"
|
||||||
}
|
}
|
||||||
# Элемент измерения (<pl:item> внутри <pl:dimension>) — рекурсивен: может нести вложенные
|
# Элемент измерения (<pl:item> внутри <pl:dimension>) — рекурсивен: может нести вложенные
|
||||||
@@ -3655,7 +3713,7 @@ function Emit-PlannerSettings {
|
|||||||
$wfmt = PL-Get $pl 'timeScaleWrapHeadersFormat' $null
|
$wfmt = PL-Get $pl 'timeScaleWrapHeadersFormat' $null
|
||||||
if ($null -eq $wfmt) { $wfmt = [ordered]@{ '#' = 'DLF="DD"'; 'ru' = 'DLF="DD"' } }
|
if ($null -eq $wfmt) { $wfmt = [ordered]@{ '#' = 'DLF="DD"'; 'ru' = 'DLF="DD"' } }
|
||||||
Emit-MLText -tag 'pl:timeScaleWrapHeadersFormat' -text $wfmt -indent $si
|
Emit-MLText -tag 'pl:timeScaleWrapHeadersFormat' -text $wfmt -indent $si
|
||||||
X "$si<pl:periodicVariantUnit>$(Esc-Xml "$(PL-Get $pl 'periodicVariantUnit' 'Day')")</pl:periodicVariantUnit>"
|
X "$si<pl:periodicVariantUnit>$(Esc-XmlText "$(PL-Get $pl 'periodicVariantUnit' 'Day')")</pl:periodicVariantUnit>"
|
||||||
X "$si<pl:periodicVariantRepetition>$(PL-Get $pl 'periodicVariantRepetition' 1)</pl:periodicVariantRepetition>"
|
X "$si<pl:periodicVariantRepetition>$(PL-Get $pl 'periodicVariantRepetition' 1)</pl:periodicVariantRepetition>"
|
||||||
X "$si<pl:timeScaleWrapBeginIndent>$(PL-Get $pl 'timeScaleWrapBeginIndent' 0)</pl:timeScaleWrapBeginIndent>"
|
X "$si<pl:timeScaleWrapBeginIndent>$(PL-Get $pl 'timeScaleWrapBeginIndent' 0)</pl:timeScaleWrapBeginIndent>"
|
||||||
X "$si<pl:timeScaleWrapEndIndent>$(PL-Get $pl 'timeScaleWrapEndIndent' 0)</pl:timeScaleWrapEndIndent>"
|
X "$si<pl:timeScaleWrapEndIndent>$(PL-Get $pl 'timeScaleWrapEndIndent' 0)</pl:timeScaleWrapEndIndent>"
|
||||||
@@ -3668,16 +3726,16 @@ function Emit-PlannerSettings {
|
|||||||
X "$si</pl:period>"
|
X "$si</pl:period>"
|
||||||
}
|
}
|
||||||
X "$si<pl:displayCurrentDate>$(PL-Bool (PL-Get $pl 'displayCurrentDate' $true))</pl:displayCurrentDate>"
|
X "$si<pl:displayCurrentDate>$(PL-Bool (PL-Get $pl 'displayCurrentDate' $true))</pl:displayCurrentDate>"
|
||||||
X "$si<pl:itemsTimeRepresentation>$(Esc-Xml "$(PL-Get $pl 'itemsTimeRepresentation' 'BeginTime')")</pl:itemsTimeRepresentation>"
|
X "$si<pl:itemsTimeRepresentation>$(Esc-XmlText "$(PL-Get $pl 'itemsTimeRepresentation' 'BeginTime')")</pl:itemsTimeRepresentation>"
|
||||||
X "$si<pl:itemsBehaviorWhenSpaceInsufficient>$(Esc-Xml "$(PL-Get $pl 'itemsBehaviorWhenSpaceInsufficient' 'CollapseItems')")</pl:itemsBehaviorWhenSpaceInsufficient>"
|
X "$si<pl:itemsBehaviorWhenSpaceInsufficient>$(Esc-XmlText "$(PL-Get $pl 'itemsBehaviorWhenSpaceInsufficient' 'CollapseItems')")</pl:itemsBehaviorWhenSpaceInsufficient>"
|
||||||
X "$si<pl:autoMinColumnWidth>$(PL-Bool (PL-Get $pl 'autoMinColumnWidth' $true))</pl:autoMinColumnWidth>"
|
X "$si<pl:autoMinColumnWidth>$(PL-Bool (PL-Get $pl 'autoMinColumnWidth' $true))</pl:autoMinColumnWidth>"
|
||||||
X "$si<pl:autoMinRowHeight>$(PL-Bool (PL-Get $pl 'autoMinRowHeight' $true))</pl:autoMinRowHeight>"
|
X "$si<pl:autoMinRowHeight>$(PL-Bool (PL-Get $pl 'autoMinRowHeight' $true))</pl:autoMinRowHeight>"
|
||||||
X "$si<pl:minColumnWidth>$(PL-Get $pl 'minColumnWidth' 0)</pl:minColumnWidth>"
|
X "$si<pl:minColumnWidth>$(PL-Get $pl 'minColumnWidth' 0)</pl:minColumnWidth>"
|
||||||
X "$si<pl:minRowHeight>$(PL-Get $pl 'minRowHeight' 0)</pl:minRowHeight>"
|
X "$si<pl:minRowHeight>$(PL-Get $pl 'minRowHeight' 0)</pl:minRowHeight>"
|
||||||
X "$si<pl:fixDimensionsHeader>$(Esc-Xml "$(PL-Get $pl 'fixDimensionsHeader' 'auto')")</pl:fixDimensionsHeader>"
|
X "$si<pl:fixDimensionsHeader>$(Esc-XmlText "$(PL-Get $pl 'fixDimensionsHeader' 'auto')")</pl:fixDimensionsHeader>"
|
||||||
X "$si<pl:fixTimeScaleHeader>$(Esc-Xml "$(PL-Get $pl 'fixTimeScaleHeader' 'auto')")</pl:fixTimeScaleHeader>"
|
X "$si<pl:fixTimeScaleHeader>$(Esc-XmlText "$(PL-Get $pl 'fixTimeScaleHeader' 'auto')")</pl:fixTimeScaleHeader>"
|
||||||
Emit-PlannerBorder $pl $si 'border'
|
Emit-PlannerBorder $pl $si 'border'
|
||||||
X "$si<pl:newItemsTextType>$(Esc-Xml "$(PL-Get $pl 'newItemsTextType' 'String')")</pl:newItemsTextType>"
|
X "$si<pl:newItemsTextType>$(Esc-XmlText "$(PL-Get $pl 'newItemsTextType' 'String')")</pl:newItemsTextType>"
|
||||||
X "$ind</Settings>"
|
X "$ind</Settings>"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3710,13 +3768,13 @@ function Emit-ChartNode {
|
|||||||
if ($keys -contains 'gap') {
|
if ($keys -contains 'gap') {
|
||||||
$w = Get-Prop $val 'width'; $g = Get-Prop $val 'gap'; $st = Get-Prop $val 'style'
|
$w = Get-Prop $val 'width'; $g = Get-Prop $val 'gap'; $st = Get-Prop $val 'style'
|
||||||
X "$ind<d4p1:$name width=`"$w`" gap=`"$(PL-Bool $g)`">"
|
X "$ind<d4p1:$name width=`"$w`" gap=`"$(PL-Bool $g)`">"
|
||||||
X "$ind`t<v8ui:style xsi:type=`"v8ui:ChartLineType`">$(Esc-Xml "$st")</v8ui:style>"
|
X "$ind`t<v8ui:style xsi:type=`"v8ui:ChartLineType`">$(Esc-XmlText "$st")</v8ui:style>"
|
||||||
X "$ind</d4p1:$name>"; return
|
X "$ind</d4p1:$name>"; return
|
||||||
}
|
}
|
||||||
if (($keys -contains 'style') -and ($keys -contains 'width')) {
|
if (($keys -contains 'style') -and ($keys -contains 'width')) {
|
||||||
$w = Get-Prop $val 'width'; $st = Get-Prop $val 'style'
|
$w = Get-Prop $val 'width'; $st = Get-Prop $val 'style'
|
||||||
X "$ind<d4p1:$name width=`"$w`">"
|
X "$ind<d4p1:$name width=`"$w`">"
|
||||||
X "$ind`t<v8ui:style xsi:type=`"v8ui:ControlBorderType`">$(Esc-Xml "$st")</v8ui:style>"
|
X "$ind`t<v8ui:style xsi:type=`"v8ui:ControlBorderType`">$(Esc-XmlText "$st")</v8ui:style>"
|
||||||
X "$ind</d4p1:$name>"; return
|
X "$ind</d4p1:$name>"; return
|
||||||
}
|
}
|
||||||
$isFont = $false; foreach ($fk in $script:CHART_FONT_KEYS) { if ($keys -contains $fk) { $isFont = $true; break } }
|
$isFont = $false; foreach ($fk in $script:CHART_FONT_KEYS) { if ($keys -contains $fk) { $isFont = $true; break } }
|
||||||
@@ -3732,7 +3790,7 @@ function Emit-ChartNode {
|
|||||||
}
|
}
|
||||||
if ($null -eq $val -or "$val" -eq '') { X "$ind<d4p1:$name/>"; return }
|
if ($null -eq $val -or "$val" -eq '') { X "$ind<d4p1:$name/>"; return }
|
||||||
if ($val -is [bool]) { X "$ind<d4p1:$name>$(PL-Bool $val)</d4p1:$name>"; return }
|
if ($val -is [bool]) { X "$ind<d4p1:$name>$(PL-Bool $val)</d4p1:$name>"; return }
|
||||||
X "$ind<d4p1:$name>$(Esc-Xml "$val")</d4p1:$name>"
|
X "$ind<d4p1:$name>$(Esc-XmlText "$val")</d4p1:$name>"
|
||||||
}
|
}
|
||||||
function Emit-ChartSettings {
|
function Emit-ChartSettings {
|
||||||
param($chart, [string]$ind, [string]$ctype = 'd4p1:Chart')
|
param($chart, [string]$ind, [string]$ctype = 'd4p1:Chart')
|
||||||
@@ -3754,7 +3812,7 @@ function Emit-Appearance {
|
|||||||
if ($null -eq $val -or ($val -is [string] -and $val -eq '')) { continue }
|
if ($null -eq $val -or ($val -is [string] -and $val -eq '')) { continue }
|
||||||
$spec = $script:appearanceSpec[$key]
|
$spec = $script:appearanceSpec[$key]
|
||||||
switch ($spec.kind) {
|
switch ($spec.kind) {
|
||||||
'color' { X "$indent<$($spec.tag)>$(Esc-Xml "$val")</$($spec.tag)>" }
|
'color' { X "$indent<$($spec.tag)>$(Esc-XmlText "$val")</$($spec.tag)>" }
|
||||||
'font' { Emit-FontTag -tag $spec.tag -val $val -indent $indent }
|
'font' { Emit-FontTag -tag $spec.tag -val $val -indent $indent }
|
||||||
'border' { Emit-BorderTag -val $val -indent $indent }
|
'border' { Emit-BorderTag -val $val -indent $indent }
|
||||||
}
|
}
|
||||||
@@ -4039,13 +4097,13 @@ function Emit-Input {
|
|||||||
@('choiceForm','ChoiceForm'), @('choiceHistoryOnInput','ChoiceHistoryOnInput'),
|
@('choiceForm','ChoiceForm'), @('choiceHistoryOnInput','ChoiceHistoryOnInput'),
|
||||||
@('choiceFoldersAndItems','ChoiceFoldersAndItems'), @('footerDataPath','FooterDataPath')
|
@('choiceFoldersAndItems','ChoiceFoldersAndItems'), @('footerDataPath','FooterDataPath')
|
||||||
)) {
|
)) {
|
||||||
if ($el.($p[0])) { X "$inner<$($p[1])>$(Esc-Xml "$($el.($p[0]))")</$($p[1])>" }
|
if ($el.($p[0])) { X "$inner<$($p[1])>$(Esc-XmlText "$($el.($p[0]))")</$($p[1])>" }
|
||||||
}
|
}
|
||||||
# MinValue/MaxValue — типизированное. JSON-число → xs:decimal, строка → xs:string (тип сохранён декомпилятором).
|
# MinValue/MaxValue — типизированное. JSON-число → xs:decimal, строка → xs:string (тип сохранён декомпилятором).
|
||||||
foreach ($p in @(@('minValue','MinValue'), @('maxValue','MaxValue'))) {
|
foreach ($p in @(@('minValue','MinValue'), @('maxValue','MaxValue'))) {
|
||||||
if ($null -ne $el.($p[0])) {
|
if ($null -ne $el.($p[0])) {
|
||||||
$mvt = if ($el.($p[0]) -is [string]) { 'xs:string' } else { 'xs:decimal' }
|
$mvt = if ($el.($p[0]) -is [string]) { 'xs:string' } else { 'xs:decimal' }
|
||||||
X "$inner<$($p[1]) xsi:type=`"$mvt`">$(Esc-Xml "$($el.($p[0]))")</$($p[1])>"
|
X "$inner<$($p[1]) xsi:type=`"$mvt`">$(Esc-XmlText "$($el.($p[0]))")</$($p[1])>"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ($el.choiceButtonRepresentation) { X "$inner<ChoiceButtonRepresentation>$($el.choiceButtonRepresentation)</ChoiceButtonRepresentation>" }
|
if ($el.choiceButtonRepresentation) { X "$inner<ChoiceButtonRepresentation>$($el.choiceButtonRepresentation)</ChoiceButtonRepresentation>" }
|
||||||
@@ -4108,7 +4166,7 @@ function Emit-Check {
|
|||||||
|
|
||||||
if ($null -ne $el.warningOnEdit) { Emit-MLText -tag "WarningOnEdit" -text $el.warningOnEdit -indent $inner }
|
if ($null -ne $el.warningOnEdit) { Emit-MLText -tag "WarningOnEdit" -text $el.warningOnEdit -indent $inner }
|
||||||
# FooterDataPath / FooterText — общие cell-свойства колонки (как у input/labelField)
|
# FooterDataPath / FooterText — общие cell-свойства колонки (как у input/labelField)
|
||||||
if ($el.footerDataPath) { X "$inner<FooterDataPath>$(Esc-Xml "$($el.footerDataPath)")</FooterDataPath>" }
|
if ($el.footerDataPath) { X "$inner<FooterDataPath>$(Esc-XmlText "$($el.footerDataPath)")</FooterDataPath>" }
|
||||||
if ($null -ne $el.footerText) { Emit-MLText -tag "FooterText" -text $el.footerText -indent $inner }
|
if ($null -ne $el.footerText) { Emit-MLText -tag "FooterText" -text $el.footerText -indent $inner }
|
||||||
|
|
||||||
# Формат / формат редактирования (LocalStringType — строка или {ru,en})
|
# Формат / формат редактирования (LocalStringType — строка или {ru,en})
|
||||||
@@ -4265,7 +4323,7 @@ function Emit-ChoicePresentation {
|
|||||||
foreach ($pair in $pairs) {
|
foreach ($pair in $pairs) {
|
||||||
X "$indent`t<v8:item>"
|
X "$indent`t<v8:item>"
|
||||||
X "$indent`t`t<v8:lang>$($pair[0])</v8:lang>"
|
X "$indent`t`t<v8:lang>$($pair[0])</v8:lang>"
|
||||||
X "$indent`t`t<v8:content>$(Esc-Xml $pair[1])</v8:content>"
|
X "$indent`t`t<v8:content>$(Esc-XmlText $pair[1])</v8:content>"
|
||||||
X "$indent`t</v8:item>"
|
X "$indent`t</v8:item>"
|
||||||
}
|
}
|
||||||
X "$indent</Presentation>"
|
X "$indent</Presentation>"
|
||||||
@@ -4275,7 +4333,7 @@ function Emit-ChoicePresentation {
|
|||||||
function Get-ChoiceValueTag {
|
function Get-ChoiceValueTag {
|
||||||
param($norm)
|
param($norm)
|
||||||
if ([string]::IsNullOrEmpty($norm.Text)) { return "<Value xsi:type=`"$($norm.XsiType)`"/>" }
|
if ([string]::IsNullOrEmpty($norm.Text)) { return "<Value xsi:type=`"$($norm.XsiType)`"/>" }
|
||||||
return "<Value xsi:type=`"$($norm.XsiType)`">$(Esc-Xml $norm.Text)</Value>"
|
return "<Value xsi:type=`"$($norm.XsiType)`">$(Esc-XmlText $norm.Text)</Value>"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Emit <ChoiceList> (список выбора) — у RadioButtonField и InputField.
|
# Emit <ChoiceList> (список выбора) — у RadioButtonField и InputField.
|
||||||
@@ -4488,8 +4546,8 @@ function Emit-ChoiceParameterLinks {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
X "$indent`t<xr:Link>"
|
X "$indent`t<xr:Link>"
|
||||||
X "$indent`t`t<xr:Name>$(Esc-Xml "$name")</xr:Name>"
|
X "$indent`t`t<xr:Name>$(Esc-XmlText "$name")</xr:Name>"
|
||||||
X "$indent`t`t<xr:DataPath xsi:type=`"xs:string`">$(Esc-Xml "$dp")</xr:DataPath>"
|
X "$indent`t`t<xr:DataPath xsi:type=`"xs:string`">$(Esc-XmlText "$dp")</xr:DataPath>"
|
||||||
X "$indent`t`t<xr:ValueChange>$vc</xr:ValueChange>"
|
X "$indent`t`t<xr:ValueChange>$vc</xr:ValueChange>"
|
||||||
X "$indent`t</xr:Link>"
|
X "$indent`t</xr:Link>"
|
||||||
}
|
}
|
||||||
@@ -4506,7 +4564,7 @@ function Emit-TypeLink {
|
|||||||
$li = Get-ElProp $tl @('linkItem','элементСвязи')
|
$li = Get-ElProp $tl @('linkItem','элементСвязи')
|
||||||
if ($null -eq $li) { $li = 0 }
|
if ($null -eq $li) { $li = 0 }
|
||||||
X "$indent<TypeLink>"
|
X "$indent<TypeLink>"
|
||||||
X "$indent`t<xr:DataPath>$(Esc-Xml "$dp")</xr:DataPath>"
|
X "$indent`t<xr:DataPath>$(Esc-XmlText "$dp")</xr:DataPath>"
|
||||||
X "$indent`t<xr:LinkItem>$li</xr:LinkItem>"
|
X "$indent`t<xr:LinkItem>$li</xr:LinkItem>"
|
||||||
X "$indent</TypeLink>"
|
X "$indent</TypeLink>"
|
||||||
}
|
}
|
||||||
@@ -4613,7 +4671,7 @@ function Emit-LabelField {
|
|||||||
if ($el.titleLocation) { X "$inner<TitleLocation>$(Map-TitleLoc "$($el.titleLocation)")</TitleLocation>" }
|
if ($el.titleLocation) { X "$inner<TitleLocation>$(Map-TitleLoc "$($el.titleLocation)")</TitleLocation>" }
|
||||||
if ($el.editMode) { X "$inner<EditMode>$($el.editMode)</EditMode>" }
|
if ($el.editMode) { X "$inner<EditMode>$($el.editMode)</EditMode>" }
|
||||||
# FooterDataPath — путь данных подвала колонки (общий cell-prop, как у input); после EditMode
|
# FooterDataPath — путь данных подвала колонки (общий cell-prop, как у input); после EditMode
|
||||||
if ($el.footerDataPath) { X "$inner<FooterDataPath>$(Esc-Xml "$($el.footerDataPath)")</FooterDataPath>" }
|
if ($el.footerDataPath) { X "$inner<FooterDataPath>$(Esc-XmlText "$($el.footerDataPath)")</FooterDataPath>" }
|
||||||
# PasswordMode на LabelField — платформа эмитит явный false (редко); факт. значение
|
# PasswordMode на LabelField — платформа эмитит явный false (редко); факт. значение
|
||||||
if ($null -ne $el.passwordMode) { X "$inner<PasswordMode>$(if ($el.passwordMode){'true'}else{'false'})</PasswordMode>" }
|
if ($null -ne $el.passwordMode) { X "$inner<PasswordMode>$(if ($el.passwordMode){'true'}else{'false'})</PasswordMode>" }
|
||||||
Emit-ColumnPics -el $el -indent $inner
|
Emit-ColumnPics -el $el -indent $inner
|
||||||
@@ -4936,7 +4994,7 @@ function Emit-Button {
|
|||||||
if (($btnParam -is [System.Management.Automation.PSCustomObject] -or $btnParam -is [hashtable]) -and $btnParam.type) {
|
if (($btnParam -is [System.Management.Automation.PSCustomObject] -or $btnParam -is [hashtable]) -and $btnParam.type) {
|
||||||
Emit-Type -typeStr "$($btnParam.type)" -indent $inner -tag "Parameter" -tagAttrs ' xsi:type="v8:TypeDescription"'
|
Emit-Type -typeStr "$($btnParam.type)" -indent $inner -tag "Parameter" -tagAttrs ' xsi:type="v8:TypeDescription"'
|
||||||
} else {
|
} else {
|
||||||
X "$inner<Parameter xsi:type=`"xr:MDObjectRef`">$(Esc-Xml "$btnParam")</Parameter>"
|
X "$inner<Parameter xsi:type=`"xr:MDObjectRef`">$(Esc-XmlText "$btnParam")</Parameter>"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
# DataPath — привязка команды кнопки к контексту (Объект.Ref, Items.X.CurrentData.Поле)
|
# DataPath — привязка команды кнопки к контексту (Объект.Ref, Items.X.CurrentData.Поле)
|
||||||
@@ -4991,8 +5049,8 @@ function Emit-PictureDecoration {
|
|||||||
$srcStr = "$($el.src)"
|
$srcStr = "$($el.src)"
|
||||||
$lt = if ($el.loadTransparent -eq $true) { "true" } else { "false" }
|
$lt = if ($el.loadTransparent -eq $true) { "true" } else { "false" }
|
||||||
X "$inner<Picture>"
|
X "$inner<Picture>"
|
||||||
if ($srcStr -match '^abs:(.*)$') { X "$inner`t<xr:Abs>$(Esc-Xml $matches[1])</xr:Abs>" }
|
if ($srcStr -match '^abs:(.*)$') { X "$inner`t<xr:Abs>$(Esc-XmlText $matches[1])</xr:Abs>" }
|
||||||
else { X "$inner`t<xr:Ref>$(Esc-Xml $srcStr)</xr:Ref>" }
|
else { X "$inner`t<xr:Ref>$(Esc-XmlText $srcStr)</xr:Ref>" }
|
||||||
X "$inner`t<xr:LoadTransparent>$lt</xr:LoadTransparent>"
|
X "$inner`t<xr:LoadTransparent>$lt</xr:LoadTransparent>"
|
||||||
if ($el.transparentPixel) { X "$inner`t<xr:TransparentPixel x=`"$($el.transparentPixel.x)`" y=`"$($el.transparentPixel.y)`"/>" }
|
if ($el.transparentPixel) { X "$inner`t<xr:TransparentPixel x=`"$($el.transparentPixel.x)`" y=`"$($el.transparentPixel.y)`"/>" }
|
||||||
X "$inner</Picture>"
|
X "$inner</Picture>"
|
||||||
@@ -5036,7 +5094,7 @@ function Emit-PictureField {
|
|||||||
if ($null -ne $el.enableDrag) { X "$inner<EnableDrag>$(if ($el.enableDrag){'true'}else{'false'})</EnableDrag>" }
|
if ($null -ne $el.enableDrag) { X "$inner<EnableDrag>$(if ($el.enableDrag){'true'}else{'false'})</EnableDrag>" }
|
||||||
|
|
||||||
# FooterDataPath / FooterText — общие cell-свойства колонки (как у input/labelField)
|
# FooterDataPath / FooterText — общие cell-свойства колонки (как у input/labelField)
|
||||||
if ($el.footerDataPath) { X "$inner<FooterDataPath>$(Esc-Xml "$($el.footerDataPath)")</FooterDataPath>" }
|
if ($el.footerDataPath) { X "$inner<FooterDataPath>$(Esc-XmlText "$($el.footerDataPath)")</FooterDataPath>" }
|
||||||
if ($null -ne $el.footerText) { Emit-MLText -tag "FooterText" -text $el.footerText -indent $inner }
|
if ($null -ne $el.footerText) { Emit-MLText -tag "FooterText" -text $el.footerText -indent $inner }
|
||||||
|
|
||||||
# ValuesPicture — picture (collection) used to render the field's value.
|
# ValuesPicture — picture (collection) used to render the field's value.
|
||||||
@@ -5392,18 +5450,18 @@ function Emit-DLValue {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
$valStr = if ($val -is [bool]) { if ($val) { 'true' } else { 'false' } } else { "$val" }
|
$valStr = if ($val -is [bool]) { if ($val) { 'true' } else { 'false' } } else { "$val" }
|
||||||
if ($type -match '^(date|dateTime|time)') { X "$indent<dcssch:value xsi:type=`"xs:dateTime`">$(Esc-Xml $valStr)</dcssch:value>" }
|
if ($type -match '^(date|dateTime|time)') { X "$indent<dcssch:value xsi:type=`"xs:dateTime`">$(Esc-XmlText $valStr)</dcssch:value>" }
|
||||||
elseif ($type -eq "boolean") { X "$indent<dcssch:value xsi:type=`"xs:boolean`">$(Esc-Xml $valStr)</dcssch:value>" }
|
elseif ($type -eq "boolean") { X "$indent<dcssch:value xsi:type=`"xs:boolean`">$(Esc-XmlText $valStr)</dcssch:value>" }
|
||||||
elseif ($type -eq 'v8:Type') { $nsAttr = Get-ValueTypeNsAttr -valueType 'v8:Type' -value $valStr; X "$indent<dcssch:value$nsAttr xsi:type=`"v8:Type`">$(Esc-Xml $valStr)</dcssch:value>" }
|
elseif ($type -eq 'v8:Type') { $nsAttr = Get-ValueTypeNsAttr -valueType 'v8:Type' -value $valStr; X "$indent<dcssch:value$nsAttr xsi:type=`"v8:Type`">$(Esc-XmlText $valStr)</dcssch:value>" }
|
||||||
elseif ($type -match '^ent:') { X "$indent<dcssch:value xsi:type=`"$type`">$(Esc-Xml $valStr)</dcssch:value>" } # системное перечисление (ent:X) — value несёт тот же xsi:type
|
elseif ($type -match '^ent:') { X "$indent<dcssch:value xsi:type=`"$type`">$(Esc-XmlText $valStr)</dcssch:value>" } # системное перечисление (ent:X) — value несёт тот же xsi:type
|
||||||
elseif ($type -match '^decimal') { X "$indent<dcssch:value xsi:type=`"xs:decimal`">$(Esc-Xml $valStr)</dcssch:value>" }
|
elseif ($type -match '^decimal') { X "$indent<dcssch:value xsi:type=`"xs:decimal`">$(Esc-XmlText $valStr)</dcssch:value>" }
|
||||||
elseif ($type -match '^string') { X "$indent<dcssch:value xsi:type=`"xs:string`">$(Esc-Xml $valStr)</dcssch:value>" }
|
elseif ($type -match '^string') { X "$indent<dcssch:value xsi:type=`"xs:string`">$(Esc-XmlText $valStr)</dcssch:value>" }
|
||||||
elseif ($type -match '^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|BusinessProcessRef|TaskRef|ExchangePlanRef)\.') { X "$indent<dcssch:value xsi:type=`"dcscor:DesignTimeValue`">$(Esc-Xml $valStr)</dcssch:value>" }
|
elseif ($type -match '^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|BusinessProcessRef|TaskRef|ExchangePlanRef)\.') { X "$indent<dcssch:value xsi:type=`"dcscor:DesignTimeValue`">$(Esc-XmlText $valStr)</dcssch:value>" }
|
||||||
else {
|
else {
|
||||||
if ($valStr -match '^\d{4}-\d{2}-\d{2}T') { X "$indent<dcssch:value xsi:type=`"xs:dateTime`">$(Esc-Xml $valStr)</dcssch:value>" }
|
if ($valStr -match '^\d{4}-\d{2}-\d{2}T') { X "$indent<dcssch:value xsi:type=`"xs:dateTime`">$(Esc-XmlText $valStr)</dcssch:value>" }
|
||||||
elseif ($valStr -eq "true" -or $valStr -eq "false") { X "$indent<dcssch:value xsi:type=`"xs:boolean`">$(Esc-Xml $valStr)</dcssch:value>" }
|
elseif ($valStr -eq "true" -or $valStr -eq "false") { X "$indent<dcssch:value xsi:type=`"xs:boolean`">$(Esc-XmlText $valStr)</dcssch:value>" }
|
||||||
elseif ($valStr -match '^(ПланСчетов|Справочник|Перечисление|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена)\.' -or $valStr -match '^(ChartOfAccounts|Catalog|Enum|Document|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.') { X "$indent<dcssch:value xsi:type=`"dcscor:DesignTimeValue`">$(Esc-Xml $valStr)</dcssch:value>" }
|
elseif ($valStr -match '^(ПланСчетов|Справочник|Перечисление|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена)\.' -or $valStr -match '^(ChartOfAccounts|Catalog|Enum|Document|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.') { X "$indent<dcssch:value xsi:type=`"dcscor:DesignTimeValue`">$(Esc-XmlText $valStr)</dcssch:value>" }
|
||||||
else { X "$indent<dcssch:value xsi:type=`"xs:string`">$(Esc-Xml $valStr)</dcssch:value>" }
|
else { X "$indent<dcssch:value xsi:type=`"xs:string`">$(Esc-XmlText $valStr)</dcssch:value>" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5437,7 +5495,7 @@ function Emit-DLInputParameters {
|
|||||||
foreach ($item in $items) {
|
foreach ($item in $items) {
|
||||||
X "$indent`t<dcscor:item>"
|
X "$indent`t<dcscor:item>"
|
||||||
if ((Has-DLProp $item 'use') -and $null -ne $item.use -and -not $item.use) { X "$indent`t`t<dcscor:use>false</dcscor:use>" }
|
if ((Has-DLProp $item 'use') -and $null -ne $item.use -and -not $item.use) { X "$indent`t`t<dcscor:use>false</dcscor:use>" }
|
||||||
X "$indent`t`t<dcscor:parameter>$(Esc-Xml "$($item.parameter)")</dcscor:parameter>"
|
X "$indent`t`t<dcscor:parameter>$(Esc-XmlText "$($item.parameter)")</dcscor:parameter>"
|
||||||
if (Has-DLProp $item 'choiceParameters') {
|
if (Has-DLProp $item 'choiceParameters') {
|
||||||
$cpItems = if ($null -ne $item.choiceParameters) { @($item.choiceParameters) } else { @() }
|
$cpItems = if ($null -ne $item.choiceParameters) { @($item.choiceParameters) } else { @() }
|
||||||
if ($cpItems.Count -eq 0) { X "$indent`t`t<dcscor:value xsi:type=`"dcscor:ChoiceParameters`"/>" }
|
if ($cpItems.Count -eq 0) { X "$indent`t`t<dcscor:value xsi:type=`"dcscor:ChoiceParameters`"/>" }
|
||||||
@@ -5445,11 +5503,11 @@ function Emit-DLInputParameters {
|
|||||||
X "$indent`t`t<dcscor:value xsi:type=`"dcscor:ChoiceParameters`">"
|
X "$indent`t`t<dcscor:value xsi:type=`"dcscor:ChoiceParameters`">"
|
||||||
foreach ($cpItem in $cpItems) {
|
foreach ($cpItem in $cpItems) {
|
||||||
X "$indent`t`t`t<dcscor:item>"
|
X "$indent`t`t`t<dcscor:item>"
|
||||||
X "$indent`t`t`t`t<dcscor:choiceParameter>$(Esc-Xml "$($cpItem.name)")</dcscor:choiceParameter>"
|
X "$indent`t`t`t`t<dcscor:choiceParameter>$(Esc-XmlText "$($cpItem.name)")</dcscor:choiceParameter>"
|
||||||
foreach ($v in @($cpItem.values)) {
|
foreach ($v in @($cpItem.values)) {
|
||||||
if ($v -is [bool]) { X "$indent`t`t`t`t<dcscor:value xsi:type=`"xs:boolean`">$(if ($v) { 'true' } else { 'false' })</dcscor:value>" }
|
if ($v -is [bool]) { X "$indent`t`t`t`t<dcscor:value xsi:type=`"xs:boolean`">$(if ($v) { 'true' } else { 'false' })</dcscor:value>" }
|
||||||
elseif ($v -is [int] -or $v -is [long] -or $v -is [double] -or $v -is [decimal]) { X "$indent`t`t`t`t<dcscor:value xsi:type=`"xs:decimal`">$v</dcscor:value>" }
|
elseif ($v -is [int] -or $v -is [long] -or $v -is [double] -or $v -is [decimal]) { X "$indent`t`t`t`t<dcscor:value xsi:type=`"xs:decimal`">$v</dcscor:value>" }
|
||||||
else { X "$indent`t`t`t`t<dcscor:value xsi:type=`"dcscor:DesignTimeValue`">$(Esc-Xml "$v")</dcscor:value>" }
|
else { X "$indent`t`t`t`t<dcscor:value xsi:type=`"dcscor:DesignTimeValue`">$(Esc-XmlText "$v")</dcscor:value>" }
|
||||||
}
|
}
|
||||||
X "$indent`t`t`t</dcscor:item>"
|
X "$indent`t`t`t</dcscor:item>"
|
||||||
}
|
}
|
||||||
@@ -5462,8 +5520,8 @@ function Emit-DLInputParameters {
|
|||||||
X "$indent`t`t<dcscor:value xsi:type=`"dcscor:ChoiceParameterLinks`">"
|
X "$indent`t`t<dcscor:value xsi:type=`"dcscor:ChoiceParameterLinks`">"
|
||||||
foreach ($cplItem in $cplItems) {
|
foreach ($cplItem in $cplItems) {
|
||||||
X "$indent`t`t`t<dcscor:item>"
|
X "$indent`t`t`t<dcscor:item>"
|
||||||
X "$indent`t`t`t`t<dcscor:choiceParameter>$(Esc-Xml "$($cplItem.name)")</dcscor:choiceParameter>"
|
X "$indent`t`t`t`t<dcscor:choiceParameter>$(Esc-XmlText "$($cplItem.name)")</dcscor:choiceParameter>"
|
||||||
X "$indent`t`t`t`t<dcscor:value>$(Esc-Xml "$($cplItem.value)")</dcscor:value>"
|
X "$indent`t`t`t`t<dcscor:value>$(Esc-XmlText "$($cplItem.value)")</dcscor:value>"
|
||||||
$mode = if ($cplItem.mode) { "$($cplItem.mode)" } else { 'Auto' }
|
$mode = if ($cplItem.mode) { "$($cplItem.mode)" } else { 'Auto' }
|
||||||
X "$indent`t`t`t`t<dcscor:mode xmlns:d8p1=`"http://v8.1c.ru/8.1/data/enterprise`" xsi:type=`"d8p1:LinkedValueChangeMode`">$mode</dcscor:mode>"
|
X "$indent`t`t`t`t<dcscor:mode xmlns:d8p1=`"http://v8.1c.ru/8.1/data/enterprise`" xsi:type=`"d8p1:LinkedValueChangeMode`">$mode</dcscor:mode>"
|
||||||
X "$indent`t`t`t</dcscor:item>"
|
X "$indent`t`t`t</dcscor:item>"
|
||||||
@@ -5474,15 +5532,15 @@ function Emit-DLInputParameters {
|
|||||||
# Связь по типу (dcscor:TypeLink) — field + linkItem (структурное значение параметра).
|
# Связь по типу (dcscor:TypeLink) — field + linkItem (структурное значение параметра).
|
||||||
$tl = $item.typeLink
|
$tl = $item.typeLink
|
||||||
X "$indent`t`t<dcscor:value xsi:type=`"dcscor:TypeLink`">"
|
X "$indent`t`t<dcscor:value xsi:type=`"dcscor:TypeLink`">"
|
||||||
$tlf = Get-Prop $tl 'field'; if ($null -ne $tlf) { X "$indent`t`t`t<dcscor:field>$(Esc-Xml "$tlf")</dcscor:field>" }
|
$tlf = Get-Prop $tl 'field'; if ($null -ne $tlf) { X "$indent`t`t`t<dcscor:field>$(Esc-XmlText "$tlf")</dcscor:field>" }
|
||||||
$tli = Get-Prop $tl 'linkItem'; if ($null -ne $tli) { X "$indent`t`t`t<dcscor:linkItem>$(Esc-Xml "$tli")</dcscor:linkItem>" }
|
$tli = Get-Prop $tl 'linkItem'; if ($null -ne $tli) { X "$indent`t`t`t<dcscor:linkItem>$(Esc-XmlText "$tli")</dcscor:linkItem>" }
|
||||||
X "$indent`t`t</dcscor:value>"
|
X "$indent`t`t</dcscor:value>"
|
||||||
} elseif (Has-DLProp $item 'value') {
|
} elseif (Has-DLProp $item 'value') {
|
||||||
$val = $item.value
|
$val = $item.value
|
||||||
if ($val -is [bool]) { X "$indent`t`t<dcscor:value xsi:type=`"xs:boolean`">$(if ($val) { 'true' } else { 'false' })</dcscor:value>" }
|
if ($val -is [bool]) { X "$indent`t`t<dcscor:value xsi:type=`"xs:boolean`">$(if ($val) { 'true' } else { 'false' })</dcscor:value>" }
|
||||||
elseif ($val -is [int] -or $val -is [long] -or $val -is [double] -or $val -is [decimal]) { X "$indent`t`t<dcscor:value xsi:type=`"xs:decimal`">$val</dcscor:value>" }
|
elseif ($val -is [int] -or $val -is [long] -or $val -is [double] -or $val -is [decimal]) { X "$indent`t`t<dcscor:value xsi:type=`"xs:decimal`">$val</dcscor:value>" }
|
||||||
elseif ($val -is [hashtable] -or $val -is [System.Collections.IDictionary] -or $val -is [PSCustomObject]) { Emit-DLMLText -tag "dcscor:value" -text $val -indent "$indent`t`t" }
|
elseif ($val -is [hashtable] -or $val -is [System.Collections.IDictionary] -or $val -is [PSCustomObject]) { Emit-DLMLText -tag "dcscor:value" -text $val -indent "$indent`t`t" }
|
||||||
else { X "$indent`t`t<dcscor:value xsi:type=`"xs:string`">$(Esc-Xml "$val")</dcscor:value>" }
|
else { X "$indent`t`t<dcscor:value xsi:type=`"xs:string`">$(Esc-XmlText "$val")</dcscor:value>" }
|
||||||
}
|
}
|
||||||
X "$indent`t</dcscor:item>"
|
X "$indent`t</dcscor:item>"
|
||||||
}
|
}
|
||||||
@@ -5557,16 +5615,16 @@ function Emit-DataParameters {
|
|||||||
}
|
}
|
||||||
X "$indent`t<dcscor:item xsi:type=`"dcsset:SettingsParameterValue`">"
|
X "$indent`t<dcscor:item xsi:type=`"dcsset:SettingsParameterValue`">"
|
||||||
if ($dp.use -eq $false) { X "$indent`t`t<dcscor:use>false</dcscor:use>" }
|
if ($dp.use -eq $false) { X "$indent`t`t<dcscor:use>false</dcscor:use>" }
|
||||||
X "$indent`t`t<dcscor:parameter>$(Esc-Xml "$($dp.parameter)")</dcscor:parameter>"
|
X "$indent`t`t<dcscor:parameter>$(Esc-XmlText "$($dp.parameter)")</dcscor:parameter>"
|
||||||
$dpValIsArr = ($dp.value -is [array]) -or ($dp.value -is [System.Collections.IList] -and $dp.value -isnot [string])
|
$dpValIsArr = ($dp.value -is [array]) -or ($dp.value -is [System.Collections.IList] -and $dp.value -isnot [string])
|
||||||
if ($dpValIsArr) {
|
if ($dpValIsArr) {
|
||||||
# Список значений параметра (valueListAllowed) — отдельный <dcscor:value> на каждое.
|
# Список значений параметра (valueListAllowed) — отдельный <dcscor:value> на каждое.
|
||||||
$avtype = "$($dp.valueType)"
|
$avtype = "$($dp.valueType)"
|
||||||
foreach ($v in @($dp.value)) {
|
foreach ($v in @($dp.value)) {
|
||||||
$vStr = if ($v -is [bool]) { "$v".ToLower() } else { "$v" }
|
$vStr = if ($v -is [bool]) { "$v".ToLower() } else { "$v" }
|
||||||
if ($avtype -match '^[a-zA-Z]+:') { X "$indent`t`t<dcscor:value xsi:type=`"$avtype`">$(Esc-Xml $vStr)</dcscor:value>" }
|
if ($avtype -match '^[a-zA-Z]+:') { X "$indent`t`t<dcscor:value xsi:type=`"$avtype`">$(Esc-XmlText $vStr)</dcscor:value>" }
|
||||||
elseif ("$vStr" -match '^(ПланСчетов|Справочник|Перечисление|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена)\.' -or "$vStr" -match '^(ChartOfAccounts|Catalog|Enum|Document|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.') { X "$indent`t`t<dcscor:value xsi:type=`"dcscor:DesignTimeValue`">$(Esc-Xml $vStr)</dcscor:value>" }
|
elseif ("$vStr" -match '^(ПланСчетов|Справочник|Перечисление|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена)\.' -or "$vStr" -match '^(ChartOfAccounts|Catalog|Enum|Document|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.') { X "$indent`t`t<dcscor:value xsi:type=`"dcscor:DesignTimeValue`">$(Esc-XmlText $vStr)</dcscor:value>" }
|
||||||
else { X "$indent`t`t<dcscor:value xsi:type=`"xs:string`">$(Esc-Xml $vStr)</dcscor:value>" }
|
else { X "$indent`t`t<dcscor:value xsi:type=`"xs:string`">$(Esc-XmlText $vStr)</dcscor:value>" }
|
||||||
}
|
}
|
||||||
} elseif ($dp.nilValue -eq $true) {
|
} elseif ($dp.nilValue -eq $true) {
|
||||||
X "$indent`t`t<dcscor:value xsi:nil=`"true`"/>"
|
X "$indent`t`t<dcscor:value xsi:nil=`"true`"/>"
|
||||||
@@ -5589,41 +5647,41 @@ function Emit-DataParameters {
|
|||||||
if ($dp.value -is [PSCustomObject] -and $dp.value.PSObject.Properties['date']) { $_d = "$($dp.value.date)" }
|
if ($dp.value -is [PSCustomObject] -and $dp.value.PSObject.Properties['date']) { $_d = "$($dp.value.date)" }
|
||||||
elseif (($dp.value -is [System.Collections.IDictionary]) -and $dp.value.Contains('date')) { $_d = "$($dp.value['date'])" }
|
elseif (($dp.value -is [System.Collections.IDictionary]) -and $dp.value.Contains('date')) { $_d = "$($dp.value['date'])" }
|
||||||
X "$indent`t`t<dcscor:value xsi:type=`"v8:StandardBeginningDate`">"
|
X "$indent`t`t<dcscor:value xsi:type=`"v8:StandardBeginningDate`">"
|
||||||
X "$indent`t`t`t<v8:variant xsi:type=`"v8:StandardBeginningDateVariant`">$(Esc-Xml $_variantStr)</v8:variant>"
|
X "$indent`t`t`t<v8:variant xsi:type=`"v8:StandardBeginningDateVariant`">$(Esc-XmlText $_variantStr)</v8:variant>"
|
||||||
if ($_variantStr -eq 'Custom') { if (-not $_d) { $_d = '0001-01-01T00:00:00' }; X "$indent`t`t`t<v8:date>$(Esc-Xml $_d)</v8:date>" }
|
if ($_variantStr -eq 'Custom') { if (-not $_d) { $_d = '0001-01-01T00:00:00' }; X "$indent`t`t`t<v8:date>$(Esc-XmlText $_d)</v8:date>" }
|
||||||
X "$indent`t`t</dcscor:value>"
|
X "$indent`t`t</dcscor:value>"
|
||||||
} else {
|
} else {
|
||||||
$_sd = $null; $_ed = $null
|
$_sd = $null; $_ed = $null
|
||||||
if ($dp.value -is [PSCustomObject]) { if ($dp.value.PSObject.Properties['startDate']) { $_sd = "$($dp.value.startDate)" }; if ($dp.value.PSObject.Properties['endDate']) { $_ed = "$($dp.value.endDate)" } }
|
if ($dp.value -is [PSCustomObject]) { if ($dp.value.PSObject.Properties['startDate']) { $_sd = "$($dp.value.startDate)" }; if ($dp.value.PSObject.Properties['endDate']) { $_ed = "$($dp.value.endDate)" } }
|
||||||
else { if ($dp.value.Contains('startDate')) { $_sd = "$($dp.value['startDate'])" }; if ($dp.value.Contains('endDate')) { $_ed = "$($dp.value['endDate'])" } }
|
else { if ($dp.value.Contains('startDate')) { $_sd = "$($dp.value['startDate'])" }; if ($dp.value.Contains('endDate')) { $_ed = "$($dp.value['endDate'])" } }
|
||||||
X "$indent`t`t<dcscor:value xsi:type=`"v8:StandardPeriod`">"
|
X "$indent`t`t<dcscor:value xsi:type=`"v8:StandardPeriod`">"
|
||||||
X "$indent`t`t`t<v8:variant xsi:type=`"v8:StandardPeriodVariant`">$(Esc-Xml $_variantStr)</v8:variant>"
|
X "$indent`t`t`t<v8:variant xsi:type=`"v8:StandardPeriodVariant`">$(Esc-XmlText $_variantStr)</v8:variant>"
|
||||||
if ($_variantStr -eq 'Custom') { if (-not $_sd) { $_sd = '0001-01-01T00:00:00' }; if (-not $_ed) { $_ed = '0001-01-01T00:00:00' }; X "$indent`t`t`t<v8:startDate>$(Esc-Xml $_sd)</v8:startDate>"; X "$indent`t`t`t<v8:endDate>$(Esc-Xml $_ed)</v8:endDate>" }
|
if ($_variantStr -eq 'Custom') { if (-not $_sd) { $_sd = '0001-01-01T00:00:00' }; if (-not $_ed) { $_ed = '0001-01-01T00:00:00' }; X "$indent`t`t`t<v8:startDate>$(Esc-XmlText $_sd)</v8:startDate>"; X "$indent`t`t`t<v8:endDate>$(Esc-XmlText $_ed)</v8:endDate>" }
|
||||||
X "$indent`t`t</dcscor:value>"
|
X "$indent`t`t</dcscor:value>"
|
||||||
}
|
}
|
||||||
} elseif ($vtype -match '^[a-zA-Z]+:') {
|
} elseif ($vtype -match '^[a-zA-Z]+:') {
|
||||||
$vStr = if ($dp.value -is [bool]) { "$($dp.value)".ToLower() } else { "$($dp.value)" }
|
$vStr = if ($dp.value -is [bool]) { "$($dp.value)".ToLower() } else { "$($dp.value)" }
|
||||||
X "$indent`t`t<dcscor:value xsi:type=`"$vtype`">$(Esc-Xml $vStr)</dcscor:value>"
|
X "$indent`t`t<dcscor:value xsi:type=`"$vtype`">$(Esc-XmlText $vStr)</dcscor:value>"
|
||||||
} elseif ($vtype -eq 'boolean' -or $dp.value -is [bool]) {
|
} elseif ($vtype -eq 'boolean' -or $dp.value -is [bool]) {
|
||||||
X "$indent`t`t<dcscor:value xsi:type=`"xs:boolean`">$(Esc-Xml ("$($dp.value)".ToLower()))</dcscor:value>"
|
X "$indent`t`t<dcscor:value xsi:type=`"xs:boolean`">$(Esc-XmlText ("$($dp.value)".ToLower()))</dcscor:value>"
|
||||||
} elseif ($vtype -match '^date' -or "$($dp.value)" -match '^\d{4}-\d{2}-\d{2}T') {
|
} elseif ($vtype -match '^date' -or "$($dp.value)" -match '^\d{4}-\d{2}-\d{2}T') {
|
||||||
X "$indent`t`t<dcscor:value xsi:type=`"xs:dateTime`">$(Esc-Xml "$($dp.value)")</dcscor:value>"
|
X "$indent`t`t<dcscor:value xsi:type=`"xs:dateTime`">$(Esc-XmlText "$($dp.value)")</dcscor:value>"
|
||||||
} elseif ($vtype -match '^decimal') {
|
} elseif ($vtype -match '^decimal') {
|
||||||
X "$indent`t`t<dcscor:value xsi:type=`"xs:decimal`">$(Esc-Xml "$($dp.value)")</dcscor:value>"
|
X "$indent`t`t<dcscor:value xsi:type=`"xs:decimal`">$(Esc-XmlText "$($dp.value)")</dcscor:value>"
|
||||||
} elseif ($vtype -match '^string') {
|
} elseif ($vtype -match '^string') {
|
||||||
X "$indent`t`t<dcscor:value xsi:type=`"xs:string`">$(Esc-Xml "$($dp.value)")</dcscor:value>"
|
X "$indent`t`t<dcscor:value xsi:type=`"xs:string`">$(Esc-XmlText "$($dp.value)")</dcscor:value>"
|
||||||
} elseif ("$($dp.value)" -match '^(ПланСчетов|Справочник|Перечисление|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена)\.' -or "$($dp.value)" -match '^(ChartOfAccounts|Catalog|Enum|Document|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.') {
|
} elseif ("$($dp.value)" -match '^(ПланСчетов|Справочник|Перечисление|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена)\.' -or "$($dp.value)" -match '^(ChartOfAccounts|Catalog|Enum|Document|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.') {
|
||||||
X "$indent`t`t<dcscor:value xsi:type=`"dcscor:DesignTimeValue`">$(Esc-Xml "$($dp.value)")</dcscor:value>"
|
X "$indent`t`t<dcscor:value xsi:type=`"dcscor:DesignTimeValue`">$(Esc-XmlText "$($dp.value)")</dcscor:value>"
|
||||||
} else {
|
} else {
|
||||||
X "$indent`t`t<dcscor:value xsi:type=`"xs:string`">$(Esc-Xml "$($dp.value)")</dcscor:value>"
|
X "$indent`t`t<dcscor:value xsi:type=`"xs:string`">$(Esc-XmlText "$($dp.value)")</dcscor:value>"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ($dp.viewMode) { X "$indent`t`t<dcsset:viewMode>$(Esc-Xml "$($dp.viewMode)")</dcsset:viewMode>" }
|
if ($dp.viewMode) { X "$indent`t`t<dcsset:viewMode>$(Esc-XmlText "$($dp.viewMode)")</dcsset:viewMode>" }
|
||||||
if ($dp.userSettingID) { $uid = if ("$($dp.userSettingID)" -eq "auto") { New-Guid-String } else { "$($dp.userSettingID)" }; X "$indent`t`t<dcsset:userSettingID>$(Esc-Xml $uid)</dcsset:userSettingID>" }
|
if ($dp.userSettingID) { $uid = if ("$($dp.userSettingID)" -eq "auto") { New-Guid-String } else { "$($dp.userSettingID)" }; X "$indent`t`t<dcsset:userSettingID>$(Esc-XmlText $uid)</dcsset:userSettingID>" }
|
||||||
if ($dp.userSettingPresentation) { Emit-USPresentation -val $dp.userSettingPresentation -tag "dcsset:userSettingPresentation" -indent "$indent`t`t" }
|
if ($dp.userSettingPresentation) { Emit-USPresentation -val $dp.userSettingPresentation -tag "dcsset:userSettingPresentation" -indent "$indent`t`t" }
|
||||||
X "$indent`t</dcscor:item>"
|
X "$indent`t</dcscor:item>"
|
||||||
}
|
}
|
||||||
if ($null -ne $blockViewMode) { X "$indent`t<dcsset:viewMode>$(Esc-Xml "$blockViewMode")</dcsset:viewMode>" }
|
if ($null -ne $blockViewMode) { X "$indent`t<dcsset:viewMode>$(Esc-XmlText "$blockViewMode")</dcsset:viewMode>" }
|
||||||
X "$indent</dcsset:dataParameters>"
|
X "$indent</dcsset:dataParameters>"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5631,7 +5689,7 @@ function Emit-DLParameter {
|
|||||||
param($p, $parsed, [string]$indent)
|
param($p, $parsed, [string]$indent)
|
||||||
X "$indent<Parameter>"
|
X "$indent<Parameter>"
|
||||||
$ci = "$indent`t"
|
$ci = "$indent`t"
|
||||||
X "$ci<dcssch:name>$(Esc-Xml $parsed.name)</dcssch:name>"
|
X "$ci<dcssch:name>$(Esc-XmlText $parsed.name)</dcssch:name>"
|
||||||
# Title: явный override (shorthand [..] / объект title/presentation) или авто из имени.
|
# Title: явный override (shorthand [..] / объект title/presentation) или авто из имени.
|
||||||
$title = $null
|
$title = $null
|
||||||
if ($parsed.title) { $title = $parsed.title }
|
if ($parsed.title) { $title = $parsed.title }
|
||||||
@@ -5665,7 +5723,7 @@ function Emit-DLParameter {
|
|||||||
# expression
|
# expression
|
||||||
$expr = $null
|
$expr = $null
|
||||||
if ($p -isnot [string] -and (Has-DLProp $p 'expression') -and $p.expression) { $expr = "$($p.expression)" }
|
if ($p -isnot [string] -and (Has-DLProp $p 'expression') -and $p.expression) { $expr = "$($p.expression)" }
|
||||||
if ($expr) { X "$ci<dcssch:expression>$(Esc-Xml $expr)</dcssch:expression>" }
|
if ($expr) { X "$ci<dcssch:expression>$(Esc-XmlText $expr)</dcssch:expression>" }
|
||||||
# availableValues
|
# availableValues
|
||||||
if ($p -isnot [string] -and (Has-DLProp $p 'availableValues') -and $p.availableValues) {
|
if ($p -isnot [string] -and (Has-DLProp $p 'availableValues') -and $p.availableValues) {
|
||||||
foreach ($av in @($p.availableValues)) { Emit-DLAvailableValue -av $av -type $parsed.type -indent $ci }
|
foreach ($av in @($p.availableValues)) { Emit-DLAvailableValue -av $av -type $parsed.type -indent $ci }
|
||||||
@@ -5684,7 +5742,7 @@ function Emit-DLParameter {
|
|||||||
# use
|
# use
|
||||||
$useVal = $null
|
$useVal = $null
|
||||||
if ($p -isnot [string] -and (Has-DLProp $p 'use') -and $p.use) { $useVal = "$($p.use)" }
|
if ($p -isnot [string] -and (Has-DLProp $p 'use') -and $p.use) { $useVal = "$($p.use)" }
|
||||||
if ($useVal) { X "$ci<dcssch:use>$(Esc-Xml $useVal)</dcssch:use>" }
|
if ($useVal) { X "$ci<dcssch:use>$(Esc-XmlText $useVal)</dcssch:use>" }
|
||||||
X "$indent</Parameter>"
|
X "$indent</Parameter>"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5804,7 +5862,7 @@ function Emit-Attributes {
|
|||||||
}
|
}
|
||||||
if ($saveFields.Count -gt 0) {
|
if ($saveFields.Count -gt 0) {
|
||||||
X "$inner<Save>"
|
X "$inner<Save>"
|
||||||
foreach ($f in $saveFields) { X "$inner`t<Field>$(Esc-Xml $f)</Field>" }
|
foreach ($f in $saveFields) { X "$inner`t<Field>$(Esc-XmlText $f)</Field>" }
|
||||||
X "$inner</Save>"
|
X "$inner</Save>"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5868,9 +5926,17 @@ function Emit-Attributes {
|
|||||||
}
|
}
|
||||||
if ($hasAddCols) {
|
if ($hasAddCols) {
|
||||||
foreach ($ac in @($attr.additionalColumns)) {
|
foreach ($ac in @($attr.additionalColumns)) {
|
||||||
|
# Пустой список колонок задаётся ЯВНО (`"columns": []`) — это законная форма,
|
||||||
|
# платформа так пишет таблицу, у которой доп. колонок нет. А вот отсутствие ключа
|
||||||
|
# — недосказанность автора: «доп. колонки есть», а какие, не указано. Раньше на
|
||||||
|
# этом PS падал с «Не удается индексировать в массив NULL» (@($null).Count = 1).
|
||||||
|
if ($null -eq $ac.PSObject.Properties['columns'] -or $null -eq $ac.columns) {
|
||||||
|
Write-Error "additionalColumns group for table '$($ac.table)': key 'columns' is missing — list the columns, or pass an empty array for a table without extra columns"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
$acCols = @($ac.columns)
|
$acCols = @($ac.columns)
|
||||||
if ($acCols.Count -eq 0) {
|
if ($acCols.Count -eq 0) {
|
||||||
# Пустая группа доп.колонок (table-ref без колонок) → self-closing (как платформа)
|
# Явно пустая группа → self-closing (как платформа)
|
||||||
X "$inner`t<AdditionalColumns table=`"$($ac.table)`"/>"
|
X "$inner`t<AdditionalColumns table=`"$($ac.table)`"/>"
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -5905,7 +5971,7 @@ function Emit-Attributes {
|
|||||||
X "$si<DynamicDataRead>$ddr</DynamicDataRead>"
|
X "$si<DynamicDataRead>$ddr</DynamicDataRead>"
|
||||||
if ($hasQuery) {
|
if ($hasQuery) {
|
||||||
$qtext = Resolve-QueryValue "$($st.query)" $script:queryBaseDir
|
$qtext = Resolve-QueryValue "$($st.query)" $script:queryBaseDir
|
||||||
X "$si<QueryText>$(Esc-Xml $qtext)</QueryText>"
|
X "$si<QueryText>$(Esc-XmlText $qtext)</QueryText>"
|
||||||
}
|
}
|
||||||
# Явные поля набора (редко): override title/dataPath
|
# Явные поля набора (редко): override title/dataPath
|
||||||
if ($st.fields) {
|
if ($st.fields) {
|
||||||
@@ -5920,8 +5986,8 @@ function Emit-Attributes {
|
|||||||
if ($null -ne (Get-Prop $fld 'dataPath')) { $dp = "$($fld.dataPath)" }
|
if ($null -ne (Get-Prop $fld 'dataPath')) { $dp = "$($fld.dataPath)" }
|
||||||
elseif ($isFolder) { $dp = "" }
|
elseif ($isFolder) { $dp = "" }
|
||||||
else { $dp = "$($fld.field)" }
|
else { $dp = "$($fld.field)" }
|
||||||
if ($dp -eq "") { X "$si`t<dcssch:dataPath/>" } else { X "$si`t<dcssch:dataPath>$(Esc-Xml "$dp")</dcssch:dataPath>" }
|
if ($dp -eq "") { X "$si`t<dcssch:dataPath/>" } else { X "$si`t<dcssch:dataPath>$(Esc-XmlText "$dp")</dcssch:dataPath>" }
|
||||||
if (-not $isFolder) { X "$si`t<dcssch:field>$(Esc-Xml "$($fld.field)")</dcssch:field>" }
|
if (-not $isFolder) { X "$si`t<dcssch:field>$(Esc-XmlText "$($fld.field)")</dcssch:field>" }
|
||||||
if ($fld.title) {
|
if ($fld.title) {
|
||||||
X "$si`t<dcssch:title xsi:type=`"v8:LocalStringType`">"
|
X "$si`t<dcssch:title xsi:type=`"v8:LocalStringType`">"
|
||||||
Emit-MLItems -val $fld.title -indent "$si`t`t"
|
Emit-MLItems -val $fld.title -indent "$si`t`t"
|
||||||
@@ -5931,7 +5997,7 @@ function Emit-Attributes {
|
|||||||
Emit-RestrictBlock 'useRestriction' $fld.useRestriction "$si`t"
|
Emit-RestrictBlock 'useRestriction' $fld.useRestriction "$si`t"
|
||||||
Emit-RestrictBlock 'attributeUseRestriction' $fld.attributeUseRestriction "$si`t"
|
Emit-RestrictBlock 'attributeUseRestriction' $fld.attributeUseRestriction "$si`t"
|
||||||
# presentationExpression поля — перед valueType (порядок исходника)
|
# presentationExpression поля — перед valueType (порядок исходника)
|
||||||
if ($fld.presentationExpression) { X "$si`t<dcssch:presentationExpression>$(Esc-Xml "$($fld.presentationExpression)")</dcssch:presentationExpression>" }
|
if ($fld.presentationExpression) { X "$si`t<dcssch:presentationExpression>$(Esc-XmlText "$($fld.presentationExpression)")</dcssch:presentationExpression>" }
|
||||||
# valueType поля набора (тип значения; вычисляемые/кастомные поля)
|
# valueType поля набора (тип значения; вычисляемые/кастомные поля)
|
||||||
if ($fld.valueType) { Emit-DLValueType -typeStr "$($fld.valueType)" -indent "$si`t" }
|
if ($fld.valueType) { Emit-DLValueType -typeStr "$($fld.valueType)" -indent "$si`t" }
|
||||||
# appearance поля (формат/оформление) — после valueType (порядок исходника)
|
# appearance поля (формат/оформление) — после valueType (порядок исходника)
|
||||||
@@ -5951,8 +6017,8 @@ function Emit-Attributes {
|
|||||||
Emit-DLParameters -params $st.parameters -indent $si
|
Emit-DLParameters -params $st.parameters -indent $si
|
||||||
# Ключ набора (query-based список без MainTable): KeyType (RowNumber/FieldValue/RowKey)
|
# Ключ набора (query-based список без MainTable): KeyType (RowNumber/FieldValue/RowKey)
|
||||||
# + KeyField* — после Parameter*, до MainTable. Захват/эмит факт. значений.
|
# + KeyField* — после Parameter*, до MainTable. Захват/эмит факт. значений.
|
||||||
if ($st.keyType) { X "$si<KeyType>$(Esc-Xml "$($st.keyType)")</KeyType>" }
|
if ($st.keyType) { X "$si<KeyType>$(Esc-XmlText "$($st.keyType)")</KeyType>" }
|
||||||
if ($st.keyFields) { foreach ($kf in @($st.keyFields)) { X "$si<KeyField>$(Esc-Xml "$kf")</KeyField>" } }
|
if ($st.keyFields) { foreach ($kf in @($st.keyFields)) { X "$si<KeyField>$(Esc-XmlText "$kf")</KeyField>" } }
|
||||||
if ($st.mainTable) { X "$si<MainTable>$(Normalize-MetaTypeRef "$($st.mainTable)")</MainTable>" }
|
if ($st.mainTable) { X "$si<MainTable>$(Normalize-MetaTypeRef "$($st.mainTable)")</MainTable>" }
|
||||||
# GetInvisibleFieldPresentations — после MainTable (дефолт true; эмитим только при заданном ключе = отклонении false).
|
# GetInvisibleFieldPresentations — после MainTable (дефолт true; эмитим только при заданном ключе = отклонении false).
|
||||||
if ($null -ne $st.getInvisibleFieldPresentations) { X "$si<GetInvisibleFieldPresentations>$(if ($st.getInvisibleFieldPresentations){'true'}else{'false'})</GetInvisibleFieldPresentations>" }
|
if ($null -ne $st.getInvisibleFieldPresentations) { X "$si<GetInvisibleFieldPresentations>$(if ($st.getInvisibleFieldPresentations){'true'}else{'false'})</GetInvisibleFieldPresentations>" }
|
||||||
@@ -6089,7 +6155,7 @@ function Emit-Commands {
|
|||||||
if (-not $cmdTable) { $cmdTable = $cmd.associatedTableElementId }
|
if (-not $cmdTable) { $cmdTable = $cmd.associatedTableElementId }
|
||||||
if (-not $cmdTable) { $cmdTable = $cmd.используемаяТаблица }
|
if (-not $cmdTable) { $cmdTable = $cmd.используемаяТаблица }
|
||||||
if ($cmdTable) {
|
if ($cmdTable) {
|
||||||
X "$inner<AssociatedTableElementId xsi:type=`"xs:string`">$(Esc-Xml "$cmdTable")</AssociatedTableElementId>"
|
X "$inner<AssociatedTableElementId xsi:type=`"xs:string`">$(Esc-XmlText "$cmdTable")</AssociatedTableElementId>"
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($cmd.shortcut) {
|
if ($cmd.shortcut) {
|
||||||
@@ -6181,10 +6247,10 @@ function Emit-CommandInterface {
|
|||||||
# group из дерева побеждает (если задан и непустой); явный group элемента — фолбэк
|
# group из дерева побеждает (если задан и непустой); явный group элемента — фолбэк
|
||||||
if ($treeGroup) { $grp = $treeGroup }
|
if ($treeGroup) { $grp = $treeGroup }
|
||||||
X "$inner`t<Item>"
|
X "$inner`t<Item>"
|
||||||
X "$inner`t`t<Command>$(Esc-Xml "$cmd")</Command>"
|
X "$inner`t`t<Command>$(Esc-XmlText "$cmd")</Command>"
|
||||||
X "$inner`t`t<Type>$type</Type>"
|
X "$inner`t`t<Type>$type</Type>"
|
||||||
if ($attr) { X "$inner`t`t<Attribute>$(Esc-Xml "$attr")</Attribute>" }
|
if ($attr) { X "$inner`t`t<Attribute>$(Esc-XmlText "$attr")</Attribute>" }
|
||||||
if ($grp) { X "$inner`t`t<CommandGroup>$(Esc-Xml "$grp")</CommandGroup>" }
|
if ($grp) { X "$inner`t`t<CommandGroup>$(Esc-XmlText "$grp")</CommandGroup>" }
|
||||||
if ($null -ne $idx) { X "$inner`t`t<Index>$idx</Index>" }
|
if ($null -ne $idx) { X "$inner`t`t<Index>$idx</Index>" }
|
||||||
if ($null -ne $dv) { X "$inner`t`t<DefaultVisible>$(if ($dv){'true'}else{'false'})</DefaultVisible>" }
|
if ($null -ne $dv) { X "$inner`t`t<DefaultVisible>$(if ($dv){'true'}else{'false'})</DefaultVisible>" }
|
||||||
if ($null -ne $vis) { Emit-XrFlag -tag 'Visible' -val $vis -indent "$inner`t`t" }
|
if ($null -ne $vis) { Emit-XrFlag -tag 'Visible' -val $vis -indent "$inner`t`t" }
|
||||||
@@ -6460,25 +6526,14 @@ function Compute-MainAcbAutofill {
|
|||||||
|
|
||||||
# --- 12. Main compilation ---
|
# --- 12. Main compilation ---
|
||||||
|
|
||||||
# Title
|
# Буфер и счётчики — с чистого листа: до этой точки они могли быть тронуты режимом from-object.
|
||||||
if ($def.title) {
|
|
||||||
Emit-MLText -tag "Title" -text $def.title -indent "`t"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Header
|
|
||||||
X '<?xml version="1.0" encoding="UTF-8"?>'
|
|
||||||
X "<Form xmlns=`"http://v8.1c.ru/8.3/xcf/logform`" xmlns:app=`"http://v8.1c.ru/8.2/managed-application/core`" xmlns:cfg=`"http://v8.1c.ru/8.1/data/enterprise/current-config`" xmlns:dcscor=`"http://v8.1c.ru/8.1/data-composition-system/core`" xmlns:dcssch=`"http://v8.1c.ru/8.1/data-composition-system/schema`" xmlns:dcsset=`"http://v8.1c.ru/8.1/data-composition-system/settings`" xmlns:ent=`"http://v8.1c.ru/8.1/data/enterprise`" xmlns:lf=`"http://v8.1c.ru/8.2/managed-application/logform`" xmlns:style=`"http://v8.1c.ru/8.1/data/ui/style`" xmlns:sys=`"http://v8.1c.ru/8.1/data/ui/fonts/system`" xmlns:v8=`"http://v8.1c.ru/8.1/data/core`" xmlns:v8ui=`"http://v8.1c.ru/8.1/data/ui`" xmlns:web=`"http://v8.1c.ru/8.1/data/ui/colors/web`" xmlns:win=`"http://v8.1c.ru/8.1/data/ui/colors/windows`" xmlns:xr=`"http://v8.1c.ru/8.3/xcf/readable`" xmlns:xs=`"http://www.w3.org/2001/XMLSchema`" xmlns:xsi=`"http://www.w3.org/2001/XMLSchema-instance`" version=`"$($script:formatVersion)`">"
|
|
||||||
|
|
||||||
# Oops — Title was emitted before header. Need to fix the order.
|
|
||||||
# Actually, let me restructure: build the body into a separate buffer, then assemble
|
|
||||||
|
|
||||||
# Reset and rebuild properly
|
|
||||||
$script:xml = New-Object System.Text.StringBuilder 8192
|
$script:xml = New-Object System.Text.StringBuilder 8192
|
||||||
$script:nextId = 1
|
$script:nextId = 1
|
||||||
$script:seenElementNames = @{} # пул имён элементов (глобально по всей форме)
|
$script:seenElementNames = @{} # пул имён элементов (глобально по всей форме)
|
||||||
|
|
||||||
|
# Header
|
||||||
X '<?xml version="1.0" encoding="UTF-8"?>'
|
X '<?xml version="1.0" encoding="UTF-8"?>'
|
||||||
X "<Form xmlns=`"http://v8.1c.ru/8.3/xcf/logform`" xmlns:app=`"http://v8.1c.ru/8.2/managed-application/core`" xmlns:cfg=`"http://v8.1c.ru/8.1/data/enterprise/current-config`" xmlns:dcscor=`"http://v8.1c.ru/8.1/data-composition-system/core`" xmlns:dcssch=`"http://v8.1c.ru/8.1/data-composition-system/schema`" xmlns:dcsset=`"http://v8.1c.ru/8.1/data-composition-system/settings`" xmlns:ent=`"http://v8.1c.ru/8.1/data/enterprise`" xmlns:lf=`"http://v8.1c.ru/8.2/managed-application/logform`" xmlns:style=`"http://v8.1c.ru/8.1/data/ui/style`" xmlns:sys=`"http://v8.1c.ru/8.1/data/ui/fonts/system`" xmlns:v8=`"http://v8.1c.ru/8.1/data/core`" xmlns:v8ui=`"http://v8.1c.ru/8.1/data/ui`" xmlns:web=`"http://v8.1c.ru/8.1/data/ui/colors/web`" xmlns:win=`"http://v8.1c.ru/8.1/data/ui/colors/windows`" xmlns:xr=`"http://v8.1c.ru/8.3/xcf/readable`" xmlns:xs=`"http://www.w3.org/2001/XMLSchema`" xmlns:xsi=`"http://www.w3.org/2001/XMLSchema-instance`" version=`"$($script:formatVersion)`">"
|
X "<Form $($script:formNsDecl) version=`"$($script:formatVersion)`">"
|
||||||
|
|
||||||
# 12a. Title (from def.title or properties.title — must be multilingual XML)
|
# 12a. Title (from def.title or properties.title — must be multilingual XML)
|
||||||
$formTitle = $def.title
|
$formTitle = $def.title
|
||||||
@@ -6532,7 +6587,7 @@ if ($null -ne $def.mobileCommandBarContent -and @($def.mobileCommandBarContent).
|
|||||||
X "`t`t`t<xr:CheckState>0</xr:CheckState>"
|
X "`t`t`t<xr:CheckState>0</xr:CheckState>"
|
||||||
# пустое значение → самозакрывающийся тег (зеркало платформы)
|
# пустое значение → самозакрывающийся тег (зеркало платформы)
|
||||||
if ([string]::IsNullOrEmpty("$nm")) { X "`t`t`t<xr:Value xsi:type=`"xs:string`"/>" }
|
if ([string]::IsNullOrEmpty("$nm")) { X "`t`t`t<xr:Value xsi:type=`"xs:string`"/>" }
|
||||||
else { X "`t`t`t<xr:Value xsi:type=`"xs:string`">$(Esc-Xml "$nm")</xr:Value>" }
|
else { X "`t`t`t<xr:Value xsi:type=`"xs:string`">$(Esc-XmlText "$nm")</xr:Value>" }
|
||||||
X "`t`t</xr:Item>"
|
X "`t`t</xr:Item>"
|
||||||
}
|
}
|
||||||
X "`t</MobileDeviceCommandBarContent>"
|
X "`t</MobileDeviceCommandBarContent>"
|
||||||
@@ -6619,7 +6674,7 @@ if (-not (Test-Path $outDir)) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$enc = New-Object System.Text.UTF8Encoding($true)
|
$enc = New-Object System.Text.UTF8Encoding($true)
|
||||||
[System.IO.File]::WriteAllText($outPath, $xml.ToString(), $enc)
|
[System.IO.File]::WriteAllText($outPath, $xml.ToString().TrimEnd("`r", "`n"), $enc)
|
||||||
|
|
||||||
# --- 13b. Auto-register form in parent object XML ---
|
# --- 13b. Auto-register form in parent object XML ---
|
||||||
|
|
||||||
@@ -6674,11 +6729,26 @@ if ($formsLeaf -eq 'Forms') {
|
|||||||
$regSettings = New-Object System.Xml.XmlWriterSettings
|
$regSettings = New-Object System.Xml.XmlWriterSettings
|
||||||
$regSettings.Encoding = $regEnc
|
$regSettings.Encoding = $regEnc
|
||||||
$regSettings.Indent = $false
|
$regSettings.Indent = $false
|
||||||
$regStream = New-Object System.IO.FileStream($objectXmlPath, [System.IO.FileMode]::Create)
|
$regSettings.NewLineHandling = [System.Xml.NewLineHandling]::None
|
||||||
$regWriter = [System.Xml.XmlWriter]::Create($regStream, $regSettings)
|
# Через MemoryStream, а не прямо в файл: нужен шаг пост-обработки строки.
|
||||||
|
$regMem = New-Object System.IO.MemoryStream
|
||||||
|
$regWriter = [System.Xml.XmlWriter]::Create($regMem, $regSettings)
|
||||||
$objDoc.Save($regWriter)
|
$objDoc.Save($regWriter)
|
||||||
$regWriter.Close()
|
$regWriter.Flush(); $regWriter.Close()
|
||||||
$regStream.Close()
|
|
||||||
|
$regText = [System.Text.Encoding]::UTF8.GetString($regMem.ToArray())
|
||||||
|
$regMem.Close()
|
||||||
|
if ($regText.Length -gt 0 -and $regText[0] -eq [char]0xFEFF) { $regText = $regText.Substring(1) }
|
||||||
|
$regText = $regText.Replace('encoding="utf-8"', 'encoding="UTF-8"')
|
||||||
|
# Пустой элемент: XmlWriter отдаёт `<a />`, Конфигуратор пишет `<a/>`. Внутри
|
||||||
|
# CDATA/комментария ` />` может быть содержимым (там `>` не экранируется),
|
||||||
|
# поэтому они идут первыми ветками альтернации и возвращаются как есть.
|
||||||
|
$regText = [regex]::Replace($regText, '(?s)<!\[CDATA\[.*?\]\]>|<!--.*?-->|(?<=\S) />', { param($m) if ($m.Value -eq ' />') { '/>' } else { $m.Value } })
|
||||||
|
# Целевой перевод строки: стиль файла-назначения — правка наследует его (#44/#46/#47),
|
||||||
|
# новый файл получает канон выгрузки CRLF. Зеркало _detect_xml_style в py-порту.
|
||||||
|
$targetEol = if ((Test-Path -LiteralPath $objectXmlPath) -and ([System.IO.File]::ReadAllText($objectXmlPath) -notmatch "`r`n")) { "`n" } else { "`r`n" }
|
||||||
|
$regText = ($regText -replace "`r`n", "`n") -replace "`n", $targetEol
|
||||||
|
[System.IO.File]::WriteAllText($objectXmlPath, $regText, $regEnc)
|
||||||
|
|
||||||
Write-Host " Registered: <Form>$formName</Form> in $objectName.xml"
|
Write-Host " Registered: <Form>$formName</Form> in $objectName.xml"
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
|||||||
# form-decompile v0.147 — Decompile 1C managed Form.xml to JSON DSL (draft)
|
# form-decompile v0.149 — Decompile 1C managed Form.xml to JSON DSL (draft)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
# ВНИМАНИЕ: раундтрип не гарантируется. Навык исключён из авто-использования моделью.
|
# ВНИМАНИЕ: раундтрип не гарантируется. Навык исключён из авто-использования моделью.
|
||||||
param(
|
param(
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# form-decompile v0.147 — Decompile 1C managed Form.xml to JSON DSL (draft)
|
# form-decompile v0.149 — Decompile 1C managed Form.xml to JSON DSL (draft)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
# ВНИМАНИЕ: раундтрип не гарантируется. Навык исключён из авто-использования моделью.
|
# ВНИМАНИЕ: раундтрип не гарантируется. Навык исключён из авто-использования моделью.
|
||||||
#
|
#
|
||||||
@@ -13,6 +13,28 @@ import xml.etree.ElementTree as ET
|
|||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
|
# Регистронезависимый ввод — паритет с 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)
|
||||||
|
|
||||||
|
|
||||||
# --- 1. Namespaces ---
|
# --- 1. Namespaces ---
|
||||||
NS_LF = "http://v8.1c.ru/8.3/xcf/logform"
|
NS_LF = "http://v8.1c.ru/8.3/xcf/logform"
|
||||||
NS_V8 = "http://v8.1c.ru/8.1/data/core"
|
NS_V8 = "http://v8.1c.ru/8.1/data/core"
|
||||||
@@ -81,29 +103,29 @@ def _attr(node, name, ns_uri=None):
|
|||||||
def convert_string_to_json_literal(s):
|
def convert_string_to_json_literal(s):
|
||||||
if s is None:
|
if s is None:
|
||||||
return 'null'
|
return 'null'
|
||||||
sb = ['"']
|
out = ['"']
|
||||||
for ch in s:
|
for ch in s:
|
||||||
code = ord(ch)
|
code = ord(ch)
|
||||||
if code == 0x22:
|
if code == 0x22:
|
||||||
sb.append('\\"')
|
out.append('\\"')
|
||||||
elif code == 0x5C:
|
elif code == 0x5C:
|
||||||
sb.append('\\\\')
|
out.append('\\\\')
|
||||||
elif code == 0x08:
|
elif code == 0x08:
|
||||||
sb.append('\\b')
|
out.append('\\b')
|
||||||
elif code == 0x09:
|
elif code == 0x09:
|
||||||
sb.append('\\t')
|
out.append('\\t')
|
||||||
elif code == 0x0A:
|
elif code == 0x0A:
|
||||||
sb.append('\\n')
|
out.append('\\n')
|
||||||
elif code == 0x0C:
|
elif code == 0x0C:
|
||||||
sb.append('\\f')
|
out.append('\\f')
|
||||||
elif code == 0x0D:
|
elif code == 0x0D:
|
||||||
sb.append('\\r')
|
out.append('\\r')
|
||||||
elif code < 0x20:
|
elif code < 0x20:
|
||||||
sb.append('\\u%04x' % code)
|
out.append('\\u%04x' % code)
|
||||||
else:
|
else:
|
||||||
sb.append(ch)
|
out.append(ch)
|
||||||
sb.append('"')
|
out.append('"')
|
||||||
return ''.join(sb)
|
return ''.join(out)
|
||||||
|
|
||||||
|
|
||||||
def _num_to_str(obj):
|
def _num_to_str(obj):
|
||||||
@@ -3115,7 +3137,7 @@ def main():
|
|||||||
parser = argparse.ArgumentParser(description='Decompile 1C managed Form.xml to JSON DSL', allow_abbrev=False)
|
parser = argparse.ArgumentParser(description='Decompile 1C managed Form.xml to JSON DSL', allow_abbrev=False)
|
||||||
parser.add_argument('-FormPath', '-Path', dest='FormPath', type=str, required=True)
|
parser.add_argument('-FormPath', '-Path', dest='FormPath', type=str, required=True)
|
||||||
parser.add_argument('-OutputPath', dest='OutputPath', type=str, default=None)
|
parser.add_argument('-OutputPath', dest='OutputPath', type=str, default=None)
|
||||||
args = parser.parse_args()
|
args = ci_parse_args(parser)
|
||||||
|
|
||||||
form_path = args.FormPath
|
form_path = args.FormPath
|
||||||
output_path = args.OutputPath
|
output_path = args.OutputPath
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# form-edit v1.5 — Edit 1C managed form elements
|
# form-edit v1.14 — Edit 1C managed form elements (+esc_xml/esc_xml_text: разное экранирование атрибута и текста)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory)]
|
||||||
@@ -271,15 +271,24 @@ function X {
|
|||||||
|
|
||||||
function Esc-Xml {
|
function Esc-Xml {
|
||||||
param([string]$s)
|
param([string]$s)
|
||||||
|
# Эскейп ЗНАЧЕНИЯ АТРИБУТА: & < > и кавычка — внутри "..." литеральная " невалидна.
|
||||||
return $s.Replace('&','&').Replace('<','<').Replace('>','>').Replace('"','"')
|
return $s.Replace('&','&').Replace('<','<').Replace('>','>').Replace('"','"')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Esc-XmlText {
|
||||||
|
# Экранирование ТЕКСТА элемента: только & < > . Кавычки в тексте платформа НЕ экранирует —
|
||||||
|
# пишет литерально (проверено: 92142 сырых кавычки на корпус, ни одной "). " платформа
|
||||||
|
# принимает, но при выгрузке нормализует обратно в кавычку → лишний шум в роундтрипе.
|
||||||
|
param([string]$s)
|
||||||
|
return $s.Replace('&','&').Replace('<','<').Replace('>','>')
|
||||||
|
}
|
||||||
|
|
||||||
function Emit-MLText {
|
function Emit-MLText {
|
||||||
param([string]$tag, [string]$text, [string]$indent)
|
param([string]$tag, [string]$text, [string]$indent)
|
||||||
X "$indent<$tag>"
|
X "$indent<$tag>"
|
||||||
X "$indent`t<v8:item>"
|
X "$indent`t<v8:item>"
|
||||||
X "$indent`t`t<v8:lang>ru</v8:lang>"
|
X "$indent`t`t<v8:lang>ru</v8:lang>"
|
||||||
X "$indent`t`t<v8:content>$(Esc-Xml $text)</v8:content>"
|
X "$indent`t`t<v8:content>$(Esc-XmlText $text)</v8:content>"
|
||||||
X "$indent`t</v8:item>"
|
X "$indent`t</v8:item>"
|
||||||
X "$indent</$tag>"
|
X "$indent</$tag>"
|
||||||
}
|
}
|
||||||
@@ -308,24 +317,49 @@ $script:formTypeSynonyms["бизнеспроцессссылка"] = "
|
|||||||
$script:formTypeSynonyms["задачассылка"] = "TaskRef"
|
$script:formTypeSynonyms["задачассылка"] = "TaskRef"
|
||||||
$script:formTypeSynonyms["определяемыйтип"] = "DefinedType"
|
$script:formTypeSynonyms["определяемыйтип"] = "DefinedType"
|
||||||
|
|
||||||
|
# Алиас на локальный словарь: тело Resolve-TypeStr ниже — общая реализация,
|
||||||
|
# одинаковая во всех навыках (реестр в tests/skills/check-inline-drift.mjs).
|
||||||
|
$script:typeSynonyms = $script:formTypeSynonyms
|
||||||
|
|
||||||
function Resolve-TypeStr {
|
function Resolve-TypeStr {
|
||||||
param([string]$typeStr)
|
param([string]$typeStr)
|
||||||
if (-not $typeStr) { return $typeStr }
|
if (-not $typeStr) { return $typeStr }
|
||||||
|
|
||||||
|
# Прощающий ввод: ведущий префикс приходит копипастой из выгрузки. Без срезания он ломает
|
||||||
|
# поиск в словаре — русское имя типа остаётся непереведённым, и платформа отвечает
|
||||||
|
# «Неизвестное имя типа». cfg: снимаем всегда — он однозначно означает текущую конфигурацию.
|
||||||
|
# Сгенерированный dNpM: (в корпусе на этом URI встречаются d4p1, d5p1, d6p1 — имя префикса
|
||||||
|
# платформа выдаёт по порядку объявления) снимаем ТОЛЬКО у ссылочных типов, с точкой:
|
||||||
|
# сам по себе префикс многозначен — в формах d5p1:Chart, d5p1:TextDocument,
|
||||||
|
# d5p1:GeographicalSchema адресуют чужие пространства имён, и там он часть значения.
|
||||||
|
if ($typeStr.StartsWith('cfg:')) {
|
||||||
|
$typeStr = $typeStr.Substring(4)
|
||||||
|
} elseif ($typeStr.Contains('.') -and $typeStr -match '^d\d+p\d+:') {
|
||||||
|
$typeStr = $typeStr.Substring($typeStr.IndexOf(':') + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Параметризованные типы: Number(15,2), Строка(100)
|
||||||
if ($typeStr -match '^([^(]+)\((.+)\)$') {
|
if ($typeStr -match '^([^(]+)\((.+)\)$') {
|
||||||
$base = $Matches[1].Trim(); $params = $Matches[2]
|
$baseName = $Matches[1].Trim()
|
||||||
$r = $script:formTypeSynonyms[$base.ToLower()]
|
$params = $Matches[2]
|
||||||
if ($r) { return "$r($params)" }
|
$resolved = $script:typeSynonyms[$baseName.ToLower()]
|
||||||
|
if ($resolved) { return "$resolved($params)" }
|
||||||
return $typeStr
|
return $typeStr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Ссылочные типы: СправочникСсылка.Организации → CatalogRef.Организации
|
||||||
if ($typeStr.Contains('.')) {
|
if ($typeStr.Contains('.')) {
|
||||||
$i = $typeStr.IndexOf('.')
|
$dotIdx = $typeStr.IndexOf('.')
|
||||||
$prefix = $typeStr.Substring(0, $i); $suffix = $typeStr.Substring($i)
|
$prefix = $typeStr.Substring(0, $dotIdx)
|
||||||
$r = $script:formTypeSynonyms[$prefix.ToLower()]
|
$suffix = $typeStr.Substring($dotIdx) # includes the dot
|
||||||
if ($r) { return "$r$suffix" }
|
$resolved = $script:typeSynonyms[$prefix.ToLower()]
|
||||||
|
if ($resolved) { return "$resolved$suffix" }
|
||||||
return $typeStr
|
return $typeStr
|
||||||
}
|
}
|
||||||
$r = $script:formTypeSynonyms[$typeStr.ToLower()]
|
|
||||||
if ($r) { return $r }
|
# Простое имя
|
||||||
|
$resolved = $script:typeSynonyms[$typeStr.ToLower()]
|
||||||
|
if ($resolved) { return $resolved }
|
||||||
return $typeStr
|
return $typeStr
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -589,7 +623,7 @@ function Emit-Label {
|
|||||||
X "$inner<Title formatted=`"$formatted`">"
|
X "$inner<Title formatted=`"$formatted`">"
|
||||||
X "$inner`t<v8:item>"
|
X "$inner`t<v8:item>"
|
||||||
X "$inner`t`t<v8:lang>ru</v8:lang>"
|
X "$inner`t`t<v8:lang>ru</v8:lang>"
|
||||||
X "$inner`t`t<v8:content>$(Esc-Xml "$($el.title)")</v8:content>"
|
X "$inner`t`t<v8:content>$(Esc-XmlText "$($el.title)")</v8:content>"
|
||||||
X "$inner`t</v8:item>"
|
X "$inner`t</v8:item>"
|
||||||
X "$inner</Title>"
|
X "$inner</Title>"
|
||||||
}
|
}
|
||||||
@@ -1384,8 +1418,16 @@ if ($def.elementEvents -and $def.elementEvents.Count -gt 0) {
|
|||||||
$content = $xmlDoc.OuterXml
|
$content = $xmlDoc.OuterXml
|
||||||
# Ensure encoding declaration is uppercase UTF-8
|
# Ensure encoding declaration is uppercase UTF-8
|
||||||
$content = $content -replace '^<\?xml version="1.0" encoding="utf-8"\?>', '<?xml version="1.0" encoding="UTF-8"?>'
|
$content = $content -replace '^<\?xml version="1.0" encoding="utf-8"\?>', '<?xml version="1.0" encoding="UTF-8"?>'
|
||||||
|
# Пустой элемент: XmlWriter отдаёт `<a />`, Конфигуратор пишет `<a/>`. Внутри
|
||||||
|
# CDATA/комментария ` />` может быть содержимым (там `>` не экранируется),
|
||||||
|
# поэтому они идут первыми ветками альтернации и возвращаются как есть.
|
||||||
|
$content = [regex]::Replace($content, '(?s)<!\[CDATA\[.*?\]\]>|<!--.*?-->|(?<=\S) />', { param($m) if ($m.Value -eq ' />') { '/>' } else { $m.Value } })
|
||||||
|
|
||||||
$enc = New-Object System.Text.UTF8Encoding($true)
|
$enc = New-Object System.Text.UTF8Encoding($true)
|
||||||
|
# Целевой перевод строки: стиль файла-назначения — правка наследует его (#44/#46/#47),
|
||||||
|
# новый файл получает канон выгрузки CRLF. Зеркало _detect_xml_style в py-порту.
|
||||||
|
$targetEol = if ((Test-Path -LiteralPath $resolvedFormPath) -and ([System.IO.File]::ReadAllText($resolvedFormPath) -notmatch "`r`n")) { "`n" } else { "`r`n" }
|
||||||
|
$content = ($content -replace "`r`n", "`n") -replace "`n", $targetEol
|
||||||
[System.IO.File]::WriteAllText($resolvedFormPath, $content, $enc)
|
[System.IO.File]::WriteAllText($resolvedFormPath, $content, $enc)
|
||||||
|
|
||||||
# === 14. Summary ===
|
# === 14. Summary ===
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# form-edit v1.5 — Edit 1C managed form elements (Python port)
|
# form-edit v1.14 — Edit 1C managed form elements (Python port) (+esc_xml/esc_xml_text: разное экранирование атрибута и текста)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
import argparse
|
import argparse
|
||||||
import json
|
import json
|
||||||
@@ -11,6 +11,65 @@ from lxml import etree
|
|||||||
sys.stdout.reconfigure(encoding="utf-8")
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
sys.stderr.reconfigure(encoding="utf-8")
|
||||||
|
|
||||||
|
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
|
||||||
|
# регистр не различают, в argparse совпадение точное.
|
||||||
|
class CIDict(dict):
|
||||||
|
# Ключи храним КАК ЕСТЬ: часть из них — имена объектов (табличные части, стандартные
|
||||||
|
# реквизиты), они попадают в XML. Регистронезависим только поиск. Порядок вставки
|
||||||
|
# сохраняется — от него зависит порядок эмиссии.
|
||||||
|
def _actual(self, key):
|
||||||
|
if not isinstance(key, str) or dict.__contains__(self, key):
|
||||||
|
return key
|
||||||
|
ci = self.__dict__.get('_ci')
|
||||||
|
if ci is None or len(ci) != len(self):
|
||||||
|
ci = {k.lower(): k for k in self if isinstance(k, str)}
|
||||||
|
self.__dict__['_ci'] = ci
|
||||||
|
return ci.get(key.lower(), key)
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
return dict.__getitem__(self, self._actual(key))
|
||||||
|
|
||||||
|
def __contains__(self, key):
|
||||||
|
return dict.__contains__(self, self._actual(key))
|
||||||
|
|
||||||
|
def get(self, key, default=None):
|
||||||
|
return dict.get(self, self._actual(key), default)
|
||||||
|
|
||||||
|
def pop(self, key, *default):
|
||||||
|
return dict.pop(self, self._actual(key), *default)
|
||||||
|
|
||||||
|
def __setitem__(self, key, value):
|
||||||
|
# запись по ключу, отличающемуся регистром, обновляет существующий, а не плодит дубль
|
||||||
|
dict.__setitem__(self, self._actual(key), value)
|
||||||
|
|
||||||
|
def ci_json(obj):
|
||||||
|
"""Рекурсивно оборачивает разобранный JSON: словари → CIDict, списки обходятся."""
|
||||||
|
if isinstance(obj, dict):
|
||||||
|
return CIDict((k, ci_json(v)) for k, v in obj.items())
|
||||||
|
if isinstance(obj, list):
|
||||||
|
return [ci_json(v) for v in obj]
|
||||||
|
return obj
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# Support guard (Ext/ParentConfigurations.bin) — see docs/1c-support-state-spec.md
|
# Support guard (Ext/ParentConfigurations.bin) — see docs/1c-support-state-spec.md
|
||||||
# Blocks edits of vendor objects "на замке" / read-only configs. Trigger = bin
|
# Blocks edits of vendor objects "на замке" / read-only configs. Trigger = bin
|
||||||
@@ -192,7 +251,7 @@ def assert_edit_allowed(target_path, require):
|
|||||||
parser = argparse.ArgumentParser(allow_abbrev=False)
|
parser = argparse.ArgumentParser(allow_abbrev=False)
|
||||||
parser.add_argument("-FormPath", "-Path", required=True)
|
parser.add_argument("-FormPath", "-Path", required=True)
|
||||||
parser.add_argument("-JsonPath", required=True)
|
parser.add_argument("-JsonPath", required=True)
|
||||||
args = parser.parse_args()
|
args = ci_parse_args(parser)
|
||||||
|
|
||||||
form_path = args.FormPath
|
form_path = args.FormPath
|
||||||
json_path = args.JsonPath
|
json_path = args.JsonPath
|
||||||
@@ -226,9 +285,16 @@ def local_name(node):
|
|||||||
# ── helpers ──────────────────────────────────────────────────
|
# ── helpers ──────────────────────────────────────────────────
|
||||||
|
|
||||||
def esc_xml(s):
|
def esc_xml(s):
|
||||||
|
# Эскейп ЗНАЧЕНИЯ АТРИБУТА: & < > и кавычка — внутри "..." литеральная " невалидна.
|
||||||
return s.replace('&', '&').replace('<', '<').replace('>', '>').replace('"', '"')
|
return s.replace('&', '&').replace('<', '<').replace('>', '>').replace('"', '"')
|
||||||
|
|
||||||
|
|
||||||
|
def esc_xml_text(s):
|
||||||
|
"""Экранирование ТЕКСТА элемента: только & < > . Кавычки платформа в тексте не экранирует
|
||||||
|
(92142 сырых кавычки на корпус, ни одной "); " она принимает, но нормализует обратно."""
|
||||||
|
return s.replace('&', '&').replace('<', '<').replace('>', '>')
|
||||||
|
|
||||||
|
|
||||||
# ── 1. Load Form.xml ────────────────────────────────────────
|
# ── 1. Load Form.xml ────────────────────────────────────────
|
||||||
|
|
||||||
if not os.path.exists(form_path):
|
if not os.path.exists(form_path):
|
||||||
@@ -252,7 +318,7 @@ root = tree.getroot()
|
|||||||
# ── 2. Load JSON ────────────────────────────────────────────
|
# ── 2. Load JSON ────────────────────────────────────────────
|
||||||
|
|
||||||
with open(json_path, "r", encoding="utf-8-sig") as f:
|
with open(json_path, "r", encoding="utf-8-sig") as f:
|
||||||
defn = json.load(f)
|
defn = ci_json(json.load(f))
|
||||||
|
|
||||||
# ── 3. Form name + header ───────────────────────────────────
|
# ── 3. Form name + header ───────────────────────────────────
|
||||||
|
|
||||||
@@ -384,23 +450,48 @@ _FORM_TYPE_SYNONYMS = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Алиас на локальный словарь: тело resolve_type_str ниже — общая реализация,
|
||||||
|
# одинаковая во всех навыках (реестр в tests/skills/check-inline-drift.mjs).
|
||||||
|
TYPE_SYNONYMS = _FORM_TYPE_SYNONYMS
|
||||||
|
|
||||||
|
|
||||||
def resolve_type_str(type_str):
|
def resolve_type_str(type_str):
|
||||||
if not type_str:
|
if not type_str:
|
||||||
return type_str
|
return type_str
|
||||||
|
# Прощающий ввод: ведущий префикс приходит копипастой из выгрузки. Без срезания он ломает
|
||||||
|
# поиск в словаре — русское имя типа остаётся непереведённым, и платформа отвечает
|
||||||
|
# «Неизвестное имя типа». cfg: снимаем всегда — он однозначно означает текущую конфигурацию.
|
||||||
|
# Сгенерированный dNpM: (в корпусе на этом URI встречаются d4p1, d5p1, d6p1 — имя префикса
|
||||||
|
# платформа выдаёт по порядку объявления) снимаем ТОЛЬКО у ссылочных типов, с точкой:
|
||||||
|
# сам по себе префикс многозначен — в формах d5p1:Chart, d5p1:TextDocument,
|
||||||
|
# d5p1:GeographicalSchema адресуют чужие пространства имён, и там он часть значения.
|
||||||
|
if type_str.startswith('cfg:'):
|
||||||
|
type_str = type_str[4:]
|
||||||
|
elif '.' in type_str and re.match(r'^d\d+p\d+:', type_str):
|
||||||
|
type_str = type_str[type_str.index(':') + 1:]
|
||||||
|
# Параметризованные типы: Number(15,2), Строка(100)
|
||||||
m = re.match(r'^([^(]+)\((.+)\)$', type_str)
|
m = re.match(r'^([^(]+)\((.+)\)$', type_str)
|
||||||
if m:
|
if m:
|
||||||
base, params = m.group(1).strip(), m.group(2)
|
base_name = m.group(1).strip()
|
||||||
r = _FORM_TYPE_SYNONYMS.get(base.lower())
|
params = m.group(2)
|
||||||
return f"{r}({params})" if r else type_str
|
resolved = TYPE_SYNONYMS.get(base_name.lower())
|
||||||
|
if resolved:
|
||||||
|
return f'{resolved}({params})'
|
||||||
|
return type_str
|
||||||
|
# Ссылочные типы: СправочникСсылка.Организации -> CatalogRef.Организации
|
||||||
if '.' in type_str:
|
if '.' in type_str:
|
||||||
i = type_str.index('.')
|
dot_idx = type_str.index('.')
|
||||||
prefix, suffix = type_str[:i], type_str[i:]
|
prefix = type_str[:dot_idx]
|
||||||
r = _FORM_TYPE_SYNONYMS.get(prefix.lower())
|
suffix = type_str[dot_idx:] # includes the dot
|
||||||
return f"{r}{suffix}" if r else type_str
|
resolved = TYPE_SYNONYMS.get(prefix.lower())
|
||||||
r = _FORM_TYPE_SYNONYMS.get(type_str.lower())
|
if resolved:
|
||||||
return r if r else type_str
|
return f'{resolved}{suffix}'
|
||||||
|
return type_str
|
||||||
|
# Простое имя
|
||||||
|
resolved = TYPE_SYNONYMS.get(type_str.lower())
|
||||||
|
if resolved:
|
||||||
|
return resolved
|
||||||
|
return type_str
|
||||||
def emit_type(type_str, indent):
|
def emit_type(type_str, indent):
|
||||||
if not type_str:
|
if not type_str:
|
||||||
X(f"{indent}<Type/>")
|
X(f"{indent}<Type/>")
|
||||||
@@ -494,7 +585,7 @@ def emit_mltext(tag, text, indent):
|
|||||||
X(f"{indent}<{tag}>")
|
X(f"{indent}<{tag}>")
|
||||||
X(f"{indent}\t<v8:item>")
|
X(f"{indent}\t<v8:item>")
|
||||||
X(f"{indent}\t\t<v8:lang>ru</v8:lang>")
|
X(f"{indent}\t\t<v8:lang>ru</v8:lang>")
|
||||||
X(f"{indent}\t\t<v8:content>{esc_xml(text)}</v8:content>")
|
X(f"{indent}\t\t<v8:content>{esc_xml_text(text)}</v8:content>")
|
||||||
X(f"{indent}\t</v8:item>")
|
X(f"{indent}\t</v8:item>")
|
||||||
X(f"{indent}</{tag}>")
|
X(f"{indent}</{tag}>")
|
||||||
|
|
||||||
@@ -722,7 +813,7 @@ def emit_label(el, name, _id, indent):
|
|||||||
X(f'{inner}<Title formatted="{formatted}">')
|
X(f'{inner}<Title formatted="{formatted}">')
|
||||||
X(f"{inner}\t<v8:item>")
|
X(f"{inner}\t<v8:item>")
|
||||||
X(f"{inner}\t\t<v8:lang>ru</v8:lang>")
|
X(f"{inner}\t\t<v8:lang>ru</v8:lang>")
|
||||||
X(f"{inner}\t\t<v8:content>{esc_xml(str(el['title']))}</v8:content>")
|
X(f"{inner}\t\t<v8:content>{esc_xml_text(str(el['title']))}</v8:content>")
|
||||||
X(f"{inner}\t</v8:item>")
|
X(f"{inner}\t</v8:item>")
|
||||||
X(f"{inner}</Title>")
|
X(f"{inner}</Title>")
|
||||||
emit_common_flags(el, inner)
|
emit_common_flags(el, inner)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# form-info v1.5 — Analyze 1C managed form structure
|
# form-info v1.7 — Analyze 1C managed form structure (+единое имя хелпера состояния поддержки)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory=$true)]
|
[Parameter(Mandatory=$true)]
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# form-info v1.5 — Analyze 1C managed form structure
|
# form-info v1.7 — Analyze 1C managed form structure (+единое имя хелпера состояния поддержки)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -8,6 +8,28 @@ import re
|
|||||||
import sys
|
import sys
|
||||||
from lxml import etree
|
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)
|
||||||
|
|
||||||
|
|
||||||
# --- Namespace map ---
|
# --- Namespace map ---
|
||||||
|
|
||||||
NSMAP = {
|
NSMAP = {
|
||||||
@@ -353,7 +375,7 @@ def get_support_status_for_path(target_path):
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
return None
|
return None
|
||||||
def is_external_root(xml_path):
|
def _sg_is_external_root(xml_path):
|
||||||
if not os.path.isfile(xml_path):
|
if not os.path.isfile(xml_path):
|
||||||
return False
|
return False
|
||||||
try:
|
try:
|
||||||
@@ -367,14 +389,14 @@ def get_support_status_for_path(target_path):
|
|||||||
rp = os.path.abspath(target_path)
|
rp = os.path.abspath(target_path)
|
||||||
# The target file itself may be the element meta-xml (e.g. Subsystems/X.xml).
|
# The target file itself may be the element meta-xml (e.g. Subsystems/X.xml).
|
||||||
elem_uuid = root_uuid(rp)
|
elem_uuid = root_uuid(rp)
|
||||||
if is_external_root(rp):
|
if _sg_is_external_root(rp):
|
||||||
return None
|
return None
|
||||||
bin_path = None
|
bin_path = None
|
||||||
d = os.path.dirname(rp)
|
d = os.path.dirname(rp)
|
||||||
for _ in range(12):
|
for _ in range(12):
|
||||||
if not d:
|
if not d:
|
||||||
break
|
break
|
||||||
if is_external_root(d + ".xml"):
|
if _sg_is_external_root(d + ".xml"):
|
||||||
return None
|
return None
|
||||||
if not elem_uuid:
|
if not elem_uuid:
|
||||||
elem_uuid = root_uuid(d + ".xml")
|
elem_uuid = root_uuid(d + ".xml")
|
||||||
@@ -433,7 +455,7 @@ def main():
|
|||||||
parser.add_argument("-Limit", type=int, default=150, help="Max lines to show")
|
parser.add_argument("-Limit", type=int, default=150, help="Max lines to show")
|
||||||
parser.add_argument("-Offset", type=int, default=0, help="Line offset for pagination")
|
parser.add_argument("-Offset", type=int, default=0, help="Line offset for pagination")
|
||||||
parser.add_argument("-Expand", default="", help="Expand collapsed section by name, or * for all")
|
parser.add_argument("-Expand", default="", help="Expand collapsed section by name, or * for all")
|
||||||
args = parser.parse_args()
|
args = ci_parse_args(parser)
|
||||||
|
|
||||||
form_path = args.FormPath
|
form_path = args.FormPath
|
||||||
limit = args.Limit
|
limit = args.Limit
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# form-remove v1.4 — Remove form from 1C object
|
# form-remove v1.9 — Remove form from 1C object
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory)]
|
||||||
@@ -64,6 +64,10 @@ foreach ($node in $formNodes) {
|
|||||||
$parent.RemoveChild($prev) | Out-Null
|
$parent.RemoveChild($prev) | Out-Null
|
||||||
}
|
}
|
||||||
$parent.RemoveChild($node) | Out-Null
|
$parent.RemoveChild($node) | Out-Null
|
||||||
|
# Опустевший контейнер: остаётся отступ-whitespace, и XmlWriter пишет пару
|
||||||
|
# <ChildObjects>\n\t\t</ChildObjects>. Платформа пишет только <ChildObjects/>
|
||||||
|
# (1394 самозакрывающихся на acc+erp, пустых пар ни в одной форме — 0).
|
||||||
|
if ($parent.SelectNodes("*").Count -eq 0) { $parent.IsEmpty = $true }
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -74,7 +78,9 @@ foreach ($node in $formNodes) {
|
|||||||
$formRefRe = "Form\.$([regex]::Escape($FormName))$"
|
$formRefRe = "Form\.$([regex]::Escape($FormName))$"
|
||||||
foreach ($node in $xmlDoc.SelectNodes("//md:*", $nsMgr)) {
|
foreach ($node in $xmlDoc.SelectNodes("//md:*", $nsMgr)) {
|
||||||
if ($node.LocalName -like "*Form" -and $node.InnerText -and $node.InnerText -match $formRefRe) {
|
if ($node.LocalName -like "*Form" -and $node.InnerText -and $node.InnerText -match $formRefRe) {
|
||||||
$node.InnerText = ""
|
# IsEmpty, а не InnerText="": пустая строка сериализуется парой <Tag></Tag>, а
|
||||||
|
# Конфигуратор пустых пар не пишет (0 на 476 942 XML корпуса) — нужен <Tag/>.
|
||||||
|
$node.IsEmpty = $true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,11 +89,26 @@ $encBom = New-Object System.Text.UTF8Encoding($true)
|
|||||||
$settings = New-Object System.Xml.XmlWriterSettings
|
$settings = New-Object System.Xml.XmlWriterSettings
|
||||||
$settings.Encoding = $encBom
|
$settings.Encoding = $encBom
|
||||||
$settings.Indent = $false
|
$settings.Indent = $false
|
||||||
|
$settings.NewLineHandling = [System.Xml.NewLineHandling]::None
|
||||||
|
|
||||||
$stream = New-Object System.IO.FileStream($rootXmlFull.Path, [System.IO.FileMode]::Create)
|
# Через MemoryStream, а не прямо в файл: нужен шаг пост-обработки строки.
|
||||||
$writer = [System.Xml.XmlWriter]::Create($stream, $settings)
|
$memStream = New-Object System.IO.MemoryStream
|
||||||
|
$writer = [System.Xml.XmlWriter]::Create($memStream, $settings)
|
||||||
$xmlDoc.Save($writer)
|
$xmlDoc.Save($writer)
|
||||||
$writer.Close()
|
$writer.Flush(); $writer.Close()
|
||||||
$stream.Close()
|
|
||||||
|
$xmlText = [System.Text.Encoding]::UTF8.GetString($memStream.ToArray())
|
||||||
|
$memStream.Close()
|
||||||
|
if ($xmlText.Length -gt 0 -and $xmlText[0] -eq [char]0xFEFF) { $xmlText = $xmlText.Substring(1) }
|
||||||
|
$xmlText = $xmlText.Replace('encoding="utf-8"', 'encoding="UTF-8"')
|
||||||
|
# Пустой элемент: XmlWriter отдаёт `<a />`, Конфигуратор пишет `<a/>`. Внутри
|
||||||
|
# CDATA/комментария ` />` может быть содержимым (там `>` не экранируется),
|
||||||
|
# поэтому они идут первыми ветками альтернации и возвращаются как есть.
|
||||||
|
$xmlText = [regex]::Replace($xmlText, '(?s)<!\[CDATA\[.*?\]\]>|<!--.*?-->|(?<=\S) />', { param($m) if ($m.Value -eq ' />') { '/>' } else { $m.Value } })
|
||||||
|
# Целевой перевод строки: стиль файла-назначения — правка наследует его (#44/#46/#47),
|
||||||
|
# новый файл получает канон выгрузки CRLF. Зеркало _detect_xml_style в py-порту.
|
||||||
|
$targetEol = if ((Test-Path -LiteralPath $rootXmlFull.Path) -and ([System.IO.File]::ReadAllText($rootXmlFull.Path) -notmatch "`r`n")) { "`n" } else { "`r`n" }
|
||||||
|
$xmlText = ($xmlText -replace "`r`n", "`n") -replace "`n", $targetEol
|
||||||
|
[System.IO.File]::WriteAllText($rootXmlFull.Path, $xmlText, $encBom)
|
||||||
|
|
||||||
Write-Host "[OK] Форма $FormName удалена из $rootXmlPath"
|
Write-Host "[OK] Форма $FormName удалена из $rootXmlPath"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# remove-form v1.4 — Remove form from 1C object
|
# form-remove v1.9 — Remove form from 1C object
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -10,6 +10,28 @@ import sys
|
|||||||
|
|
||||||
from lxml import etree
|
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"}
|
NSMAP = {"md": "http://v8.1c.ru/8.3/MDClasses"}
|
||||||
|
|
||||||
|
|
||||||
@@ -30,21 +52,22 @@ def _detect_xml_style(path):
|
|||||||
|
|
||||||
|
|
||||||
def _finalize_xml_bytes(xml_bytes, style):
|
def _finalize_xml_bytes(xml_bytes, style):
|
||||||
"""Привести сериализованные байты к стилю оригинала (или к дефолту, если style is None)."""
|
"""Привести байты к стилю оригинала; для НОВОГО файла (style is None) — к канону
|
||||||
enc_decl = style["enc"] if style else "utf-8"
|
выгрузки Конфигуратора: encoding="UTF-8", CRLF в разделителях, без перевода в конце."""
|
||||||
|
enc_decl = style["enc"] if style else "UTF-8"
|
||||||
xml_bytes = xml_bytes.replace(
|
xml_bytes = xml_bytes.replace(
|
||||||
b"<?xml version='1.0' encoding='UTF-8'?>",
|
b"<?xml version='1.0' encoding='UTF-8'?>",
|
||||||
b'<?xml version="1.0" encoding="' + enc_decl.encode("ascii") + b'"?>')
|
b'<?xml version="1.0" encoding="' + enc_decl.encode("ascii") + b'"?>')
|
||||||
# Канонизировать переносы к LF (убирает от \r в tail'ах)
|
# Канонизировать переносы к LF (убирает от \r в tail'ах)
|
||||||
xml_bytes = (xml_bytes.replace(b" \n", b"\n").replace(b" ", b"")
|
xml_bytes = (xml_bytes.replace(b" \n", b"\n").replace(b" ", b"")
|
||||||
.replace(b"\r\n", b"\n").replace(b"\r", b"\n"))
|
.replace(b"\r\n", b"\n").replace(b"\r", b"\n"))
|
||||||
# Финальный перенос — как в оригинале (новый файл → есть)
|
# Финальный перенос — как в оригинале (новый файл → нет, канон #57)
|
||||||
want_final_nl = style["final_nl"] if style else True
|
want_final_nl = style["final_nl"] if style else False
|
||||||
xml_bytes = xml_bytes.rstrip(b"\n")
|
xml_bytes = xml_bytes.rstrip(b"\n")
|
||||||
if want_final_nl:
|
if want_final_nl:
|
||||||
xml_bytes += b"\n"
|
xml_bytes += b"\n"
|
||||||
# EOL — как в оригинале (новый файл → LF, текущее поведение)
|
# EOL — как в оригинале (новый файл → CRLF, канон #57)
|
||||||
if style and style["crlf"]:
|
if (style["crlf"] if style else True):
|
||||||
xml_bytes = xml_bytes.replace(b"\n", b"\r\n")
|
xml_bytes = xml_bytes.replace(b"\n", b"\r\n")
|
||||||
return xml_bytes
|
return xml_bytes
|
||||||
|
|
||||||
@@ -67,7 +90,7 @@ def main():
|
|||||||
parser.add_argument("-ObjectName", "-ProcessorName", required=True)
|
parser.add_argument("-ObjectName", "-ProcessorName", required=True)
|
||||||
parser.add_argument("-FormName", required=True)
|
parser.add_argument("-FormName", required=True)
|
||||||
parser.add_argument("-SrcDir", default="src")
|
parser.add_argument("-SrcDir", default="src")
|
||||||
args = parser.parse_args()
|
args = ci_parse_args(parser)
|
||||||
|
|
||||||
object_name = args.ObjectName
|
object_name = args.ObjectName
|
||||||
form_name = args.FormName
|
form_name = args.FormName
|
||||||
@@ -119,6 +142,10 @@ def main():
|
|||||||
if parent.text and parent.text.strip() == "":
|
if parent.text and parent.text.strip() == "":
|
||||||
parent.text = ""
|
parent.text = ""
|
||||||
parent.remove(node)
|
parent.remove(node)
|
||||||
|
# Опустевший контейнер: text="" сериализуется парой <ChildObjects></ChildObjects>,
|
||||||
|
# а нужен <ChildObjects/> — PS-порт через DOM даёт именно его.
|
||||||
|
if len(parent) == 0 and not (parent.text or "").strip():
|
||||||
|
parent.text = None
|
||||||
break
|
break
|
||||||
|
|
||||||
# Clear any Default*/Auxiliary* form slot that pointed to the removed form
|
# Clear any Default*/Auxiliary* form slot that pointed to the removed form
|
||||||
@@ -129,7 +156,9 @@ def main():
|
|||||||
if not isinstance(el.tag, str):
|
if not isinstance(el.tag, str):
|
||||||
continue
|
continue
|
||||||
if etree.QName(el).localname.endswith("Form") and el.text and ref_re.search(el.text):
|
if etree.QName(el).localname.endswith("Form") and el.text and ref_re.search(el.text):
|
||||||
el.text = ""
|
# text=None, а не "": пустая строка сериализуется парой <Tag></Tag>, а
|
||||||
|
# Конфигуратор пустых пар не пишет (0 на 476 942 XML корпуса) — нужен <Tag/>.
|
||||||
|
el.text = None
|
||||||
|
|
||||||
# Save with BOM
|
# Save with BOM
|
||||||
save_xml_with_bom(tree, root_xml_full)
|
save_xml_with_bom(tree, root_xml_full)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# form-validate v1.8 — Validate 1C managed form
|
# form-validate v1.15 — Validate 1C managed form
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory)]
|
||||||
@@ -56,9 +56,23 @@ try {
|
|||||||
$nsMgr = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable)
|
$nsMgr = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable)
|
||||||
$nsMgr.AddNamespace("f", "http://v8.1c.ru/8.3/xcf/logform")
|
$nsMgr.AddNamespace("f", "http://v8.1c.ru/8.3/xcf/logform")
|
||||||
$nsMgr.AddNamespace("v8", "http://v8.1c.ru/8.1/data/core")
|
$nsMgr.AddNamespace("v8", "http://v8.1c.ru/8.1/data/core")
|
||||||
|
$nsMgr.AddNamespace("xsi", "http://www.w3.org/2001/XMLSchema-instance")
|
||||||
|
|
||||||
$root = $xmlDoc.DocumentElement
|
$root = $xmlDoc.DocumentElement
|
||||||
|
|
||||||
|
# Корень автономной внешней обработки/отчёта. Копия общего эталона (семья
|
||||||
|
# support-guard: is_external_root, авторитет — cf-edit).
|
||||||
|
function Test-ExternalObjectRoot([string]$xmlPath) {
|
||||||
|
if (-not (Test-Path $xmlPath)) { return $false }
|
||||||
|
try {
|
||||||
|
[xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8
|
||||||
|
$el = $mx.DocumentElement.FirstChild
|
||||||
|
while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling }
|
||||||
|
if ($el) { return @('ExternalDataProcessor','ExternalReport') -contains $el.LocalName }
|
||||||
|
} catch {}
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
# --- Detect context: config vs EPF/ERF ---
|
# --- Detect context: config vs EPF/ERF ---
|
||||||
# Walk up from FormPath looking for Configuration.xml → config context
|
# Walk up from FormPath looking for Configuration.xml → config context
|
||||||
# No Configuration.xml → external data processor / report (EPF/ERF)
|
# No Configuration.xml → external data processor / report (EPF/ERF)
|
||||||
@@ -66,13 +80,56 @@ $script:isConfigContext = $false
|
|||||||
$walkDir = Split-Path (Resolve-Path $FormPath) -Parent
|
$walkDir = Split-Path (Resolve-Path $FormPath) -Parent
|
||||||
for ($i = 0; $i -lt 15; $i++) {
|
for ($i = 0; $i -lt 15; $i++) {
|
||||||
if (-not $walkDir -or $walkDir -eq (Split-Path $walkDir)) { break }
|
if (-not $walkDir -or $walkDir -eq (Split-Path $walkDir)) { break }
|
||||||
|
# Порядок проверок тот же, что у Detect-FormatVersion: сначала корень автономной обработки,
|
||||||
|
# потом Configuration.xml — иначе форма внутри EPF, лежащей в дереве конфигурации, взяла бы
|
||||||
|
# версию конфигурации.
|
||||||
|
$extRoot = "$walkDir.xml"
|
||||||
|
if (-not $script:versionAnchor) {
|
||||||
|
if (Test-ExternalObjectRoot $extRoot) {
|
||||||
|
# Ближайший якорь побеждает: автономная обработка остаётся автономной, даже если её
|
||||||
|
# исходники лежат внутри дерева с Configuration.xml (типовая раскладка проекта:
|
||||||
|
# src/cf рядом с src/epf). Иначе её собственные External*-типы считались бы ошибкой.
|
||||||
|
$script:versionAnchor = $extRoot
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
if (Test-Path (Join-Path $walkDir "Configuration.xml")) {
|
if (Test-Path (Join-Path $walkDir "Configuration.xml")) {
|
||||||
$script:isConfigContext = $true
|
$script:isConfigContext = $true
|
||||||
|
$script:configXmlPath = Join-Path $walkDir "Configuration.xml"
|
||||||
|
if (-not $script:versionAnchor) { $script:versionAnchor = $script:configXmlPath }
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
$walkDir = Split-Path $walkDir
|
$walkDir = Split-Path $walkDir
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Версия формата выгрузки. Копия общего эталона (семья detect_format_version, авторитет —
|
||||||
|
# form-compile): та же ветка для автономной EPF/ERF, где версию несёт корень обработки.
|
||||||
|
function Detect-FormatVersion([string]$dir) {
|
||||||
|
$d = $dir
|
||||||
|
while ($d) {
|
||||||
|
# Автономная внешняя обработка/отчёт: своего Configuration.xml у неё нет, версию несёт
|
||||||
|
# корень самой обработки. Без этого форма и макет внутри обработки 2.21 писались бы 2.17.
|
||||||
|
$extPath = "$d.xml"
|
||||||
|
if (Test-Path $extPath) {
|
||||||
|
$extText = [System.IO.File]::ReadAllText($extPath, [System.Text.Encoding]::UTF8)
|
||||||
|
$extHead = $extText.Substring(0, [Math]::Min(2000, $extText.Length))
|
||||||
|
if ($extHead -match '<(ExternalDataProcessor|ExternalReport)[ >]' -and $extHead -match '<MetaDataObject[^>]+version="(\d+\.\d+)"') { return $Matches[1] }
|
||||||
|
}
|
||||||
|
$cfgPath = Join-Path $d "Configuration.xml"
|
||||||
|
if (Test-Path $cfgPath) {
|
||||||
|
$cfgText = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8)
|
||||||
|
# Длину среза берём по СТРОКЕ, а не по размеру файла: размер в БАЙТАХ, Substring считает
|
||||||
|
# СИМВОЛЫ, и на кириллице байт больше — короткий Configuration.xml ронял навык исключением.
|
||||||
|
$head = $cfgText.Substring(0, [Math]::Min(2000, $cfgText.Length))
|
||||||
|
if ($head -match '<MetaDataObject[^>]+version="(\d+\.\d+)"') { return $Matches[1] }
|
||||||
|
}
|
||||||
|
$parent = Split-Path $d -Parent
|
||||||
|
if ($parent -eq $d) { break }
|
||||||
|
$d = $parent
|
||||||
|
}
|
||||||
|
return "2.17"
|
||||||
|
}
|
||||||
|
|
||||||
# --- Counters ---
|
# --- Counters ---
|
||||||
|
|
||||||
$errors = 0
|
$errors = 0
|
||||||
@@ -127,10 +184,11 @@ if ($root.LocalName -ne "Form") {
|
|||||||
Report-Error "Root element is '$($root.LocalName)', expected 'Form'"
|
Report-Error "Root element is '$($root.LocalName)', expected 'Form'"
|
||||||
} else {
|
} else {
|
||||||
$version = $root.GetAttribute("version")
|
$version = $root.GetAttribute("version")
|
||||||
if ($version -eq "2.17" -or $version -eq "2.20") {
|
# Лестница версий формата: 2.17 (8.3.20-8.3.24), 2.18 (8.3.25), 2.19 (8.3.26), 2.20 (8.3.27).
|
||||||
|
if ($version -in @("2.17", "2.18", "2.19", "2.20")) {
|
||||||
Report-OK "Root element: Form version=$version"
|
Report-OK "Root element: Form version=$version"
|
||||||
} elseif ($version) {
|
} elseif ($version) {
|
||||||
Report-Warn "Form version='$version' (expected 2.17 or 2.20)"
|
Report-Warn "Form version='$version' (expected 2.17-2.20)"
|
||||||
} else {
|
} else {
|
||||||
Report-Warn "Form version attribute missing"
|
Report-Warn "Form version attribute missing"
|
||||||
}
|
}
|
||||||
@@ -143,10 +201,15 @@ if (-not $stopped) {
|
|||||||
if ($acb) {
|
if ($acb) {
|
||||||
$acbName = $acb.GetAttribute("name")
|
$acbName = $acb.GetAttribute("name")
|
||||||
$acbId = $acb.GetAttribute("id")
|
$acbId = $acb.GetAttribute("id")
|
||||||
|
# id=-1 — соглашение, а не требование: в корпусе УТ/БП/ERP так у 21 094 форм из 21 097,
|
||||||
|
# но три формы платформа выгружает с обычным id и грузит их без нареканий. Поэтому
|
||||||
|
# предупреждение; ошибка — только если id вовсе не число.
|
||||||
if ($acbId -eq "-1") {
|
if ($acbId -eq "-1") {
|
||||||
Report-OK "AutoCommandBar: name='$acbName', id=$acbId"
|
Report-OK "AutoCommandBar: name='$acbName', id=$acbId"
|
||||||
|
} elseif ($acbId -match '^-?\d+$') {
|
||||||
|
Report-Warn "AutoCommandBar id='$acbId', usually '-1'"
|
||||||
} else {
|
} else {
|
||||||
Report-Error "AutoCommandBar id='$acbId', expected '-1'"
|
Report-Error "AutoCommandBar id='$acbId' is not a number"
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Report-Error "AutoCommandBar element missing"
|
Report-Error "AutoCommandBar element missing"
|
||||||
@@ -426,11 +489,19 @@ if (-not $stopped) {
|
|||||||
$segments = $cleanPath -split '\.'
|
$segments = $cleanPath -split '\.'
|
||||||
$rootAttr = $segments[0]
|
$rootAttr = $segments[0]
|
||||||
|
|
||||||
# Resolve Items.<TableName>.CurrentData.<Field>... — table element, not attribute
|
# Resolve Items.<TableName>.CurrentData.<Field>... — table element, not attribute.
|
||||||
if ($rootAttr -eq 'Items') {
|
# Разрешаем ЦЕПОЧКОЙ: таблица во вложенной таблице сама привязана через Items.*, и один
|
||||||
|
# шаг оставлял корнем литерал «Items» — форма платформы объявлялась битой (типовые
|
||||||
|
# НастройкаПравилОбработкиЗаявокСотрудников в БП и ERP).
|
||||||
|
$itemsHops = 0
|
||||||
|
$itemsBroken = $false
|
||||||
|
while ($rootAttr -eq 'Items') {
|
||||||
|
$itemsHops++
|
||||||
|
if ($itemsHops -gt 10) { $itemsBroken = $true; break } # страховка от кольца ссылок
|
||||||
if ($segments.Count -lt 3 -or $segments[2] -ne 'CurrentData') {
|
if ($segments.Count -lt 3 -or $segments[2] -ne 'CurrentData') {
|
||||||
Report-Warn "[$tag] '$elName': $bTag='$dataPath' — unknown Items.* shape, expected Items.<Table>.CurrentData.*"
|
Report-Warn "[$tag] '$elName': $bTag='$dataPath' — unknown Items.* shape, expected Items.<Table>.CurrentData.*"
|
||||||
continue
|
$itemsBroken = $true
|
||||||
|
break
|
||||||
}
|
}
|
||||||
$tableName = $segments[1]
|
$tableName = $segments[1]
|
||||||
$tableEl = $null
|
$tableEl = $null
|
||||||
@@ -443,17 +514,21 @@ if (-not $stopped) {
|
|||||||
if (-not $tableEl) {
|
if (-not $tableEl) {
|
||||||
Report-Error "[$tag] '$elName': $bTag='$dataPath' — table element '$tableName' not found"
|
Report-Error "[$tag] '$elName': $bTag='$dataPath' — table element '$tableName' not found"
|
||||||
$pathErrors++
|
$pathErrors++
|
||||||
continue
|
$itemsBroken = $true
|
||||||
|
break
|
||||||
}
|
}
|
||||||
$tableDpNode = $tableEl.Node.SelectSingleNode("f:DataPath", $nsMgr)
|
$tableDpNode = $tableEl.Node.SelectSingleNode("f:DataPath", $nsMgr)
|
||||||
if (-not $tableDpNode -or -not $tableDpNode.InnerText.Trim()) {
|
if (-not $tableDpNode -or -not $tableDpNode.InnerText.Trim()) {
|
||||||
# Table without DataPath — can't resolve further, accept silently
|
# Table without DataPath — can't resolve further, accept silently
|
||||||
continue
|
$itemsBroken = $true
|
||||||
|
break
|
||||||
}
|
}
|
||||||
$tableDp = $tableDpNode.InnerText.Trim() -replace '\[\d+\]', ''
|
$tableDp = $tableDpNode.InnerText.Trim() -replace '\[\d+\]', ''
|
||||||
if ($tableDp.StartsWith('~')) { $tableDp = $tableDp.Substring(1) }
|
if ($tableDp.StartsWith('~')) { $tableDp = $tableDp.Substring(1) }
|
||||||
$rootAttr = ($tableDp -split '\.')[0]
|
$segments = $tableDp -split '\.'
|
||||||
|
$rootAttr = $segments[0]
|
||||||
}
|
}
|
||||||
|
if ($itemsBroken) { continue }
|
||||||
|
|
||||||
if (-not $attrMap.ContainsKey($rootAttr)) {
|
if (-not $attrMap.ContainsKey($rootAttr)) {
|
||||||
Report-Error "[$tag] '$elName': $bTag='$dataPath' — attribute '$rootAttr' not found"
|
Report-Error "[$tag] '$elName': $bTag='$dataPath' — attribute '$rootAttr' not found"
|
||||||
@@ -568,13 +643,18 @@ if (-not $stopped) {
|
|||||||
$actionErrors = 0
|
$actionErrors = 0
|
||||||
$actionChecked = 0
|
$actionChecked = 0
|
||||||
|
|
||||||
|
# Предупреждение, а не ошибка: <Action> может назначаться в рантайме
|
||||||
|
# (`Команда.Действие = "Подключаемый_…"` в ПриСозданииНаСервере) — приём типовых конфигураций
|
||||||
|
# там, где обработчик существует не во всякой сборке. Назначать может и чужой модуль
|
||||||
|
# (переопределяемый слой, подключаемые команды), так что по одному Form.xml не решить.
|
||||||
|
# Корпус УТ/БП/ERP: 406 таких команд на 275 формах, произведённых платформой.
|
||||||
foreach ($cmd in $cmdNodes) {
|
foreach ($cmd in $cmdNodes) {
|
||||||
if ($stopped) { break }
|
if ($stopped) { break }
|
||||||
$cmdName = $cmd.GetAttribute("name")
|
$cmdName = $cmd.GetAttribute("name")
|
||||||
$actionNode = $cmd.SelectSingleNode("f:Action", $nsMgr)
|
$actionNode = $cmd.SelectSingleNode("f:Action", $nsMgr)
|
||||||
$actionChecked++
|
$actionChecked++
|
||||||
if (-not $actionNode -or -not $actionNode.InnerText.Trim()) {
|
if (-not $actionNode -or -not $actionNode.InnerText.Trim()) {
|
||||||
Report-Error "Command '$cmdName': missing or empty Action"
|
Report-Warn "Command '$cmdName': no Action — handler must be assigned at runtime, otherwise the command does nothing"
|
||||||
$actionErrors++
|
$actionErrors++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -741,6 +821,36 @@ if (-not $stopped -and $isExtension) {
|
|||||||
Report-OK "Extension ID ranges: $extAttrCount attr(s), $extCmdCount cmd(s) — all >= 1000000"
|
Report-OK "Extension ID ranges: $extAttrCount attr(s), $extCmdCount cmd(s) — all >= 1000000"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# 11d. Пути на основной реквизит, которого форма не объявляет.
|
||||||
|
# Check 5 такое пропускает: у заимствованной формы он не проверяет базовые элементы (id < 1000000),
|
||||||
|
# а привязки в <xr:Link> вообще вне его списка тегов. Между тем это ровно тот случай, на котором
|
||||||
|
# платформа отвергает загрузку: «Неверный путь к полю - Объект.X». Правило: если основной реквизит
|
||||||
|
# не объявлен в <Attributes> формы, любой путь с корнем «Объект» не разрешится.
|
||||||
|
$mainAttrDeclared = $false
|
||||||
|
foreach ($attr in $attrNodes) {
|
||||||
|
$maNode = $attr.SelectSingleNode("f:MainAttribute", $nsMgr)
|
||||||
|
if ($maNode -and $maNode.InnerText.Trim() -eq "true") { $mainAttrDeclared = $true; break }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $mainAttrDeclared) {
|
||||||
|
# Значения привязок ищем текстом: интересуют и обычные теги, и <xr:DataPath> внутри
|
||||||
|
# <ChoiceParameterLinks>, а те живут в чужом пространстве имён.
|
||||||
|
$rawForm = [System.IO.File]::ReadAllText($FormPath, [System.Text.Encoding]::UTF8)
|
||||||
|
$mainBase = $baseFormNode.SelectSingleNode("f:Attributes/f:Attribute[f:MainAttribute='true']", $bfNs)
|
||||||
|
$danglingPaths = @{}
|
||||||
|
foreach ($m in [regex]::Matches($rawForm, '<(?:\w+:)?\w*DataPath[^>]*>(Объект\.[^<]+)</(?:\w+:)?\w*DataPath>')) {
|
||||||
|
$danglingPaths[$m.Groups[1].Value] = $true
|
||||||
|
}
|
||||||
|
if ($danglingPaths.Count -gt 0) {
|
||||||
|
$shown = @($danglingPaths.Keys | Sort-Object)
|
||||||
|
$sample = ($shown | Select-Object -First 3) -join ", "
|
||||||
|
$suffix = if ($shown.Count -gt 3) { " (и ещё $($shown.Count - 3))" } else { "" }
|
||||||
|
Report-Error "Path(s) rooted at 'Объект' but the form declares no MainAttribute: $sample$suffix"
|
||||||
|
} elseif ($mainBase) {
|
||||||
|
Report-OK "Object paths: none dangling (MainAttribute not declared)"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Check callType without BaseForm (structural warning)
|
# Check callType without BaseForm (structural warning)
|
||||||
@@ -843,6 +953,71 @@ if (-not $stopped) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- Check 13: префиксы в значениях объявлены в самом файле ---
|
||||||
|
# `cfg:DataProcessorObject.X` в <v8:Type> при незадекларированном xmlns:cfg — валидный XML, который
|
||||||
|
# платформа не читает вовсе: «Исключение XDTO произошло при чтении файла». Ошибка типична для
|
||||||
|
# рукописного XML: префикс скопирован из чужой формы, а объявление в корне забыто. Область видимости
|
||||||
|
# считаем по узлу (GetNamespaceOfPrefix), а не по корню: локальная xmlns на элементе законна.
|
||||||
|
|
||||||
|
if (-not $stopped) {
|
||||||
|
$prefixErrors = 0
|
||||||
|
$prefixChecked = 0
|
||||||
|
|
||||||
|
$prefixPattern = '^([A-Za-z_][A-Za-z0-9_.-]*):.+$'
|
||||||
|
# Значения, где префикс обязан резолвиться: тип реквизита/колонки и xsi:type
|
||||||
|
# Только листовые узлы: под local-name()='Type' подходит и обёртка <Type>, и вложенный <v8:Type>,
|
||||||
|
# а InnerText обёртки — то же значение, иначе одна ошибка сообщалась бы дважды.
|
||||||
|
foreach ($node in $xmlDoc.SelectNodes("//*[local-name()='Type' or local-name()='TypeSet']", $nsMgr)) {
|
||||||
|
if ($node.SelectSingleNode("*")) { continue }
|
||||||
|
$val = $node.InnerText.Trim()
|
||||||
|
if (-not $val) { continue }
|
||||||
|
$m = [regex]::Match($val, $prefixPattern)
|
||||||
|
if (-not $m.Success) { continue }
|
||||||
|
$prefixChecked++
|
||||||
|
$pfx = $m.Groups[1].Value
|
||||||
|
if (-not $node.GetNamespaceOfPrefix($pfx)) {
|
||||||
|
Report-Error "13. Type '$val': namespace prefix '${pfx}:' is not declared — the platform cannot read the file (XDTO)"
|
||||||
|
$prefixErrors++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foreach ($node in $xmlDoc.SelectNodes("//*[@xsi:type]", $nsMgr)) {
|
||||||
|
$val = $node.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance")
|
||||||
|
$m = [regex]::Match($val, $prefixPattern)
|
||||||
|
if (-not $m.Success) { continue }
|
||||||
|
$prefixChecked++
|
||||||
|
$pfx = $m.Groups[1].Value
|
||||||
|
if (-not $node.GetNamespaceOfPrefix($pfx)) {
|
||||||
|
Report-Error "13. xsi:type='$val': namespace prefix '${pfx}:' is not declared — the platform cannot read the file (XDTO)"
|
||||||
|
$prefixErrors++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($prefixChecked -eq 0) {
|
||||||
|
Report-OK "13. Namespace prefixes: nothing to check"
|
||||||
|
} elseif ($prefixErrors -eq 0) {
|
||||||
|
Report-OK "13. Namespace prefixes: $prefixChecked values, all declared"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Check 14: версия формата формы совпадает с версией выгрузки ---
|
||||||
|
# Версию задаёт платформа, которой выгружали, и в пределах одной выгрузки она едина. Форма из
|
||||||
|
# другой версии — «Неизвестная версия формата N загружаемого файла»: платформа не читает файл,
|
||||||
|
# который новее её самой. Источник версии ищем общим helper-ом: он же покрывает автономную
|
||||||
|
# внешнюю обработку/отчёт, где Configuration.xml нет и версию несёт корень самой обработки.
|
||||||
|
|
||||||
|
if (-not $stopped -and $script:versionAnchor) {
|
||||||
|
$formVer = $root.GetAttribute("version")
|
||||||
|
$dumpVer = Detect-FormatVersion (Split-Path (Resolve-Path $FormPath) -Parent)
|
||||||
|
|
||||||
|
if (-not $formVer) {
|
||||||
|
Report-OK "14. Format version: not comparable"
|
||||||
|
} elseif ($formVer -ne $dumpVer) {
|
||||||
|
Report-Error "14. Format version $formVer differs from the dump ($dumpVer) — a dump carries one version, the platform refuses a file it cannot read"
|
||||||
|
} else {
|
||||||
|
Report-OK "14. Format version: $formVer, matches the dump"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
# --- Summary ---
|
# --- Summary ---
|
||||||
|
|
||||||
$checks = $script:okCount + $errors + $warnings
|
$checks = $script:okCount + $errors + $warnings
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# form-validate v1.8 — Validate 1C managed form
|
# form-validate v1.15 — Validate 1C managed form
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -8,6 +8,28 @@ import re
|
|||||||
import sys
|
import sys
|
||||||
from lxml import etree
|
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)
|
||||||
|
|
||||||
|
|
||||||
F_NS = "http://v8.1c.ru/8.3/xcf/logform"
|
F_NS = "http://v8.1c.ru/8.3/xcf/logform"
|
||||||
V8_NS = "http://v8.1c.ru/8.1/data/core"
|
V8_NS = "http://v8.1c.ru/8.1/data/core"
|
||||||
|
|
||||||
@@ -49,6 +71,49 @@ VALID_CFG_PREFIXES = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Корень автономной внешней обработки/отчёта. Копия общего эталона (семья
|
||||||
|
# support-guard: is_external_root, авторитет — cf-edit).
|
||||||
|
def _sg_is_external_root(xml_path):
|
||||||
|
if not os.path.isfile(xml_path):
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
mx = etree.parse(xml_path).getroot()
|
||||||
|
for child in mx:
|
||||||
|
if isinstance(child.tag, str):
|
||||||
|
return child.tag.split("}")[-1] in ("ExternalDataProcessor", "ExternalReport")
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
# Версия формата выгрузки. Копия общего эталона (семья detect_format_version, авторитет —
|
||||||
|
# form-compile): та же ветка для автономной EPF/ERF, где версию несёт корень обработки.
|
||||||
|
def detect_format_version(d):
|
||||||
|
while d:
|
||||||
|
# Автономная внешняя обработка/отчёт: своего Configuration.xml у неё нет, версию несёт
|
||||||
|
# корень самой обработки. Без этого форма и макет внутри обработки 2.21 писались бы 2.17.
|
||||||
|
ext_path = d + ".xml"
|
||||||
|
if os.path.isfile(ext_path):
|
||||||
|
with open(ext_path, "r", encoding="utf-8-sig") as f:
|
||||||
|
ext_head = f.read(2000)
|
||||||
|
if re.search(r'<(ExternalDataProcessor|ExternalReport)[ >]', ext_head):
|
||||||
|
m = re.search(r'<MetaDataObject[^>]+version="(\d+\.\d+)"', ext_head)
|
||||||
|
if m:
|
||||||
|
return m.group(1)
|
||||||
|
cfg_path = os.path.join(d, "Configuration.xml")
|
||||||
|
if os.path.isfile(cfg_path):
|
||||||
|
with open(cfg_path, "r", encoding="utf-8-sig") as f:
|
||||||
|
head = f.read(2000)
|
||||||
|
m = re.search(r'<MetaDataObject[^>]+version="(\d+\.\d+)"', head)
|
||||||
|
if m:
|
||||||
|
return m.group(1)
|
||||||
|
parent = os.path.dirname(d)
|
||||||
|
if parent == d:
|
||||||
|
break
|
||||||
|
d = parent
|
||||||
|
return "2.17"
|
||||||
|
|
||||||
|
|
||||||
def localname(el):
|
def localname(el):
|
||||||
return etree.QName(el.tag).localname
|
return etree.QName(el.tag).localname
|
||||||
|
|
||||||
@@ -60,7 +125,7 @@ def main():
|
|||||||
parser.add_argument("-FormPath", "-Path", required=True)
|
parser.add_argument("-FormPath", "-Path", required=True)
|
||||||
parser.add_argument("-Detailed", action="store_true")
|
parser.add_argument("-Detailed", action="store_true")
|
||||||
parser.add_argument("-MaxErrors", type=int, default=30)
|
parser.add_argument("-MaxErrors", type=int, default=30)
|
||||||
args = parser.parse_args()
|
args = ci_parse_args(parser)
|
||||||
|
|
||||||
form_path = args.FormPath
|
form_path = args.FormPath
|
||||||
detailed = args.Detailed
|
detailed = args.Detailed
|
||||||
@@ -106,13 +171,29 @@ def main():
|
|||||||
|
|
||||||
# Detect context: config vs EPF/ERF
|
# Detect context: config vs EPF/ERF
|
||||||
is_config_context = False
|
is_config_context = False
|
||||||
|
config_xml_path = ''
|
||||||
|
version_anchor = ''
|
||||||
walk_dir = os.path.dirname(os.path.abspath(form_path))
|
walk_dir = os.path.dirname(os.path.abspath(form_path))
|
||||||
for _ in range(15):
|
for _ in range(15):
|
||||||
parent = os.path.dirname(walk_dir)
|
parent = os.path.dirname(walk_dir)
|
||||||
if parent == walk_dir:
|
if parent == walk_dir:
|
||||||
break
|
break
|
||||||
|
# Порядок проверок тот же, что у detect_format_version: сначала корень автономной обработки,
|
||||||
|
# потом Configuration.xml — иначе форма внутри EPF, лежащей в дереве конфигурации, взяла бы
|
||||||
|
# версию конфигурации.
|
||||||
|
ext_root = walk_dir + '.xml'
|
||||||
|
if not version_anchor:
|
||||||
|
if _sg_is_external_root(ext_root):
|
||||||
|
# Ближайший якорь побеждает: автономная обработка остаётся автономной, даже если её
|
||||||
|
# исходники лежат внутри дерева с Configuration.xml (типовая раскладка проекта:
|
||||||
|
# src/cf рядом с src/epf). Иначе её собственные External*-типы считались бы ошибкой.
|
||||||
|
version_anchor = ext_root
|
||||||
|
break
|
||||||
if os.path.isfile(os.path.join(walk_dir, 'Configuration.xml')):
|
if os.path.isfile(os.path.join(walk_dir, 'Configuration.xml')):
|
||||||
is_config_context = True
|
is_config_context = True
|
||||||
|
config_xml_path = os.path.join(walk_dir, 'Configuration.xml')
|
||||||
|
if not version_anchor:
|
||||||
|
version_anchor = config_xml_path
|
||||||
break
|
break
|
||||||
walk_dir = parent
|
walk_dir = parent
|
||||||
|
|
||||||
@@ -161,10 +242,11 @@ def main():
|
|||||||
report_error(f"Root element is '{localname(root)}', expected 'Form'")
|
report_error(f"Root element is '{localname(root)}', expected 'Form'")
|
||||||
else:
|
else:
|
||||||
version = root.get("version", "")
|
version = root.get("version", "")
|
||||||
if version in ("2.17", "2.20"):
|
# Лестница версий формата: 2.17 (8.3.20-8.3.24), 2.18 (8.3.25), 2.19 (8.3.26), 2.20 (8.3.27).
|
||||||
|
if version in ("2.17", "2.18", "2.19", "2.20"):
|
||||||
report_ok(f"Root element: Form version={version}")
|
report_ok(f"Root element: Form version={version}")
|
||||||
elif version:
|
elif version:
|
||||||
report_warn(f"Form version='{version}' (expected 2.17 or 2.20)")
|
report_warn(f"Form version='{version}' (expected 2.17-2.20)")
|
||||||
else:
|
else:
|
||||||
report_warn("Form version attribute missing")
|
report_warn("Form version attribute missing")
|
||||||
|
|
||||||
@@ -174,10 +256,15 @@ def main():
|
|||||||
if acb is not None:
|
if acb is not None:
|
||||||
acb_name = acb.get("name", "")
|
acb_name = acb.get("name", "")
|
||||||
acb_id = acb.get("id", "")
|
acb_id = acb.get("id", "")
|
||||||
|
# id=-1 — соглашение, а не требование: в корпусе УТ/БП/ERP так у 21 094 форм из 21 097,
|
||||||
|
# но три формы платформа выгружает с обычным id и грузит их без нареканий. Поэтому
|
||||||
|
# предупреждение; ошибка — только если id вовсе не число.
|
||||||
if acb_id == "-1":
|
if acb_id == "-1":
|
||||||
report_ok(f"AutoCommandBar: name='{acb_name}', id={acb_id}")
|
report_ok(f"AutoCommandBar: name='{acb_name}', id={acb_id}")
|
||||||
|
elif re.match(r'^-?\d+$', acb_id):
|
||||||
|
report_warn(f"AutoCommandBar id='{acb_id}', usually '-1'")
|
||||||
else:
|
else:
|
||||||
report_error(f"AutoCommandBar id='{acb_id}', expected '-1'")
|
report_error(f"AutoCommandBar id='{acb_id}' is not a number")
|
||||||
else:
|
else:
|
||||||
report_error("AutoCommandBar element missing")
|
report_error("AutoCommandBar element missing")
|
||||||
|
|
||||||
@@ -429,11 +516,21 @@ def main():
|
|||||||
segments = clean_path.split(".")
|
segments = clean_path.split(".")
|
||||||
root_attr = segments[0]
|
root_attr = segments[0]
|
||||||
|
|
||||||
# Resolve Items.<TableName>.CurrentData.<Field>... — table element, not attribute
|
# Resolve Items.<TableName>.CurrentData.<Field>... — table element, not attribute.
|
||||||
if root_attr == 'Items':
|
# Разрешаем ЦЕПОЧКОЙ: таблица во вложенной таблице сама привязана через Items.*, и один
|
||||||
|
# шаг оставлял корнем литерал «Items» — форма платформы объявлялась битой (типовые
|
||||||
|
# НастройкаПравилОбработкиЗаявокСотрудников в БП и ERP).
|
||||||
|
items_hops = 0
|
||||||
|
items_broken = False
|
||||||
|
while root_attr == 'Items':
|
||||||
|
items_hops += 1
|
||||||
|
if items_hops > 10: # страховка от кольца ссылок
|
||||||
|
items_broken = True
|
||||||
|
break
|
||||||
if len(segments) < 3 or segments[2] != 'CurrentData':
|
if len(segments) < 3 or segments[2] != 'CurrentData':
|
||||||
report_warn(f"[{tag}] '{el_name}': {b_tag}='{data_path}' — unknown Items.* shape, expected Items.<Table>.CurrentData.*")
|
report_warn(f"[{tag}] '{el_name}': {b_tag}='{data_path}' — unknown Items.* shape, expected Items.<Table>.CurrentData.*")
|
||||||
continue
|
items_broken = True
|
||||||
|
break
|
||||||
table_name = segments[1]
|
table_name = segments[1]
|
||||||
table_el = None
|
table_el = None
|
||||||
for candidate in all_elements:
|
for candidate in all_elements:
|
||||||
@@ -443,14 +540,19 @@ def main():
|
|||||||
if table_el is None:
|
if table_el is None:
|
||||||
report_error(f"[{tag}] '{el_name}': {b_tag}='{data_path}' — table element '{table_name}' not found")
|
report_error(f"[{tag}] '{el_name}': {b_tag}='{data_path}' — table element '{table_name}' not found")
|
||||||
path_errors += 1
|
path_errors += 1
|
||||||
continue
|
items_broken = True
|
||||||
|
break
|
||||||
table_dp_node = table_el["Node"].find(f"{{{F_NS}}}DataPath")
|
table_dp_node = table_el["Node"].find(f"{{{F_NS}}}DataPath")
|
||||||
if table_dp_node is None or not (table_dp_node.text or "").strip():
|
if table_dp_node is None or not (table_dp_node.text or "").strip():
|
||||||
continue
|
items_broken = True
|
||||||
|
break
|
||||||
table_dp = re.sub(r'\[\d+\]', '', (table_dp_node.text or "").strip())
|
table_dp = re.sub(r'\[\d+\]', '', (table_dp_node.text or "").strip())
|
||||||
if table_dp.startswith('~'):
|
if table_dp.startswith('~'):
|
||||||
table_dp = table_dp[1:]
|
table_dp = table_dp[1:]
|
||||||
root_attr = table_dp.split(".")[0]
|
segments = table_dp.split(".")
|
||||||
|
root_attr = segments[0]
|
||||||
|
if items_broken:
|
||||||
|
continue
|
||||||
|
|
||||||
if root_attr not in attr_map:
|
if root_attr not in attr_map:
|
||||||
report_error(f"[{tag}] '{el_name}': {b_tag}='{data_path}' — attribute '{root_attr}' not found")
|
report_error(f"[{tag}] '{el_name}': {b_tag}='{data_path}' — attribute '{root_attr}' not found")
|
||||||
@@ -464,6 +566,8 @@ def main():
|
|||||||
path_msg = f"{path_msg}, {skip_note}" if path_msg else skip_note
|
path_msg = f"{path_msg}, {skip_note}" if path_msg else skip_note
|
||||||
if path_errors == 0 and path_msg:
|
if path_errors == 0 and path_msg:
|
||||||
report_ok(f"Data bindings: {path_msg}")
|
report_ok(f"Data bindings: {path_msg}")
|
||||||
|
elif path_errors == 0:
|
||||||
|
report_ok("Data bindings: none")
|
||||||
|
|
||||||
# --- Check 6: Button command references ---
|
# --- Check 6: Button command references ---
|
||||||
if not stopped:
|
if not stopped:
|
||||||
@@ -498,6 +602,8 @@ def main():
|
|||||||
|
|
||||||
if cmd_errors == 0 and cmd_checked > 0:
|
if cmd_errors == 0 and cmd_checked > 0:
|
||||||
report_ok(f"Command references: {cmd_checked} buttons checked")
|
report_ok(f"Command references: {cmd_checked} buttons checked")
|
||||||
|
elif cmd_checked == 0:
|
||||||
|
report_ok("Command references: none")
|
||||||
|
|
||||||
# --- Check 7: Events have handler names ---
|
# --- Check 7: Events have handler names ---
|
||||||
if not stopped:
|
if not stopped:
|
||||||
@@ -537,12 +643,19 @@ def main():
|
|||||||
|
|
||||||
if event_errors == 0 and event_checked > 0:
|
if event_errors == 0 and event_checked > 0:
|
||||||
report_ok(f"Event handlers: {event_checked} events checked")
|
report_ok(f"Event handlers: {event_checked} events checked")
|
||||||
|
elif event_checked == 0:
|
||||||
|
report_ok("Event handlers: none")
|
||||||
|
|
||||||
# --- Check 8: Command actions ---
|
# --- Check 8: Command actions ---
|
||||||
if not stopped:
|
if not stopped:
|
||||||
action_errors = 0
|
action_errors = 0
|
||||||
action_checked = 0
|
action_checked = 0
|
||||||
|
|
||||||
|
# Предупреждение, а не ошибка: <Action> может назначаться в рантайме
|
||||||
|
# (`Команда.Действие = "Подключаемый_…"` в ПриСозданииНаСервере) — приём типовых конфигураций
|
||||||
|
# там, где обработчик существует не во всякой сборке. Назначать может и чужой модуль
|
||||||
|
# (переопределяемый слой, подключаемые команды), так что по одному Form.xml не решить.
|
||||||
|
# Корпус УТ/БП/ERP: 406 таких команд на 275 формах, произведённых платформой.
|
||||||
for cmd in cmd_nodes:
|
for cmd in cmd_nodes:
|
||||||
if stopped:
|
if stopped:
|
||||||
break
|
break
|
||||||
@@ -550,11 +663,13 @@ def main():
|
|||||||
action_node = cmd.find(f"{{{F_NS}}}Action")
|
action_node = cmd.find(f"{{{F_NS}}}Action")
|
||||||
action_checked += 1
|
action_checked += 1
|
||||||
if action_node is None or not (action_node.text or "").strip():
|
if action_node is None or not (action_node.text or "").strip():
|
||||||
report_error(f"Command '{cmd_name}': missing or empty Action")
|
report_warn(f"Command '{cmd_name}': no Action — handler must be assigned at runtime, otherwise the command does nothing")
|
||||||
action_errors += 1
|
action_errors += 1
|
||||||
|
|
||||||
if action_errors == 0 and action_checked > 0:
|
if action_errors == 0 and action_checked > 0:
|
||||||
report_ok(f"Command actions: {action_checked} commands checked")
|
report_ok(f"Command actions: {action_checked} commands checked")
|
||||||
|
elif action_checked == 0:
|
||||||
|
report_ok("Command actions: none")
|
||||||
|
|
||||||
# --- Check 9: MainAttribute count ---
|
# --- Check 9: MainAttribute count ---
|
||||||
if not stopped:
|
if not stopped:
|
||||||
@@ -685,6 +800,33 @@ def main():
|
|||||||
if (ext_attr_count + ext_cmd_count) > 0:
|
if (ext_attr_count + ext_cmd_count) > 0:
|
||||||
report_ok(f"Extension ID ranges: {ext_attr_count} attr(s), {ext_cmd_count} cmd(s) \u2014 all >= 1000000")
|
report_ok(f"Extension ID ranges: {ext_attr_count} attr(s), {ext_cmd_count} cmd(s) \u2014 all >= 1000000")
|
||||||
|
|
||||||
|
# 11d. \u041f\u0443\u0442\u0438 \u043d\u0430 \u043e\u0441\u043d\u043e\u0432\u043d\u043e\u0439 \u0440\u0435\u043a\u0432\u0438\u0437\u0438\u0442, \u043a\u043e\u0442\u043e\u0440\u043e\u0433\u043e \u0444\u043e\u0440\u043c\u0430 \u043d\u0435 \u043e\u0431\u044a\u044f\u0432\u043b\u044f\u0435\u0442.
|
||||||
|
# Check 5 \u0442\u0430\u043a\u043e\u0435 \u043f\u0440\u043e\u043f\u0443\u0441\u043a\u0430\u0435\u0442: \u0443 \u0437\u0430\u0438\u043c\u0441\u0442\u0432\u043e\u0432\u0430\u043d\u043d\u043e\u0439 \u0444\u043e\u0440\u043c\u044b \u043e\u043d \u043d\u0435 \u043f\u0440\u043e\u0432\u0435\u0440\u044f\u0435\u0442 \u0431\u0430\u0437\u043e\u0432\u044b\u0435 \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u044b (id < 1000000),
|
||||||
|
# \u0430 \u043f\u0440\u0438\u0432\u044f\u0437\u043a\u0438 \u0432 <xr:Link> \u0432\u043e\u043e\u0431\u0449\u0435 \u0432\u043d\u0435 \u0435\u0433\u043e \u0441\u043f\u0438\u0441\u043a\u0430 \u0442\u0435\u0433\u043e\u0432. \u041c\u0435\u0436\u0434\u0443 \u0442\u0435\u043c \u044d\u0442\u043e \u0440\u043e\u0432\u043d\u043e \u0442\u043e\u0442 \u0441\u043b\u0443\u0447\u0430\u0439, \u043d\u0430 \u043a\u043e\u0442\u043e\u0440\u043e\u043c
|
||||||
|
# \u043f\u043b\u0430\u0442\u0444\u043e\u0440\u043c\u0430 \u043e\u0442\u0432\u0435\u0440\u0433\u0430\u0435\u0442 \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0443: \u00ab\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043f\u0443\u0442\u044c \u043a \u043f\u043e\u043b\u044e - \u041e\u0431\u044a\u0435\u043a\u0442.X\u00bb. \u041f\u0440\u0430\u0432\u0438\u043b\u043e: \u0435\u0441\u043b\u0438 \u043e\u0441\u043d\u043e\u0432\u043d\u043e\u0439 \u0440\u0435\u043a\u0432\u0438\u0437\u0438\u0442
|
||||||
|
# \u043d\u0435 \u043e\u0431\u044a\u044f\u0432\u043b\u0435\u043d \u0432 <Attributes> \u0444\u043e\u0440\u043c\u044b, \u043b\u044e\u0431\u043e\u0439 \u043f\u0443\u0442\u044c \u0441 \u043a\u043e\u0440\u043d\u0435\u043c \u00ab\u041e\u0431\u044a\u0435\u043a\u0442\u00bb \u043d\u0435 \u0440\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u0441\u044f.
|
||||||
|
main_attr_declared = False
|
||||||
|
for attr in attr_nodes:
|
||||||
|
ma_node = attr.find(f"{{{F_NS}}}MainAttribute")
|
||||||
|
if ma_node is not None and (ma_node.text or "").strip() == "true":
|
||||||
|
main_attr_declared = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if not main_attr_declared:
|
||||||
|
# \u0417\u043d\u0430\u0447\u0435\u043d\u0438\u044f \u043f\u0440\u0438\u0432\u044f\u0437\u043e\u043a \u0438\u0449\u0435\u043c \u0442\u0435\u043a\u0441\u0442\u043e\u043c: \u0438\u043d\u0442\u0435\u0440\u0435\u0441\u0443\u044e\u0442 \u0438 \u043e\u0431\u044b\u0447\u043d\u044b\u0435 \u0442\u0435\u0433\u0438, \u0438 <xr:DataPath> \u0432\u043d\u0443\u0442\u0440\u0438
|
||||||
|
# <ChoiceParameterLinks>, \u0430 \u0442\u0435 \u0436\u0438\u0432\u0443\u0442 \u0432 \u0447\u0443\u0436\u043e\u043c \u043f\u0440\u043e\u0441\u0442\u0440\u0430\u043d\u0441\u0442\u0432\u0435 \u0438\u043c\u0451\u043d.
|
||||||
|
with open(form_path, "r", encoding="utf-8-sig") as fh:
|
||||||
|
raw_form = fh.read()
|
||||||
|
main_base = base_form_node.find(f"{{{F_NS}}}Attributes/{{{F_NS}}}Attribute[{{{F_NS}}}MainAttribute='true']")
|
||||||
|
dangling_paths = set(re.findall(r'<(?:\w+:)?\w*DataPath[^>]*>(\u041e\u0431\u044a\u0435\u043a\u0442\.[^<]+)</(?:\w+:)?\w*DataPath>', raw_form))
|
||||||
|
if dangling_paths:
|
||||||
|
shown = sorted(dangling_paths)
|
||||||
|
sample = ", ".join(shown[:3])
|
||||||
|
suffix = f" (\u0438 \u0435\u0449\u0451 {len(shown) - 3})" if len(shown) > 3 else ""
|
||||||
|
report_error(f"Path(s) rooted at '\u041e\u0431\u044a\u0435\u043a\u0442' but the form declares no MainAttribute: {sample}{suffix}")
|
||||||
|
elif main_base is not None:
|
||||||
|
report_ok("Object paths: none dangling (MainAttribute not declared)")
|
||||||
|
|
||||||
# Check callType without BaseForm
|
# Check callType without BaseForm
|
||||||
if not stopped and not is_extension:
|
if not stopped and not is_extension:
|
||||||
call_type_without_base = False
|
call_type_without_base = False
|
||||||
@@ -747,6 +889,62 @@ def main():
|
|||||||
else:
|
else:
|
||||||
report_ok('12. Types: no type values to check')
|
report_ok('12. Types: no type values to check')
|
||||||
|
|
||||||
|
# --- Check 13: префиксы в значениях объявлены в самом файле ---
|
||||||
|
# `cfg:DataProcessorObject.X` в <v8:Type> при незадекларированном xmlns:cfg — валидный XML, который
|
||||||
|
# платформа не читает вовсе: «Исключение XDTO произошло при чтении файла». Ошибка типична для
|
||||||
|
# рукописного XML: префикс скопирован из чужой формы, а объявление в корне забыто. Область видимости
|
||||||
|
# считаем по узлу (nsmap элемента), а не по корню: локальная xmlns на элементе законна.
|
||||||
|
if not stopped:
|
||||||
|
prefix_errors = 0
|
||||||
|
prefix_checked = 0
|
||||||
|
prefix_re = re.compile(r'^([A-Za-z_][A-Za-z0-9_.-]*):.+$')
|
||||||
|
|
||||||
|
for node in root.iter():
|
||||||
|
if not isinstance(node.tag, str):
|
||||||
|
continue
|
||||||
|
ln = localname(node)
|
||||||
|
values = []
|
||||||
|
if ln in ('Type', 'TypeSet'):
|
||||||
|
values.append((node.text or '').strip())
|
||||||
|
xsi_type = node.get(f'{{{"http://www.w3.org/2001/XMLSchema-instance"}}}type')
|
||||||
|
if xsi_type:
|
||||||
|
values.append(xsi_type.strip())
|
||||||
|
for val in values:
|
||||||
|
if not val:
|
||||||
|
continue
|
||||||
|
m = prefix_re.match(val)
|
||||||
|
if not m:
|
||||||
|
continue
|
||||||
|
prefix_checked += 1
|
||||||
|
pfx = m.group(1)
|
||||||
|
if pfx not in node.nsmap:
|
||||||
|
kind = "xsi:type" if val == xsi_type else "Type"
|
||||||
|
report_error(f"13. {kind} '{val}': namespace prefix '{pfx}:' is not declared "
|
||||||
|
"— the platform cannot read the file (XDTO)")
|
||||||
|
prefix_errors += 1
|
||||||
|
|
||||||
|
if prefix_checked == 0:
|
||||||
|
report_ok('13. Namespace prefixes: nothing to check')
|
||||||
|
elif prefix_errors == 0:
|
||||||
|
report_ok(f'13. Namespace prefixes: {prefix_checked} values, all declared')
|
||||||
|
|
||||||
|
# --- Check 14: версия формата формы совпадает с версией выгрузки ---
|
||||||
|
# Версию задаёт платформа, которой выгружали, и в пределах одной выгрузки она едина. Форма из
|
||||||
|
# другой версии — «Неизвестная версия формата N загружаемого файла»: платформа не читает файл,
|
||||||
|
# который новее её самой. Источник версии ищем общим helper-ом: он же покрывает автономную
|
||||||
|
# внешнюю обработку/отчёт, где Configuration.xml нет и версию несёт корень самой обработки.
|
||||||
|
if not stopped and version_anchor:
|
||||||
|
form_ver = root.get('version', '')
|
||||||
|
dump_ver = detect_format_version(os.path.dirname(os.path.abspath(form_path)))
|
||||||
|
|
||||||
|
if not form_ver:
|
||||||
|
report_ok('14. Format version: not comparable')
|
||||||
|
elif form_ver != dump_ver:
|
||||||
|
report_error(f'14. Format version {form_ver} differs from the dump ({dump_ver}) '
|
||||||
|
'— a dump carries one version, the platform refuses a file it cannot read')
|
||||||
|
else:
|
||||||
|
report_ok(f'14. Format version: {form_ver}, matches the dump')
|
||||||
|
|
||||||
# --- Finalize ---
|
# --- Finalize ---
|
||||||
checks = ok_count + errors + warnings
|
checks = ok_count + errors + warnings
|
||||||
if errors == 0 and warnings == 0 and not detailed:
|
if errors == 0 and warnings == 0 and not detailed:
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# help-add v1.9 — Add built-in help to 1C object
|
# help-add v1.19 — Add built-in help to 1C object (+write_xml_file/write_utf8_bom: общий эталон записи)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory)]
|
||||||
@@ -149,10 +149,20 @@ function Assert-EditAllowed([string]$targetPath, [string]$require) {
|
|||||||
function Detect-FormatVersion([string]$dir) {
|
function Detect-FormatVersion([string]$dir) {
|
||||||
$d = $dir
|
$d = $dir
|
||||||
while ($d) {
|
while ($d) {
|
||||||
|
# Автономная внешняя обработка/отчёт: своего Configuration.xml у неё нет, версию несёт
|
||||||
|
# корень самой обработки. Без этого форма и макет внутри обработки 2.21 писались бы 2.17.
|
||||||
|
$extPath = "$d.xml"
|
||||||
|
if (Test-Path $extPath) {
|
||||||
|
$extText = [System.IO.File]::ReadAllText($extPath, [System.Text.Encoding]::UTF8)
|
||||||
|
$extHead = $extText.Substring(0, [Math]::Min(2000, $extText.Length))
|
||||||
|
if ($extHead -match '<(ExternalDataProcessor|ExternalReport)[ >]' -and $extHead -match '<MetaDataObject[^>]+version="(\d+\.\d+)"') { return $Matches[1] }
|
||||||
|
}
|
||||||
$cfgPath = Join-Path $d "Configuration.xml"
|
$cfgPath = Join-Path $d "Configuration.xml"
|
||||||
if (Test-Path $cfgPath) {
|
if (Test-Path $cfgPath) {
|
||||||
$content = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8)
|
$cfgText = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8)
|
||||||
$head = $content.Substring(0, [Math]::Min(2000, $content.Length))
|
# Длину среза берём по СТРОКЕ, а не по размеру файла: размер в БАЙТАХ, Substring считает
|
||||||
|
# СИМВОЛЫ, и на кириллице байт больше — короткий Configuration.xml ронял навык исключением.
|
||||||
|
$head = $cfgText.Substring(0, [Math]::Min(2000, $cfgText.Length))
|
||||||
if ($head -match '<MetaDataObject[^>]+version="(\d+\.\d+)"') { return $Matches[1] }
|
if ($head -match '<MetaDataObject[^>]+version="(\d+\.\d+)"') { return $Matches[1] }
|
||||||
}
|
}
|
||||||
$parent = Split-Path $d -Parent
|
$parent = Split-Path $d -Parent
|
||||||
@@ -195,7 +205,18 @@ $helpXml = @"
|
|||||||
</Help>
|
</Help>
|
||||||
"@
|
"@
|
||||||
|
|
||||||
[System.IO.File]::WriteAllText($helpXmlPath, $helpXml, $encBom)
|
# XML в каноне выгрузки Конфигуратора: CRLF в разделителях, без перевода в конце.
|
||||||
|
#
|
||||||
|
# Копия этой функции есть в каждом навыке-эмиттере (навыки автономны). Держать
|
||||||
|
# копии одинаковыми — сознательно: разошедшиеся копии сводят на нет весь смысл.
|
||||||
|
#
|
||||||
|
# HTML-страница сюда НЕ идёт — платформа хранит её с LF.
|
||||||
|
function Write-XmlFile([string]$path, [string]$text, $encoding) {
|
||||||
|
$t = ($text -replace "`r`n", "`n") -replace "`n", "`r`n"
|
||||||
|
[System.IO.File]::WriteAllText($path, $t.TrimEnd("`r", "`n"), $encoding)
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-XmlFile $helpXmlPath $helpXml $encBom
|
||||||
|
|
||||||
# --- 2. Help/<lang>.html ---
|
# --- 2. Help/<lang>.html ---
|
||||||
|
|
||||||
@@ -255,11 +276,26 @@ if (Test-Path $formsDir) {
|
|||||||
$settings = New-Object System.Xml.XmlWriterSettings
|
$settings = New-Object System.Xml.XmlWriterSettings
|
||||||
$settings.Encoding = $encBom
|
$settings.Encoding = $encBom
|
||||||
$settings.Indent = $false
|
$settings.Indent = $false
|
||||||
$stream = New-Object System.IO.FileStream($formMeta.FullName, [System.IO.FileMode]::Create)
|
$settings.NewLineHandling = [System.Xml.NewLineHandling]::None
|
||||||
$writer = [System.Xml.XmlWriter]::Create($stream, $settings)
|
# Через MemoryStream, а не прямо в файл: нужен шаг пост-обработки строки.
|
||||||
|
$memStream = New-Object System.IO.MemoryStream
|
||||||
|
$writer = [System.Xml.XmlWriter]::Create($memStream, $settings)
|
||||||
$xmlDoc.Save($writer)
|
$xmlDoc.Save($writer)
|
||||||
$writer.Close()
|
$writer.Flush(); $writer.Close()
|
||||||
$stream.Close()
|
|
||||||
|
$xmlText = [System.Text.Encoding]::UTF8.GetString($memStream.ToArray())
|
||||||
|
$memStream.Close()
|
||||||
|
if ($xmlText.Length -gt 0 -and $xmlText[0] -eq [char]0xFEFF) { $xmlText = $xmlText.Substring(1) }
|
||||||
|
$xmlText = $xmlText.Replace('encoding="utf-8"', 'encoding="UTF-8"')
|
||||||
|
# Пустой элемент: XmlWriter отдаёт `<a />`, Конфигуратор пишет `<a/>`. Внутри
|
||||||
|
# CDATA/комментария ` />` может быть содержимым (там `>` не экранируется),
|
||||||
|
# поэтому они идут первыми ветками альтернации и возвращаются как есть.
|
||||||
|
$xmlText = [regex]::Replace($xmlText, '(?s)<!\[CDATA\[.*?\]\]>|<!--.*?-->|(?<=\S) />', { param($m) if ($m.Value -eq ' />') { '/>' } else { $m.Value } })
|
||||||
|
# Целевой перевод строки: стиль файла-назначения — правка наследует его (#44/#46/#47),
|
||||||
|
# новый файл получает канон выгрузки CRLF. Зеркало _detect_xml_style в py-порту.
|
||||||
|
$targetEol = if ((Test-Path -LiteralPath $formMeta.FullName) -and ([System.IO.File]::ReadAllText($formMeta.FullName) -notmatch "`r`n")) { "`n" } else { "`r`n" }
|
||||||
|
$xmlText = ($xmlText -replace "`r`n", "`n") -replace "`n", $targetEol
|
||||||
|
[System.IO.File]::WriteAllText($formMeta.FullName, $xmlText, $encBom)
|
||||||
|
|
||||||
Write-Host " IncludeHelpInContents добавлен: $($formMeta.Name)"
|
Write-Host " IncludeHelpInContents добавлен: $($formMeta.Name)"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# add-help v1.9 — Add built-in help to 1C object
|
# help-add v1.19 — Add built-in help to 1C object (+write_xml_file/write_utf8_bom: общий эталон записи)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -10,6 +10,28 @@ import sys
|
|||||||
|
|
||||||
from lxml import etree
|
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"}
|
NSMAP = {"md": "http://v8.1c.ru/8.3/MDClasses"}
|
||||||
|
|
||||||
|
|
||||||
@@ -191,6 +213,16 @@ def assert_edit_allowed(target_path, require):
|
|||||||
|
|
||||||
def detect_format_version(d):
|
def detect_format_version(d):
|
||||||
while d:
|
while d:
|
||||||
|
# Автономная внешняя обработка/отчёт: своего Configuration.xml у неё нет, версию несёт
|
||||||
|
# корень самой обработки. Без этого форма и макет внутри обработки 2.21 писались бы 2.17.
|
||||||
|
ext_path = d + ".xml"
|
||||||
|
if os.path.isfile(ext_path):
|
||||||
|
with open(ext_path, "r", encoding="utf-8-sig") as f:
|
||||||
|
ext_head = f.read(2000)
|
||||||
|
if re.search(r'<(ExternalDataProcessor|ExternalReport)[ >]', ext_head):
|
||||||
|
m = re.search(r'<MetaDataObject[^>]+version="(\d+\.\d+)"', ext_head)
|
||||||
|
if m:
|
||||||
|
return m.group(1)
|
||||||
cfg_path = os.path.join(d, "Configuration.xml")
|
cfg_path = os.path.join(d, "Configuration.xml")
|
||||||
if os.path.isfile(cfg_path):
|
if os.path.isfile(cfg_path):
|
||||||
with open(cfg_path, "r", encoding="utf-8-sig") as f:
|
with open(cfg_path, "r", encoding="utf-8-sig") as f:
|
||||||
@@ -222,21 +254,22 @@ def _detect_xml_style(path):
|
|||||||
|
|
||||||
|
|
||||||
def _finalize_xml_bytes(xml_bytes, style):
|
def _finalize_xml_bytes(xml_bytes, style):
|
||||||
"""Привести сериализованные байты к стилю оригинала (или к дефолту, если style is None)."""
|
"""Привести байты к стилю оригинала; для НОВОГО файла (style is None) — к канону
|
||||||
enc_decl = style["enc"] if style else "utf-8"
|
выгрузки Конфигуратора: encoding="UTF-8", CRLF в разделителях, без перевода в конце."""
|
||||||
|
enc_decl = style["enc"] if style else "UTF-8"
|
||||||
xml_bytes = xml_bytes.replace(
|
xml_bytes = xml_bytes.replace(
|
||||||
b"<?xml version='1.0' encoding='UTF-8'?>",
|
b"<?xml version='1.0' encoding='UTF-8'?>",
|
||||||
b'<?xml version="1.0" encoding="' + enc_decl.encode("ascii") + b'"?>')
|
b'<?xml version="1.0" encoding="' + enc_decl.encode("ascii") + b'"?>')
|
||||||
# Канонизировать переносы к LF (убирает от \r в tail'ах)
|
# Канонизировать переносы к LF (убирает от \r в tail'ах)
|
||||||
xml_bytes = (xml_bytes.replace(b" \n", b"\n").replace(b" ", b"")
|
xml_bytes = (xml_bytes.replace(b" \n", b"\n").replace(b" ", b"")
|
||||||
.replace(b"\r\n", b"\n").replace(b"\r", b"\n"))
|
.replace(b"\r\n", b"\n").replace(b"\r", b"\n"))
|
||||||
# Финальный перенос — как в оригинале (новый файл → есть)
|
# Финальный перенос — как в оригинале (новый файл → нет, канон #57)
|
||||||
want_final_nl = style["final_nl"] if style else True
|
want_final_nl = style["final_nl"] if style else False
|
||||||
xml_bytes = xml_bytes.rstrip(b"\n")
|
xml_bytes = xml_bytes.rstrip(b"\n")
|
||||||
if want_final_nl:
|
if want_final_nl:
|
||||||
xml_bytes += b"\n"
|
xml_bytes += b"\n"
|
||||||
# EOL — как в оригинале (новый файл → LF, текущее поведение)
|
# EOL — как в оригинале (новый файл → CRLF, канон #57)
|
||||||
if style and style["crlf"]:
|
if (style["crlf"] if style else True):
|
||||||
xml_bytes = xml_bytes.replace(b"\n", b"\r\n")
|
xml_bytes = xml_bytes.replace(b"\n", b"\r\n")
|
||||||
return xml_bytes
|
return xml_bytes
|
||||||
|
|
||||||
@@ -252,10 +285,24 @@ def save_xml_with_bom(tree, path):
|
|||||||
f.write(xml_bytes)
|
f.write(xml_bytes)
|
||||||
|
|
||||||
|
|
||||||
def write_text_with_bom(path, text):
|
def write_utf8_bom(path, content):
|
||||||
"""Write text to file with UTF-8 BOM."""
|
# newline='' — без трансляции: иначе текстовый режим Python дал бы CRLF на Windows
|
||||||
with open(path, "w", encoding="utf-8-sig") as f:
|
# и LF на macOS, то есть вывод навыка зависел бы от ОС.
|
||||||
f.write(text)
|
with open(path, 'w', encoding='utf-8-sig', newline='') as f:
|
||||||
|
f.write(content)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def write_xml_file(path, content):
|
||||||
|
"""XML в каноне выгрузки Конфигуратора: CRLF в разделителях, без перевода в конце.
|
||||||
|
|
||||||
|
Копия этой функции есть в каждом навыке-эмиттере (навыки автономны). Держать
|
||||||
|
копии одинаковыми — сознательно: разошедшиеся копии сводят на нет весь смысл.
|
||||||
|
|
||||||
|
HTML-страница сюда НЕ идёт — платформа хранит её с LF.
|
||||||
|
"""
|
||||||
|
text = content.replace('\r\n', '\n').replace('\n', '\r\n').rstrip('\r\n')
|
||||||
|
write_utf8_bom(path, text)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
@@ -265,7 +312,7 @@ def main():
|
|||||||
parser.add_argument("-ObjectName", required=True)
|
parser.add_argument("-ObjectName", required=True)
|
||||||
parser.add_argument("-Lang", default="ru")
|
parser.add_argument("-Lang", default="ru")
|
||||||
parser.add_argument("-SrcDir", default="src")
|
parser.add_argument("-SrcDir", default="src")
|
||||||
args = parser.parse_args()
|
args = ci_parse_args(parser)
|
||||||
|
|
||||||
object_name = args.ObjectName
|
object_name = args.ObjectName
|
||||||
lang = args.Lang
|
lang = args.Lang
|
||||||
@@ -301,7 +348,7 @@ def main():
|
|||||||
'</Help>'
|
'</Help>'
|
||||||
)
|
)
|
||||||
|
|
||||||
write_text_with_bom(help_xml_path, help_xml)
|
write_xml_file(help_xml_path, help_xml)
|
||||||
|
|
||||||
# --- 2. Help/<lang>.html ---
|
# --- 2. Help/<lang>.html ---
|
||||||
|
|
||||||
@@ -324,7 +371,7 @@ def main():
|
|||||||
'</html>'
|
'</html>'
|
||||||
)
|
)
|
||||||
|
|
||||||
write_text_with_bom(help_html_path, help_html)
|
write_utf8_bom(help_html_path, help_html)
|
||||||
|
|
||||||
# --- 3. Check IncludeHelpInContents in form metadata ---
|
# --- 3. Check IncludeHelpInContents in form metadata ---
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# img-grid v1.1 — Overlay numbered grid on image
|
# img-grid v1.2 — Overlay numbered grid on image
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
"""Overlay a numbered grid on an image to help determine column/row proportions.
|
"""Overlay a numbered grid on an image to help determine column/row proportions.
|
||||||
|
|
||||||
@@ -16,6 +16,28 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
from PIL import Image, ImageDraw, ImageFont
|
from PIL import Image, ImageDraw, ImageFont
|
||||||
|
|
||||||
|
# Регистронезависимый ввод — паритет с 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)
|
||||||
|
|
||||||
|
|
||||||
MARGIN_TOP = 20
|
MARGIN_TOP = 20
|
||||||
MARGIN_LEFT = 24
|
MARGIN_LEFT = 24
|
||||||
|
|
||||||
@@ -30,7 +52,7 @@ def main():
|
|||||||
parser.add_argument("-r", "--rows", type=int, default=0,
|
parser.add_argument("-r", "--rows", type=int, default=0,
|
||||||
help="Number of horizontal divisions (0 = auto, match cell aspect ratio)")
|
help="Number of horizontal divisions (0 = auto, match cell aspect ratio)")
|
||||||
parser.add_argument("-o", "--output", help="Output path (default: <name>-grid.<ext>)")
|
parser.add_argument("-o", "--output", help="Output path (default: <name>-grid.<ext>)")
|
||||||
args = parser.parse_args()
|
args = ci_parse_args(parser)
|
||||||
|
|
||||||
if args.cols <= 0:
|
if args.cols <= 0:
|
||||||
parser.error("--cols must be greater than 0")
|
parser.error("--cols must be greater than 0")
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# interface-edit v1.8 — Edit 1C CommandInterface.xml
|
# interface-edit v1.18 — Edit 1C CommandInterface.xml (+русские алиасы типов: формы с ё и без)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)][Alias('Path')][string]$CIPath,
|
[Parameter(Mandatory)][Alias('Path')][string]$CIPath,
|
||||||
@@ -162,9 +162,20 @@ Assert-EditAllowed $CIPath 'editable'
|
|||||||
function Detect-FormatVersion([string]$dir) {
|
function Detect-FormatVersion([string]$dir) {
|
||||||
$d = $dir
|
$d = $dir
|
||||||
while ($d) {
|
while ($d) {
|
||||||
|
# Автономная внешняя обработка/отчёт: своего Configuration.xml у неё нет, версию несёт
|
||||||
|
# корень самой обработки. Без этого форма и макет внутри обработки 2.21 писались бы 2.17.
|
||||||
|
$extPath = "$d.xml"
|
||||||
|
if (Test-Path $extPath) {
|
||||||
|
$extText = [System.IO.File]::ReadAllText($extPath, [System.Text.Encoding]::UTF8)
|
||||||
|
$extHead = $extText.Substring(0, [Math]::Min(2000, $extText.Length))
|
||||||
|
if ($extHead -match '<(ExternalDataProcessor|ExternalReport)[ >]' -and $extHead -match '<MetaDataObject[^>]+version="(\d+\.\d+)"') { return $Matches[1] }
|
||||||
|
}
|
||||||
$cfgPath = Join-Path $d "Configuration.xml"
|
$cfgPath = Join-Path $d "Configuration.xml"
|
||||||
if (Test-Path $cfgPath) {
|
if (Test-Path $cfgPath) {
|
||||||
$head = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8).Substring(0, [Math]::Min(2000, (Get-Item $cfgPath).Length))
|
$cfgText = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8)
|
||||||
|
# Длину среза берём по СТРОКЕ, а не по размеру файла: размер в БАЙТАХ, Substring считает
|
||||||
|
# СИМВОЛЫ, и на кириллице байт больше — короткий Configuration.xml ронял навык исключением.
|
||||||
|
$head = $cfgText.Substring(0, [Math]::Min(2000, $cfgText.Length))
|
||||||
if ($head -match '<MetaDataObject[^>]+version="(\d+\.\d+)"') { return $Matches[1] }
|
if ($head -match '<MetaDataObject[^>]+version="(\d+\.\d+)"') { return $Matches[1] }
|
||||||
}
|
}
|
||||||
$parent = Split-Path $d -Parent
|
$parent = Split-Path $d -Parent
|
||||||
@@ -199,7 +210,12 @@ if (-not (Test-Path $CIPath)) {
|
|||||||
</CommandInterface>
|
</CommandInterface>
|
||||||
"@
|
"@
|
||||||
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
|
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
|
||||||
[System.IO.File]::WriteAllText($CIPath, $emptyCI, $utf8Bom)
|
# Файл СОЗДАЁМ — пишем канон выгрузки: CRLF, без перевода строки в конце.
|
||||||
|
# (Правка существующего файла, наоборот, наследует его стиль — это делает
|
||||||
|
# основной путь сохранения ниже.) Нормализация нужна потому, что here-string
|
||||||
|
# берёт переводы строк из самого .ps1, а он в репозитории хранится с LF.
|
||||||
|
$emptyCI = ($emptyCI -replace "`r`n", "`n") -replace "`n", "`r`n"
|
||||||
|
[System.IO.File]::WriteAllText($CIPath, $emptyCI.TrimEnd("`r", "`n"), $utf8Bom)
|
||||||
Write-Host "[INFO] Created new CommandInterface.xml: $CIPath"
|
Write-Host "[INFO] Created new CommandInterface.xml: $CIPath"
|
||||||
} else {
|
} else {
|
||||||
Write-Error "File not found: $CIPath (use -CreateIfMissing to create)"
|
Write-Error "File not found: $CIPath (use -CreateIfMissing to create)"
|
||||||
@@ -380,6 +396,10 @@ $script:typeNormMap = @{
|
|||||||
"ПланОбмена"="ExchangePlan"; "ЖурналДокументов"="DocumentJournal"
|
"ПланОбмена"="ExchangePlan"; "ЖурналДокументов"="DocumentJournal"
|
||||||
"ОбщийМодуль"="CommonModule"; "ОбщаяКоманда"="CommonCommand"
|
"ОбщийМодуль"="CommonModule"; "ОбщаяКоманда"="CommonCommand"
|
||||||
"ОбщаяФорма"="CommonForm"; "Подсистема"="Subsystem"
|
"ОбщаяФорма"="CommonForm"; "Подсистема"="Subsystem"
|
||||||
|
"РегистрРасчёта"="CalculationRegister"; "РегистрРасчета"="CalculationRegister"
|
||||||
|
"ПланВидовРасчёта"="ChartOfCalculationTypes"; "ПланВидовРасчета"="ChartOfCalculationTypes"
|
||||||
|
"Роль"="Role"; "ОбщийМакет"="CommonTemplate"; "ЭлементСтиля"="StyleItem"
|
||||||
|
"ОбщийРеквизит"="CommonAttribute"; "ГруппаКоманд"="CommandGroup"
|
||||||
# Russian plural
|
# Russian plural
|
||||||
"Справочники"="Catalog"; "Документы"="Document"; "Перечисления"="Enum"
|
"Справочники"="Catalog"; "Документы"="Document"; "Перечисления"="Enum"
|
||||||
"Константы"="Constant"; "Отчёты"="Report"; "Отчеты"="Report"; "Обработки"="DataProcessor"
|
"Константы"="Constant"; "Отчёты"="Report"; "Отчеты"="Report"; "Обработки"="DataProcessor"
|
||||||
@@ -389,6 +409,10 @@ $script:typeNormMap = @{
|
|||||||
"БизнесПроцессы"="BusinessProcess"; "Задачи"="Task"
|
"БизнесПроцессы"="BusinessProcess"; "Задачи"="Task"
|
||||||
"ПланыОбмена"="ExchangePlan"; "ЖурналыДокументов"="DocumentJournal"
|
"ПланыОбмена"="ExchangePlan"; "ЖурналыДокументов"="DocumentJournal"
|
||||||
"Подсистемы"="Subsystem"
|
"Подсистемы"="Subsystem"
|
||||||
|
"РегистрыРасчёта"="CalculationRegister"; "РегистрыРасчета"="CalculationRegister"
|
||||||
|
"ПланыВидовРасчёта"="ChartOfCalculationTypes"; "ПланыВидовРасчета"="ChartOfCalculationTypes"
|
||||||
|
"Роли"="Role"; "ОбщиеМакеты"="CommonTemplate"; "ЭлементыСтиля"="StyleItem"
|
||||||
|
"ОбщиеРеквизиты"="CommonAttribute"; "ГруппыКоманд"="CommandGroup"
|
||||||
}
|
}
|
||||||
|
|
||||||
function Normalize-CmdName([string]$name) {
|
function Normalize-CmdName([string]$name) {
|
||||||
@@ -671,8 +695,16 @@ $memStream.Close()
|
|||||||
$text = [System.Text.Encoding]::UTF8.GetString($bytes)
|
$text = [System.Text.Encoding]::UTF8.GetString($bytes)
|
||||||
if ($text.Length -gt 0 -and $text[0] -eq [char]0xFEFF) { $text = $text.Substring(1) }
|
if ($text.Length -gt 0 -and $text[0] -eq [char]0xFEFF) { $text = $text.Substring(1) }
|
||||||
$text = $text.Replace('encoding="utf-8"', 'encoding="UTF-8"')
|
$text = $text.Replace('encoding="utf-8"', 'encoding="UTF-8"')
|
||||||
|
# Пустой элемент: XmlWriter отдаёт `<a />`, Конфигуратор пишет `<a/>`. Внутри
|
||||||
|
# CDATA/комментария ` />` может быть содержимым (там `>` не экранируется),
|
||||||
|
# поэтому они идут первыми ветками альтернации и возвращаются как есть.
|
||||||
|
$text = [regex]::Replace($text, '(?s)<!\[CDATA\[.*?\]\]>|<!--.*?-->|(?<=\S) />', { param($m) if ($m.Value -eq ' />') { '/>' } else { $m.Value } })
|
||||||
|
|
||||||
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
|
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
|
||||||
|
# Целевой перевод строки: стиль файла-назначения — правка наследует его (#44/#46/#47),
|
||||||
|
# новый файл получает канон выгрузки CRLF. Зеркало _detect_xml_style в py-порту.
|
||||||
|
$targetEol = if ((Test-Path -LiteralPath $resolvedPath) -and ([System.IO.File]::ReadAllText($resolvedPath) -notmatch "`r`n")) { "`n" } else { "`r`n" }
|
||||||
|
$text = ($text -replace "`r`n", "`n") -replace "`n", $targetEol
|
||||||
[System.IO.File]::WriteAllText($resolvedPath, $text, $utf8Bom)
|
[System.IO.File]::WriteAllText($resolvedPath, $text, $utf8Bom)
|
||||||
Info "Saved: $resolvedPath"
|
Info "Saved: $resolvedPath"
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# interface-edit v1.8 — Edit 1C CommandInterface.xml
|
# interface-edit v1.18 — Edit 1C CommandInterface.xml (+русские алиасы типов: формы с ё и без)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -10,6 +10,65 @@ import subprocess
|
|||||||
import sys
|
import sys
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
|
|
||||||
|
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
|
||||||
|
# регистр не различают, в argparse совпадение точное.
|
||||||
|
class CIDict(dict):
|
||||||
|
# Ключи храним КАК ЕСТЬ: часть из них — имена объектов (табличные части, стандартные
|
||||||
|
# реквизиты), они попадают в XML. Регистронезависим только поиск. Порядок вставки
|
||||||
|
# сохраняется — от него зависит порядок эмиссии.
|
||||||
|
def _actual(self, key):
|
||||||
|
if not isinstance(key, str) or dict.__contains__(self, key):
|
||||||
|
return key
|
||||||
|
ci = self.__dict__.get('_ci')
|
||||||
|
if ci is None or len(ci) != len(self):
|
||||||
|
ci = {k.lower(): k for k in self if isinstance(k, str)}
|
||||||
|
self.__dict__['_ci'] = ci
|
||||||
|
return ci.get(key.lower(), key)
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
return dict.__getitem__(self, self._actual(key))
|
||||||
|
|
||||||
|
def __contains__(self, key):
|
||||||
|
return dict.__contains__(self, self._actual(key))
|
||||||
|
|
||||||
|
def get(self, key, default=None):
|
||||||
|
return dict.get(self, self._actual(key), default)
|
||||||
|
|
||||||
|
def pop(self, key, *default):
|
||||||
|
return dict.pop(self, self._actual(key), *default)
|
||||||
|
|
||||||
|
def __setitem__(self, key, value):
|
||||||
|
# запись по ключу, отличающемуся регистром, обновляет существующий, а не плодит дубль
|
||||||
|
dict.__setitem__(self, self._actual(key), value)
|
||||||
|
|
||||||
|
def ci_json(obj):
|
||||||
|
"""Рекурсивно оборачивает разобранный JSON: словари → CIDict, списки обходятся."""
|
||||||
|
if isinstance(obj, dict):
|
||||||
|
return CIDict((k, ci_json(v)) for k, v in obj.items())
|
||||||
|
if isinstance(obj, list):
|
||||||
|
return [ci_json(v) for v in obj]
|
||||||
|
return obj
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# Support guard (Ext/ParentConfigurations.bin) — see docs/1c-support-state-spec.md
|
# Support guard (Ext/ParentConfigurations.bin) — see docs/1c-support-state-spec.md
|
||||||
@@ -189,6 +248,16 @@ def assert_edit_allowed(target_path, require):
|
|||||||
|
|
||||||
def detect_format_version(d):
|
def detect_format_version(d):
|
||||||
while d:
|
while d:
|
||||||
|
# Автономная внешняя обработка/отчёт: своего Configuration.xml у неё нет, версию несёт
|
||||||
|
# корень самой обработки. Без этого форма и макет внутри обработки 2.21 писались бы 2.17.
|
||||||
|
ext_path = d + ".xml"
|
||||||
|
if os.path.isfile(ext_path):
|
||||||
|
with open(ext_path, "r", encoding="utf-8-sig") as f:
|
||||||
|
ext_head = f.read(2000)
|
||||||
|
if re.search(r'<(ExternalDataProcessor|ExternalReport)[ >]', ext_head):
|
||||||
|
m = re.search(r'<MetaDataObject[^>]+version="(\d+\.\d+)"', ext_head)
|
||||||
|
if m:
|
||||||
|
return m.group(1)
|
||||||
cfg_path = os.path.join(d, "Configuration.xml")
|
cfg_path = os.path.join(d, "Configuration.xml")
|
||||||
if os.path.isfile(cfg_path):
|
if os.path.isfile(cfg_path):
|
||||||
with open(cfg_path, "r", encoding="utf-8-sig") as f:
|
with open(cfg_path, "r", encoding="utf-8-sig") as f:
|
||||||
@@ -282,7 +351,7 @@ def import_ci_fragment(xml_string):
|
|||||||
def parse_value_list(val):
|
def parse_value_list(val):
|
||||||
val = val.strip()
|
val = val.strip()
|
||||||
if val.startswith("["):
|
if val.startswith("["):
|
||||||
arr = json.loads(val)
|
arr = ci_json(json.loads(val))
|
||||||
return [str(item) for item in arr]
|
return [str(item) for item in arr]
|
||||||
return [val]
|
return [val]
|
||||||
|
|
||||||
@@ -304,21 +373,22 @@ def _detect_xml_style(path):
|
|||||||
|
|
||||||
|
|
||||||
def _finalize_xml_bytes(xml_bytes, style):
|
def _finalize_xml_bytes(xml_bytes, style):
|
||||||
"""Привести сериализованные байты к стилю оригинала (или к дефолту, если style is None)."""
|
"""Привести байты к стилю оригинала; для НОВОГО файла (style is None) — к канону
|
||||||
enc_decl = style["enc"] if style else "utf-8"
|
выгрузки Конфигуратора: encoding="UTF-8", CRLF в разделителях, без перевода в конце."""
|
||||||
|
enc_decl = style["enc"] if style else "UTF-8"
|
||||||
xml_bytes = xml_bytes.replace(
|
xml_bytes = xml_bytes.replace(
|
||||||
b"<?xml version='1.0' encoding='UTF-8'?>",
|
b"<?xml version='1.0' encoding='UTF-8'?>",
|
||||||
b'<?xml version="1.0" encoding="' + enc_decl.encode("ascii") + b'"?>')
|
b'<?xml version="1.0" encoding="' + enc_decl.encode("ascii") + b'"?>')
|
||||||
# Канонизировать переносы к LF (убирает от \r в tail'ах)
|
# Канонизировать переносы к LF (убирает от \r в tail'ах)
|
||||||
xml_bytes = (xml_bytes.replace(b" \n", b"\n").replace(b" ", b"")
|
xml_bytes = (xml_bytes.replace(b" \n", b"\n").replace(b" ", b"")
|
||||||
.replace(b"\r\n", b"\n").replace(b"\r", b"\n"))
|
.replace(b"\r\n", b"\n").replace(b"\r", b"\n"))
|
||||||
# Финальный перенос — как в оригинале (новый файл → есть)
|
# Финальный перенос — как в оригинале (новый файл → нет, канон #57)
|
||||||
want_final_nl = style["final_nl"] if style else True
|
want_final_nl = style["final_nl"] if style else False
|
||||||
xml_bytes = xml_bytes.rstrip(b"\n")
|
xml_bytes = xml_bytes.rstrip(b"\n")
|
||||||
if want_final_nl:
|
if want_final_nl:
|
||||||
xml_bytes += b"\n"
|
xml_bytes += b"\n"
|
||||||
# EOL — как в оригинале (новый файл → LF, текущее поведение)
|
# EOL — как в оригинале (новый файл → CRLF, канон #57)
|
||||||
if style and style["crlf"]:
|
if (style["crlf"] if style else True):
|
||||||
xml_bytes = xml_bytes.replace(b"\n", b"\r\n")
|
xml_bytes = xml_bytes.replace(b"\n", b"\r\n")
|
||||||
return xml_bytes
|
return xml_bytes
|
||||||
|
|
||||||
@@ -357,6 +427,10 @@ TYPE_NORM_MAP = {
|
|||||||
'ПланОбмена': 'ExchangePlan', 'ЖурналДокументов': 'DocumentJournal',
|
'ПланОбмена': 'ExchangePlan', 'ЖурналДокументов': 'DocumentJournal',
|
||||||
'ОбщийМодуль': 'CommonModule', 'ОбщаяКоманда': 'CommonCommand',
|
'ОбщийМодуль': 'CommonModule', 'ОбщаяКоманда': 'CommonCommand',
|
||||||
'ОбщаяФорма': 'CommonForm', 'Подсистема': 'Subsystem',
|
'ОбщаяФорма': 'CommonForm', 'Подсистема': 'Subsystem',
|
||||||
|
'РегистрРасчёта': 'CalculationRegister', 'РегистрРасчета': 'CalculationRegister',
|
||||||
|
'ПланВидовРасчёта': 'ChartOfCalculationTypes', 'ПланВидовРасчета': 'ChartOfCalculationTypes',
|
||||||
|
'Роль': 'Role', 'ОбщийМакет': 'CommonTemplate', 'ЭлементСтиля': 'StyleItem',
|
||||||
|
'ОбщийРеквизит': 'CommonAttribute', 'ГруппаКоманд': 'CommandGroup',
|
||||||
# Russian plural
|
# Russian plural
|
||||||
'Справочники': 'Catalog', 'Документы': 'Document', 'Перечисления': 'Enum',
|
'Справочники': 'Catalog', 'Документы': 'Document', 'Перечисления': 'Enum',
|
||||||
'Константы': 'Constant', 'Отчёты': 'Report', 'Отчеты': 'Report', 'Обработки': 'DataProcessor',
|
'Константы': 'Constant', 'Отчёты': 'Report', 'Отчеты': 'Report', 'Обработки': 'DataProcessor',
|
||||||
@@ -366,6 +440,10 @@ TYPE_NORM_MAP = {
|
|||||||
'БизнесПроцессы': 'BusinessProcess', 'Задачи': 'Task',
|
'БизнесПроцессы': 'BusinessProcess', 'Задачи': 'Task',
|
||||||
'ПланыОбмена': 'ExchangePlan', 'ЖурналыДокументов': 'DocumentJournal',
|
'ПланыОбмена': 'ExchangePlan', 'ЖурналыДокументов': 'DocumentJournal',
|
||||||
'Подсистемы': 'Subsystem',
|
'Подсистемы': 'Subsystem',
|
||||||
|
'РегистрыРасчёта': 'CalculationRegister', 'РегистрыРасчета': 'CalculationRegister',
|
||||||
|
'ПланыВидовРасчёта': 'ChartOfCalculationTypes', 'ПланыВидовРасчета': 'ChartOfCalculationTypes',
|
||||||
|
'Роли': 'Role', 'ОбщиеМакеты': 'CommonTemplate', 'ЭлементыСтиля': 'StyleItem',
|
||||||
|
'ОбщиеРеквизиты': 'CommonAttribute', 'ГруппыКоманд': 'CommandGroup',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -401,7 +479,7 @@ def main():
|
|||||||
parser.add_argument("-Value", default=None)
|
parser.add_argument("-Value", default=None)
|
||||||
parser.add_argument("-CreateIfMissing", action="store_true")
|
parser.add_argument("-CreateIfMissing", action="store_true")
|
||||||
parser.add_argument("-NoValidate", action="store_true")
|
parser.add_argument("-NoValidate", action="store_true")
|
||||||
args = parser.parse_args()
|
args = ci_parse_args(parser)
|
||||||
|
|
||||||
# --- Mode validation ---
|
# --- Mode validation ---
|
||||||
if args.DefinitionFile and args.Operation:
|
if args.DefinitionFile and args.Operation:
|
||||||
@@ -438,7 +516,12 @@ def main():
|
|||||||
f'\tversion="{format_version}">\n'
|
f'\tversion="{format_version}">\n'
|
||||||
f'</CommandInterface>'
|
f'</CommandInterface>'
|
||||||
)
|
)
|
||||||
with open(ci_path, "w", encoding="utf-8-sig") as fh:
|
# Файл СОЗДАЁМ — пишем канон выгрузки: CRLF в разделителях. (Правка
|
||||||
|
# существующего файла, наоборот, наследует его стиль — это делает
|
||||||
|
# save_xml_bom через _detect_xml_style.) newline="" обязателен: без него
|
||||||
|
# текстовый режим дал бы CRLF на Windows и LF на macOS.
|
||||||
|
empty_ci = empty_ci.replace("\r\n", "\n").replace("\n", "\r\n").rstrip("\r\n")
|
||||||
|
with open(ci_path, "w", encoding="utf-8-sig", newline="") as fh:
|
||||||
fh.write(empty_ci)
|
fh.write(empty_ci)
|
||||||
print(f"[INFO] Created new CommandInterface.xml: {ci_path}")
|
print(f"[INFO] Created new CommandInterface.xml: {ci_path}")
|
||||||
else:
|
else:
|
||||||
@@ -564,7 +647,7 @@ def main():
|
|||||||
|
|
||||||
def do_place(json_val):
|
def do_place(json_val):
|
||||||
nonlocal add_count, modify_count
|
nonlocal add_count, modify_count
|
||||||
defn = json_val if isinstance(json_val, dict) else json.loads(json_val)
|
defn = ci_json(json_val if isinstance(json_val, dict) else json.loads(json_val))
|
||||||
cmd_name = normalize_cmd_name(str(defn["command"]))
|
cmd_name = normalize_cmd_name(str(defn["command"]))
|
||||||
group_name = str(defn["group"])
|
group_name = str(defn["group"])
|
||||||
if not cmd_name or not group_name:
|
if not cmd_name or not group_name:
|
||||||
@@ -592,7 +675,7 @@ def main():
|
|||||||
|
|
||||||
def do_order(json_val):
|
def do_order(json_val):
|
||||||
nonlocal add_count, remove_count
|
nonlocal add_count, remove_count
|
||||||
defn = json_val if isinstance(json_val, dict) else json.loads(json_val)
|
defn = ci_json(json_val if isinstance(json_val, dict) else json.loads(json_val))
|
||||||
group_name = str(defn["group"])
|
group_name = str(defn["group"])
|
||||||
commands = [normalize_cmd_name(str(c)) for c in defn["commands"]]
|
commands = [normalize_cmd_name(str(c)) for c in defn["commands"]]
|
||||||
if not group_name or not commands:
|
if not group_name or not commands:
|
||||||
@@ -626,7 +709,7 @@ def main():
|
|||||||
|
|
||||||
def do_subsystem_order(json_val):
|
def do_subsystem_order(json_val):
|
||||||
nonlocal add_count, remove_count
|
nonlocal add_count, remove_count
|
||||||
parsed = json_val if isinstance(json_val, list) else json.loads(json_val)
|
parsed = ci_json(json_val if isinstance(json_val, list) else json.loads(json_val))
|
||||||
subsystems = [str(s) for s in parsed]
|
subsystems = [str(s) for s in parsed]
|
||||||
if not subsystems:
|
if not subsystems:
|
||||||
print("subsystem-order requires array of subsystem paths", file=sys.stderr)
|
print("subsystem-order requires array of subsystem paths", file=sys.stderr)
|
||||||
@@ -651,7 +734,7 @@ def main():
|
|||||||
|
|
||||||
def do_group_order(json_val):
|
def do_group_order(json_val):
|
||||||
nonlocal add_count, remove_count
|
nonlocal add_count, remove_count
|
||||||
parsed = json_val if isinstance(json_val, list) else json.loads(json_val)
|
parsed = ci_json(json_val if isinstance(json_val, list) else json.loads(json_val))
|
||||||
groups = [str(g) for g in parsed]
|
groups = [str(g) for g in parsed]
|
||||||
if not groups:
|
if not groups:
|
||||||
print("group-order requires array of group names", file=sys.stderr)
|
print("group-order requires array of group names", file=sys.stderr)
|
||||||
@@ -681,7 +764,7 @@ def main():
|
|||||||
if not os.path.isabs(def_file):
|
if not os.path.isabs(def_file):
|
||||||
def_file = os.path.join(os.getcwd(), def_file)
|
def_file = os.path.join(os.getcwd(), def_file)
|
||||||
with open(def_file, "r", encoding="utf-8-sig") as fh:
|
with open(def_file, "r", encoding="utf-8-sig") as fh:
|
||||||
ops = json.loads(fh.read())
|
ops = ci_json(json.loads(fh.read()))
|
||||||
if isinstance(ops, list):
|
if isinstance(ops, list):
|
||||||
operations = ops
|
operations = ops
|
||||||
else:
|
else:
|
||||||
@@ -691,19 +774,21 @@ def main():
|
|||||||
|
|
||||||
for op in operations:
|
for op in operations:
|
||||||
op_name = op.get("operation", args.Operation or "")
|
op_name = op.get("operation", args.Operation or "")
|
||||||
|
# PS сравнивает имя операции через switch, а он регистронезависим.
|
||||||
|
op_key = str(op_name).lower()
|
||||||
op_value = op.get("value", args.Value or "")
|
op_value = op.get("value", args.Value or "")
|
||||||
|
|
||||||
if op_name == "hide":
|
if op_key == "hide":
|
||||||
do_hide(parse_value_list(op_value))
|
do_hide(parse_value_list(op_value))
|
||||||
elif op_name == "show":
|
elif op_key == "show":
|
||||||
do_show(parse_value_list(op_value))
|
do_show(parse_value_list(op_value))
|
||||||
elif op_name == "place":
|
elif op_key == "place":
|
||||||
do_place(op_value)
|
do_place(op_value)
|
||||||
elif op_name == "order":
|
elif op_key == "order":
|
||||||
do_order(op_value)
|
do_order(op_value)
|
||||||
elif op_name == "subsystem-order":
|
elif op_key == "subsystem-order":
|
||||||
do_subsystem_order(op_value)
|
do_subsystem_order(op_value)
|
||||||
elif op_name == "group-order":
|
elif op_key == "group-order":
|
||||||
do_group_order(op_value)
|
do_group_order(op_value)
|
||||||
else:
|
else:
|
||||||
print(f"Unknown operation: {op_name}", file=sys.stderr)
|
print(f"Unknown operation: {op_name}", file=sys.stderr)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# interface-validate v1.1 — Validate 1C CommandInterface.xml structure
|
# interface-validate v1.3 — Validate 1C CommandInterface.xml structure (+Report-*: общий эталон вывода валидаторов)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)][Alias('Path')][string]$CIPath,
|
[Parameter(Mandatory)][Alias('Path')][string]$CIPath,
|
||||||
@@ -51,16 +51,21 @@ $script:output = New-Object System.Text.StringBuilder 8192
|
|||||||
$script:allCommandNames = @()
|
$script:allCommandNames = @()
|
||||||
|
|
||||||
function Out-Line([string]$msg) { $script:output.AppendLine($msg) | Out-Null }
|
function Out-Line([string]$msg) { $script:output.AppendLine($msg) | Out-Null }
|
||||||
function Report-OK([string]$msg) {
|
function Report-OK {
|
||||||
|
param([string]$msg)
|
||||||
$script:okCount++
|
$script:okCount++
|
||||||
if ($Detailed) { Out-Line "[OK] $msg" }
|
if ($Detailed) { Out-Line "[OK] $msg" }
|
||||||
}
|
}
|
||||||
function Report-Error([string]$msg) {
|
function Report-Error {
|
||||||
|
param([string]$msg)
|
||||||
$script:errors++
|
$script:errors++
|
||||||
Out-Line "[ERROR] $msg"
|
Out-Line "[ERROR] $msg"
|
||||||
if ($script:errors -ge $MaxErrors) { $script:stopped = $true }
|
if ($script:errors -ge $MaxErrors) {
|
||||||
|
$script:stopped = $true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
function Report-Warn([string]$msg) {
|
function Report-Warn {
|
||||||
|
param([string]$msg)
|
||||||
$script:warnings++
|
$script:warnings++
|
||||||
Out-Line "[WARN] $msg"
|
Out-Line "[WARN] $msg"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,32 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# interface-validate v1.1 — Validate 1C CommandInterface.xml structure
|
# interface-validate v1.3 — Validate 1C CommandInterface.xml structure (+Report-*: общий эталон вывода валидаторов)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
"""Validates CommandInterface.xml sections, command references, order, duplicates."""
|
"""Validates CommandInterface.xml sections, command references, order, duplicates."""
|
||||||
import sys, os, argparse, re
|
import sys, os, argparse, re
|
||||||
from lxml import etree
|
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)
|
||||||
|
|
||||||
|
|
||||||
NS_CI = 'http://v8.1c.ru/8.3/xcf/extrnprops'
|
NS_CI = 'http://v8.1c.ru/8.3/xcf/extrnprops'
|
||||||
NS_XR = 'http://v8.1c.ru/8.3/xcf/readable'
|
NS_XR = 'http://v8.1c.ru/8.3/xcf/readable'
|
||||||
NS_XSI = 'http://www.w3.org/2001/XMLSchema-instance'
|
NS_XSI = 'http://www.w3.org/2001/XMLSchema-instance'
|
||||||
@@ -83,7 +105,7 @@ def main():
|
|||||||
parser.add_argument('-Detailed', action='store_true')
|
parser.add_argument('-Detailed', action='store_true')
|
||||||
parser.add_argument('-MaxErrors', dest='MaxErrors', type=int, default=30)
|
parser.add_argument('-MaxErrors', dest='MaxErrors', type=int, default=30)
|
||||||
parser.add_argument('-OutFile', dest='OutFile', default='')
|
parser.add_argument('-OutFile', dest='OutFile', default='')
|
||||||
args = parser.parse_args()
|
args = ci_parse_args(parser)
|
||||||
|
|
||||||
ci_path = args.CIPath
|
ci_path = args.CIPath
|
||||||
detailed = args.Detailed
|
detailed = args.Detailed
|
||||||
|
|||||||
@@ -7,16 +7,21 @@
|
|||||||
| Ключ | Умолчание | Значения |
|
| Ключ | Умолчание | Значения |
|
||||||
|------|-----------|----------|
|
|------|-----------|----------|
|
||||||
| `rootURL` | `= name` (в нижнем регистре) | корневой URL |
|
| `rootURL` | `= name` (в нижнем регистре) | корневой URL |
|
||||||
| `reuseSessions` | `DontUse` | `DontUse` / `AutoUse` |
|
| `reuseSessions` | `DontUse` | `DontUse` / `Use` / `AutoUse` |
|
||||||
| `sessionMaxAge` | `20` | время жизни сессии, сек |
|
| `sessionMaxAge` | `20` | время жизни сессии, сек |
|
||||||
| `urlTemplates` | `{}` | шаблоны URL (см. ниже) |
|
| `urlTemplates` | `{}` | шаблоны URL (см. ниже) |
|
||||||
|
|
||||||
`urlTemplates` — объект `{ "ИмяШаблона": def }`, где `def`:
|
`urlTemplates` — объект `{ "ИмяШаблона": def }`, где `def`:
|
||||||
- строка — URL-путь без методов: `"/health"`;
|
- строка — URL-путь без методов: `"/health"`;
|
||||||
- объект: `template` (путь с параметрами `{id}`, по умолчанию `/имяшаблона`), `methods` — `{ "ИмяМетода": "HTTPMethod" }`.
|
- объект: `template` (путь с параметрами `{id}`, по умолчанию `/имяшаблона`), `synonym`, `comment`,
|
||||||
|
`methods` — `{ "ИмяМетода": def }`.
|
||||||
|
|
||||||
|
`methods` — значение либо строка (только HTTP-метод), либо объект: `httpMethod`, `handler`,
|
||||||
|
`synonym`, `comment`.
|
||||||
|
|
||||||
HTTP-методы: `GET`, `POST`, `PUT`, `DELETE`, `PATCH`, `HEAD`, `OPTIONS`, `CONNECT`, `TRACE`, `MERGE`.
|
HTTP-методы: `GET`, `POST`, `PUT`, `DELETE`, `PATCH`, `HEAD`, `OPTIONS`, `CONNECT`, `TRACE`, `MERGE`.
|
||||||
Обработчик метода в модуле именуется `{ИмяШаблона}{ИмяМетода}`.
|
Обработчик по умолчанию именуется `{ИмяШаблона}{ИмяМетода}`; в типовых конфигурациях он часто
|
||||||
|
произвольный — тогда задавайте `handler` явно.
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{ "type": "HTTPService", "name": "API", "rootURL": "api",
|
{ "type": "HTTPService", "name": "API", "rootURL": "api",
|
||||||
@@ -31,21 +36,29 @@ HTTP-методы: `GET`, `POST`, `PUT`, `DELETE`, `PATCH`, `HEAD`, `OPTIONS`, `
|
|||||||
| Ключ | Умолчание | Значения |
|
| Ключ | Умолчание | Значения |
|
||||||
|------|-----------|----------|
|
|------|-----------|----------|
|
||||||
| `namespace` | пусто | URI пространства имён WSDL |
|
| `namespace` | пусто | URI пространства имён WSDL |
|
||||||
| `xdtoPackages` | пусто | XDTO-пакеты |
|
| `xdtoPackages` | пусто | список пакетов (см. ниже) |
|
||||||
| `reuseSessions` | `DontUse` | `DontUse` / `AutoUse` |
|
| `descriptorFileName` | `= name` + `.1cws` | имя файла дескриптора |
|
||||||
|
| `reuseSessions` | `DontUse` | `DontUse` / `Use` / `AutoUse` |
|
||||||
| `sessionMaxAge` | `20` | время жизни сессии, сек |
|
| `sessionMaxAge` | `20` | время жизни сессии, сек |
|
||||||
| `operations` | `{}` | операции (см. ниже) |
|
| `operations` | `{}` | операции (см. ниже) |
|
||||||
|
|
||||||
|
`xdtoPackages` — **массив** значений: `"XDTOPackage.Имя"` — пакет конфигурации, любое другое
|
||||||
|
значение — URI внешнего пространства имён (например `"http://v8.1c.ru/8.3/data/ext"`).
|
||||||
|
|
||||||
`operations` — объект `{ "ИмяОперации": def }`, где `def`:
|
`operations` — объект `{ "ИмяОперации": def }`, где `def`:
|
||||||
- строка — XDTO-тип возврата без параметров: `"xs:string"`;
|
- строка — XDTO-тип возврата без параметров: `"xs:string"`;
|
||||||
- объект: `returnType` (по умолчанию `xs:string`), `nillable` (bool), `transactioned` (bool),
|
- объект: `returnType` (по умолчанию `xs:string`), `nillable` (bool), `transactioned` (bool),
|
||||||
`handler` (имя процедуры, по умолчанию = имя операции), `parameters`.
|
`procedureName` (имя процедуры, по умолчанию = имя операции; синоним ключа — `handler`),
|
||||||
|
`dataLockControlMode` (по умолчанию `Managed`), `synonym`, `comment`, `parameters`.
|
||||||
|
|
||||||
`parameters` — объект `{ "ИмяПараметра": def }`, где `def`:
|
`parameters` — объект `{ "ИмяПараметра": def }`, где `def`:
|
||||||
- строка — XDTO-тип (`direction` = `In`);
|
- строка — XDTO-тип (`direction` = `In`);
|
||||||
- объект: `type` (по умолчанию `xs:string`), `nillable` (bool, по умолчанию `true`), `direction` (`In` / `Out` / `InOut`).
|
- объект: `type` (по умолчанию `xs:string`), `nillable` (bool, по умолчанию `true`),
|
||||||
|
`direction` (`In` / `Out` / `InOut`), `synonym`, `comment`.
|
||||||
|
|
||||||
XDTO-типы: `xs:string`, `xs:boolean`, `xs:int`, `xs:long`, `xs:decimal`, `xs:dateTime`, `xs:base64Binary`.
|
XDTO-типы: `xs:string`, `xs:boolean`, `xs:int`, `xs:long`, `xs:decimal`, `xs:dateTime`, `xs:base64Binary`.
|
||||||
|
Тип из собственного пространства имён задаётся в нотации Кларка — `"{http://ваш.uri}ИмяТипа"`;
|
||||||
|
компилятор сам объявит локальный `xmlns` в теге, как это делает платформа.
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{ "type": "WebService", "name": "DataExchange", "namespace": "http://www.1c.ru/DataExchange",
|
{ "type": "WebService", "name": "DataExchange", "namespace": "http://www.1c.ru/DataExchange",
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
|||||||
# meta-decompile v0.55 — XML объекта метаданных 1С → JSON-черновик формата meta-compile
|
# meta-decompile v0.64 — XML объекта метаданных 1С → JSON-черновик формата meta-compile
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
#
|
#
|
||||||
# Поддержаны: Catalog, ExchangePlan, ChartOfCharacteristicTypes, ChartOfAccounts, ChartOfCalculationTypes, Document,
|
# Поддержаны: Catalog, ExchangePlan, ChartOfCharacteristicTypes, ChartOfAccounts, ChartOfCalculationTypes, Document,
|
||||||
@@ -92,7 +92,7 @@ foreach ($c in $rootEl.ChildNodes) { if ($c.NodeType -eq 'Element') { $objNode =
|
|||||||
if (-not $objNode) { [Console]::Error.WriteLine("meta-decompile: пустой MetaDataObject"); exit 3 }
|
if (-not $objNode) { [Console]::Error.WriteLine("meta-decompile: пустой MetaDataObject"); exit 3 }
|
||||||
$objType = $objNode.LocalName
|
$objType = $objNode.LocalName
|
||||||
|
|
||||||
if ($objType -notin @('Catalog', 'ExchangePlan', 'ChartOfCharacteristicTypes', 'ChartOfAccounts', 'ChartOfCalculationTypes', 'Document', 'InformationRegister', 'AccumulationRegister', 'AccountingRegister', 'CalculationRegister', 'BusinessProcess', 'Task', 'Enum', 'Report', 'DataProcessor', 'Constant', 'DefinedType', 'FunctionalOption', 'DocumentJournal', 'Sequence', 'FilterCriterion', 'DocumentNumerator', 'SettingsStorage', 'CommonModule', 'EventSubscription', 'ScheduledJob', 'CommonForm', 'SessionParameter', 'CommonCommand', 'CommandGroup', 'CommonAttribute', 'FunctionalOptionsParameter', 'WSReference', 'CommonPicture', 'CommonTemplate')) {
|
if ($objType -notin @('Catalog', 'ExchangePlan', 'ChartOfCharacteristicTypes', 'ChartOfAccounts', 'ChartOfCalculationTypes', 'Document', 'InformationRegister', 'AccumulationRegister', 'AccountingRegister', 'CalculationRegister', 'BusinessProcess', 'Task', 'Enum', 'Report', 'DataProcessor', 'Constant', 'DefinedType', 'FunctionalOption', 'DocumentJournal', 'Sequence', 'FilterCriterion', 'DocumentNumerator', 'SettingsStorage', 'CommonModule', 'EventSubscription', 'ScheduledJob', 'CommonForm', 'SessionParameter', 'CommonCommand', 'CommandGroup', 'CommonAttribute', 'FunctionalOptionsParameter', 'WSReference', 'CommonPicture', 'CommonTemplate', 'HTTPService', 'WebService')) {
|
||||||
[Console]::Error.WriteLine("meta-decompile: тип '$objType' пока не поддержан (…, CommonPicture, CommonTemplate)"); exit 3
|
[Console]::Error.WriteLine("meta-decompile: тип '$objType' пока не поддержан (…, CommonPicture, CommonTemplate)"); exit 3
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,6 +219,9 @@ function Get-TypeShorthand {
|
|||||||
# string/dateTime/DesignTimeRef → строка (компилятор auto-детектит обратно).
|
# string/dateTime/DesignTimeRef → строка (компилятор auto-детектит обратно).
|
||||||
function Convert-ChScalarNode {
|
function Convert-ChScalarNode {
|
||||||
param($vN)
|
param($vN)
|
||||||
|
# nil-элемент массива (<v8:Value xsi:nil="true"/>) → JSON null. Без этого он приезжал пустой
|
||||||
|
# строкой и компилятор эмитил xs:string вместо nil.
|
||||||
|
if ($vN.GetAttribute('nil', 'http://www.w3.org/2001/XMLSchema-instance') -eq 'true') { return $null }
|
||||||
$xt = $vN.GetAttribute('type', 'http://www.w3.org/2001/XMLSchema-instance')
|
$xt = $vN.GetAttribute('type', 'http://www.w3.org/2001/XMLSchema-instance')
|
||||||
$txt = $vN.InnerText
|
$txt = $vN.InnerText
|
||||||
if ($xt -match 'boolean$') { return ($txt -eq 'true') }
|
if ($xt -match 'boolean$') { return ($txt -eq 'true') }
|
||||||
@@ -226,6 +229,9 @@ function Convert-ChScalarNode {
|
|||||||
if ($txt -match '^-?\d+$') { return [int]$txt }
|
if ($txt -match '^-?\d+$') { return [int]$txt }
|
||||||
return [double]::Parse($txt, [System.Globalization.CultureInfo]::InvariantCulture)
|
return [double]::Parse($txt, [System.Globalization.CultureInfo]::InvariantCulture)
|
||||||
}
|
}
|
||||||
|
# Пустой DesignTimeRef ≠ пустая строка: без маркера тип терялся, и компилятор эмитил xs:string.
|
||||||
|
# Та же конвенция, что у fillValue (см. ниже) — маркер emptyRef.
|
||||||
|
if ($xt -match 'DesignTimeRef$' -and $txt -eq '') { return [ordered]@{ emptyRef = $true } }
|
||||||
return $txt
|
return $txt
|
||||||
}
|
}
|
||||||
# app:value (тип прямо на узле) → значение ЛИБО массив (v8:FixedArray с детьми v8:Value).
|
# app:value (тип прямо на узле) → значение ЛИБО массив (v8:FixedArray с детьми v8:Value).
|
||||||
@@ -429,7 +435,9 @@ $cmt = P 'Comment'; if ($cmt) { $dsl['comment'] = $cmt }
|
|||||||
|
|
||||||
# Свойства Catalog (omit-on-default). Порядок ключей — как удобно DSL.
|
# Свойства Catalog (omit-on-default). Порядок ключей — как удобно DSL.
|
||||||
function Add-BoolProp { param([string]$key, [string]$tag, [bool]$default) $v = P $tag; if ($null -ne $v) { $b = ($v -eq 'true'); if ($b -ne $default) { $dsl[$key] = $b } } }
|
function Add-BoolProp { param([string]$key, [string]$tag, [bool]$default) $v = P $tag; if ($null -ne $v) { $b = ($v -eq 'true'); if ($b -ne $default) { $dsl[$key] = $b } } }
|
||||||
function Add-EnumProp { param([string]$key, [string]$tag, [string]$default) $v = P $tag; if ($null -ne $v -and $v -ne '' -and $v -ne $default) { $dsl[$key] = $v } }
|
# -cne: сравнение с дефолтом ВСЕГДА регистрочувствительное. PS -ne регистронезависим, и значение,
|
||||||
|
# отличающееся от дефолта только регистром, молча терялось (ловилось трижды: синонимы, RootURL).
|
||||||
|
function Add-EnumProp { param([string]$key, [string]$tag, [string]$default) $v = P $tag; if ($null -ne $v -and $v -ne '' -and $v -cne $default) { $dsl[$key] = $v } }
|
||||||
function Add-IntProp { param([string]$key, [string]$tag, [int]$default) $v = P $tag; if ($null -ne $v -and $v -ne '') { $iv = [int]$v; if ($iv -ne $default) { $dsl[$key] = $iv } } }
|
function Add-IntProp { param([string]$key, [string]$tag, [int]$default) $v = P $tag; if ($null -ne $v -and $v -ne '') { $iv = [int]$v; if ($iv -ne $default) { $dsl[$key] = $iv } } }
|
||||||
|
|
||||||
Add-BoolProp 'hierarchical' 'Hierarchical' $false
|
Add-BoolProp 'hierarchical' 'Hierarchical' $false
|
||||||
@@ -713,7 +721,7 @@ if ($objType -eq 'CommonForm') {
|
|||||||
if ($upNode) {
|
if ($upNode) {
|
||||||
$ups = @($upNode.SelectNodes('v8:Value', $nsm) | ForEach-Object { $_.InnerText })
|
$ups = @($upNode.SelectNodes('v8:Value', $nsm) | ForEach-Object { $_.InnerText })
|
||||||
$def2 = @('PlatformApplication', 'MobilePlatformApplication')
|
$def2 = @('PlatformApplication', 'MobilePlatformApplication')
|
||||||
$same = ($ups.Count -eq $def2.Count); if ($same) { for ($k=0; $k -lt $ups.Count; $k++) { if ($ups[$k] -ne $def2[$k]) { $same=$false; break } } }
|
$same = ($ups.Count -eq $def2.Count); if ($same) { for ($k=0; $k -lt $ups.Count; $k++) { if ($ups[$k] -cne $def2[$k]) { $same=$false; break } } }
|
||||||
if (-not $same -and $ups.Count -gt 0) { $dsl['usePurposes'] = [System.Collections.ArrayList]@($ups) }
|
if (-not $same -and $ups.Count -gt 0) { $dsl['usePurposes'] = [System.Collections.ArrayList]@($ups) }
|
||||||
}
|
}
|
||||||
$ep = Get-MLValue ($props.SelectSingleNode('md:ExtendedPresentation', $nsm)); if ($null -ne $ep) { $dsl['extendedPresentation'] = $ep }
|
$ep = Get-MLValue ($props.SelectSingleNode('md:ExtendedPresentation', $nsm)); if ($null -ne $ep) { $dsl['extendedPresentation'] = $ep }
|
||||||
@@ -775,6 +783,43 @@ if ($objType -eq 'CommonCommand') {
|
|||||||
Add-BoolProp 'modifiesData' 'ModifiesData' $false
|
Add-BoolProp 'modifiesData' 'ModifiesData' $false
|
||||||
Add-EnumProp 'onMainServerUnavalableBehavior' 'OnMainServerUnavalableBehavior' 'Auto'
|
Add-EnumProp 'onMainServerUnavalableBehavior' 'OnMainServerUnavalableBehavior' 'Auto'
|
||||||
}
|
}
|
||||||
|
# XDTO-тип из элемента: если значение с префиксом (d6p1:Local) — разворачиваем префикс в URI и
|
||||||
|
# отдаём в нотации Кларка "{uri}Local"; префиксы платформы (dNpM) произвольны и переносу не подлежат.
|
||||||
|
function Get-XDTOTypeValue {
|
||||||
|
param($node)
|
||||||
|
if (-not $node) { return $null }
|
||||||
|
$txt = $node.InnerText
|
||||||
|
if ($txt -match '^([\w.-]+):(.+)$') {
|
||||||
|
$prefix = $Matches[1]; $local = $Matches[2]
|
||||||
|
$uri = $node.GetNamespaceOfPrefix($prefix)
|
||||||
|
# xs: и прочие стандартные оставляем как есть — компилятор их пишет дословно.
|
||||||
|
if ($uri -and $prefix -notin @('xs','xsi','v8','xr')) { return "{$uri}$local" }
|
||||||
|
}
|
||||||
|
return $txt
|
||||||
|
}
|
||||||
|
# WebService — пространство имён, состав XDTO-пакетов, дескриптор, операции с параметрами.
|
||||||
|
if ($objType -eq 'WebService') {
|
||||||
|
$ns = P 'Namespace'; if ($ns) { $dsl['namespace'] = $ns }
|
||||||
|
$pkgNodes = @($props.SelectNodes('md:XDTOPackages/xr:Item/xr:Value', $nsm))
|
||||||
|
if ($pkgNodes.Count -gt 0) {
|
||||||
|
$pkgs = [System.Collections.ArrayList]@()
|
||||||
|
foreach ($pn in $pkgNodes) { [void]$pkgs.Add($pn.InnerText) }
|
||||||
|
$dsl['xdtoPackages'] = $pkgs
|
||||||
|
}
|
||||||
|
$dfn = P 'DescriptorFileName'
|
||||||
|
if ($dfn -and $dfn -cne "$objName.1cws") { $dsl['descriptorFileName'] = $dfn }
|
||||||
|
Add-EnumProp 'reuseSessions' 'ReuseSessions' 'DontUse'
|
||||||
|
Add-IntProp 'sessionMaxAge' 'SessionMaxAge' 20
|
||||||
|
}
|
||||||
|
# HTTPService — корневой URL, повторное использование сеансов, время жизни сеанса.
|
||||||
|
# Шаблоны URL с методами разбираются в блоке ChildObjects.
|
||||||
|
if ($objType -eq 'HTTPService') {
|
||||||
|
# -cne: дефолт — имя в нижнем регистре, но реальный RootURL часто отличается ТОЛЬКО регистром
|
||||||
|
# (MobileAppReceiptScanner), и регистронезависимое сравнение считало его дефолтным.
|
||||||
|
$ru = P 'RootURL'; if ($ru -and $ru -cne $objName.ToLower()) { $dsl['rootURL'] = $ru }
|
||||||
|
Add-EnumProp 'reuseSessions' 'ReuseSessions' 'DontUse'
|
||||||
|
Add-IntProp 'sessionMaxAge' 'SessionMaxAge' 20
|
||||||
|
}
|
||||||
# CommonAttribute — общий реквизит: тип + value-свойства + состав объектов + свойства разделения данных.
|
# CommonAttribute — общий реквизит: тип + value-свойства + состав объектов + свойства разделения данных.
|
||||||
if ($objType -eq 'CommonAttribute') {
|
if ($objType -eq 'CommonAttribute') {
|
||||||
$vt = Get-TypeShorthand ($props.SelectSingleNode('md:Type', $nsm)); if ($vt -and $vt -ne 'String(0)') { $dsl['valueType'] = $vt }
|
$vt = Get-TypeShorthand ($props.SelectSingleNode('md:Type', $nsm)); if ($vt -and $vt -ne 'String(0)') { $dsl['valueType'] = $vt }
|
||||||
@@ -912,7 +957,7 @@ if ($ibNode) {
|
|||||||
if ($cl -gt 0) { $ibDef += "StandardAttribute.Code" }
|
if ($cl -gt 0) { $ibDef += "StandardAttribute.Code" }
|
||||||
$ibShort = @($ibActual | ForEach-Object { Short-Field $_ })
|
$ibShort = @($ibActual | ForEach-Object { Short-Field $_ })
|
||||||
$same = ($ibShort.Count -eq $ibDef.Count)
|
$same = ($ibShort.Count -eq $ibDef.Count)
|
||||||
if ($same) { for ($k = 0; $k -lt $ibShort.Count; $k++) { if ($ibShort[$k] -ne $ibDef[$k]) { $same = $false; break } } }
|
if ($same) { for ($k = 0; $k -lt $ibShort.Count; $k++) { if ($ibShort[$k] -cne $ibDef[$k]) { $same = $false; break } } }
|
||||||
if (-not $same) { $dsl['inputByString'] = [System.Collections.ArrayList]@($ibShort) }
|
if (-not $same) { $dsl['inputByString'] = [System.Collections.ArrayList]@($ibShort) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -984,7 +1029,13 @@ if ($charsNode) {
|
|||||||
filterField = Shorten-CharField (& $gt 'TypesFilterField' $ct) $tFrom
|
filterField = Shorten-CharField (& $gt 'TypesFilterField' $ct) $tFrom
|
||||||
filterValue = if ($tfvNil -eq 'true') { $null } else { Convert-ChScalarNode $tfvNode }
|
filterValue = if ($tfvNil -eq 'true') { $null } else { Convert-ChScalarNode $tfvNode }
|
||||||
}
|
}
|
||||||
$dpf = & $giv 'DataPathField' $ct; if ($dpf -ne -1) { $types['dataPathField'] = $dpf }
|
# DataPathField полиморфно: обычно -1, но встречается ПУТЬ к полю (8 случаев на корпус).
|
||||||
|
# Жёсткое [int] на нём роняло декомпиляцию всего объекта.
|
||||||
|
$dpfN = $ct.SelectSingleNode('xr:DataPathField', $nsm)
|
||||||
|
$dpfT = if ($dpfN) { $dpfN.InnerText } else { '' }
|
||||||
|
if ($dpfT -ne '' -and $dpfT -cne '-1') {
|
||||||
|
$types['dataPathField'] = if ($dpfT -match '^-?\d+$') { [int]$dpfT } else { Shorten-CharField $dpfT $tFrom }
|
||||||
|
}
|
||||||
$mvu = & $giv 'MultipleValuesUseField' $ct; if ($mvu -ne -1) { $types['multipleValuesUseField'] = $mvu }
|
$mvu = & $giv 'MultipleValuesUseField' $ct; if ($mvu -ne -1) { $types['multipleValuesUseField'] = $mvu }
|
||||||
$values = [ordered]@{
|
$values = [ordered]@{
|
||||||
from = $vFrom
|
from = $vFrom
|
||||||
@@ -1116,6 +1167,104 @@ if ($saNode) {
|
|||||||
# --- ChildObjects: Attributes + TabularSections ---
|
# --- ChildObjects: Attributes + TabularSections ---
|
||||||
$childObjs = $objNode.SelectSingleNode('md:ChildObjects', $nsm)
|
$childObjs = $objNode.SelectSingleNode('md:ChildObjects', $nsm)
|
||||||
if ($childObjs) {
|
if ($childObjs) {
|
||||||
|
# WebService: операции с параметрами. Строчное сокращение — только тип возврата (когда всё
|
||||||
|
# остальное дефолтно); иначе объект с nillable/transactioned/procedureName/параметрами.
|
||||||
|
$opNodes = @($childObjs.SelectNodes('md:Operation', $nsm))
|
||||||
|
if ($opNodes.Count -gt 0) {
|
||||||
|
$ops = [ordered]@{}
|
||||||
|
foreach ($op in $opNodes) {
|
||||||
|
$op_p = $op.SelectSingleNode('md:Properties', $nsm)
|
||||||
|
$opName = ($op_p.SelectSingleNode('md:Name', $nsm)).InnerText
|
||||||
|
$o = [ordered]@{}
|
||||||
|
$rt = Get-XDTOTypeValue ($op_p.SelectSingleNode('md:XDTOReturningValueType', $nsm))
|
||||||
|
if ($rt -and $rt -cne 'xs:string') { $o['returnType'] = $rt }
|
||||||
|
$nil = $op_p.SelectSingleNode('md:Nillable', $nsm)
|
||||||
|
if ($nil -and $nil.InnerText -eq 'true') { $o['nillable'] = $true }
|
||||||
|
$tr = $op_p.SelectSingleNode('md:Transactioned', $nsm)
|
||||||
|
if ($tr -and $tr.InnerText -eq 'true') { $o['transactioned'] = $true }
|
||||||
|
$pn = $op_p.SelectSingleNode('md:ProcedureName', $nsm)
|
||||||
|
if ($pn -and $pn.InnerText -cne $opName) { $o['procedureName'] = $pn.InnerText }
|
||||||
|
$dl = $op_p.SelectSingleNode('md:DataLockControlMode', $nsm)
|
||||||
|
if ($dl -and $dl.InnerText -cne 'Managed') { $o['dataLockControlMode'] = $dl.InnerText }
|
||||||
|
$osyn = Get-MLValue ($op_p.SelectSingleNode('md:Synonym', $nsm))
|
||||||
|
if ($null -ne $osyn -and "$osyn" -cne (Split-CamelWords $opName)) { $o['synonym'] = $osyn }
|
||||||
|
$ocmt = $op_p.SelectSingleNode('md:Comment', $nsm)
|
||||||
|
if ($ocmt -and $ocmt.InnerText) { $o['comment'] = $ocmt.InnerText }
|
||||||
|
|
||||||
|
$parNodes = @($op.SelectNodes('md:ChildObjects/md:Parameter', $nsm))
|
||||||
|
if ($parNodes.Count -gt 0) {
|
||||||
|
$pars = [ordered]@{}
|
||||||
|
foreach ($par in $parNodes) {
|
||||||
|
$pp = $par.SelectSingleNode('md:Properties', $nsm)
|
||||||
|
$parName = ($pp.SelectSingleNode('md:Name', $nsm)).InnerText
|
||||||
|
$po = [ordered]@{}
|
||||||
|
$pt = Get-XDTOTypeValue ($pp.SelectSingleNode('md:XDTOValueType', $nsm))
|
||||||
|
if ($pt) { $po['type'] = $pt }
|
||||||
|
$pnil = $pp.SelectSingleNode('md:Nillable', $nsm)
|
||||||
|
if ($pnil -and $pnil.InnerText -eq 'false') { $po['nillable'] = $false }
|
||||||
|
$pdir = $pp.SelectSingleNode('md:TransferDirection', $nsm)
|
||||||
|
if ($pdir -and $pdir.InnerText -cne 'In') { $po['direction'] = $pdir.InnerText }
|
||||||
|
$psyn = Get-MLValue ($pp.SelectSingleNode('md:Synonym', $nsm))
|
||||||
|
if ($null -ne $psyn -and "$psyn" -cne (Split-CamelWords $parName)) { $po['synonym'] = $psyn }
|
||||||
|
$pcmt = $pp.SelectSingleNode('md:Comment', $nsm)
|
||||||
|
if ($pcmt -and $pcmt.InnerText) { $po['comment'] = $pcmt.InnerText }
|
||||||
|
# Только тип и дефолтное остальное → строчное сокращение.
|
||||||
|
if ($po.Count -eq 1 -and $po.Contains('type')) { $pars[$parName] = $po['type'] } else { $pars[$parName] = $po }
|
||||||
|
}
|
||||||
|
$o['parameters'] = $pars
|
||||||
|
}
|
||||||
|
if ($o.Count -eq 1 -and $o.Contains('returnType')) { $ops[$opName] = $o['returnType'] } else { $ops[$opName] = $o }
|
||||||
|
}
|
||||||
|
$dsl['operations'] = $ops
|
||||||
|
}
|
||||||
|
# HTTPService: шаблоны URL и их методы. Шаблон — {template, methods{}}, метод — строка (только
|
||||||
|
# HTTP-метод, когда обработчик совпадает с авто-выводом ИмяШаблона+ИмяМетода) либо объект.
|
||||||
|
$tmplNodes = @($childObjs.SelectNodes('md:URLTemplate', $nsm))
|
||||||
|
if ($tmplNodes.Count -gt 0) {
|
||||||
|
$tmpls = [ordered]@{}
|
||||||
|
foreach ($t in $tmplNodes) {
|
||||||
|
$tp = $t.SelectSingleNode('md:Properties', $nsm)
|
||||||
|
$tName = ($tp.SelectSingleNode('md:Name', $nsm)).InnerText
|
||||||
|
$tObj = [ordered]@{}
|
||||||
|
$tTemplate = $tp.SelectSingleNode('md:Template', $nsm)
|
||||||
|
if ($tTemplate) { $tObj['template'] = $tTemplate.InnerText }
|
||||||
|
$tSyn = Get-MLValue ($tp.SelectSingleNode('md:Synonym', $nsm))
|
||||||
|
# -cne, не -ne: сравнение синонима с авто-выводом ДОЛЖНО быть регистрочувствительным,
|
||||||
|
# иначе "Post" против "post" считается совпадением и синоним теряется.
|
||||||
|
if ($null -ne $tSyn -and "$tSyn" -cne (Split-CamelWords $tName)) { $tObj['synonym'] = $tSyn }
|
||||||
|
$tCmt = $tp.SelectSingleNode('md:Comment', $nsm)
|
||||||
|
if ($tCmt -and $tCmt.InnerText) { $tObj['comment'] = $tCmt.InnerText }
|
||||||
|
|
||||||
|
$mNodes = @($t.SelectNodes('md:ChildObjects/md:Method', $nsm))
|
||||||
|
if ($mNodes.Count -gt 0) {
|
||||||
|
$methods = [ordered]@{}
|
||||||
|
foreach ($m in $mNodes) {
|
||||||
|
$mp = $m.SelectSingleNode('md:Properties', $nsm)
|
||||||
|
$mName = ($mp.SelectSingleNode('md:Name', $nsm)).InnerText
|
||||||
|
$mHttp = $mp.SelectSingleNode('md:HTTPMethod', $nsm)
|
||||||
|
$mHandler = $mp.SelectSingleNode('md:Handler', $nsm)
|
||||||
|
$mSyn = Get-MLValue ($mp.SelectSingleNode('md:Synonym', $nsm))
|
||||||
|
$mCmt = $mp.SelectSingleNode('md:Comment', $nsm)
|
||||||
|
$httpVal = if ($mHttp) { $mHttp.InnerText } else { 'GET' }
|
||||||
|
$handlerVal = if ($mHandler) { $mHandler.InnerText } else { '' }
|
||||||
|
$synDefault = ($null -eq $mSyn) -or ("$mSyn" -ceq (Split-CamelWords $mName))
|
||||||
|
$cmtEmpty = (-not $mCmt) -or (-not $mCmt.InnerText)
|
||||||
|
if ($handlerVal -ceq "$tName$mName" -and $synDefault -and $cmtEmpty) {
|
||||||
|
$methods[$mName] = $httpVal
|
||||||
|
} else {
|
||||||
|
$mo = [ordered]@{ httpMethod = $httpVal }
|
||||||
|
if ($handlerVal) { $mo['handler'] = $handlerVal }
|
||||||
|
if (-not $synDefault) { $mo['synonym'] = $mSyn }
|
||||||
|
if (-not $cmtEmpty) { $mo['comment'] = $mCmt.InnerText }
|
||||||
|
$methods[$mName] = $mo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$tObj['methods'] = $methods
|
||||||
|
}
|
||||||
|
$tmpls[$tName] = $tObj
|
||||||
|
}
|
||||||
|
$dsl['urlTemplates'] = $tmpls
|
||||||
|
}
|
||||||
$attrs = @($childObjs.SelectNodes('md:Attribute', $nsm))
|
$attrs = @($childObjs.SelectNodes('md:Attribute', $nsm))
|
||||||
if ($attrs.Count -gt 0) {
|
if ($attrs.Count -gt 0) {
|
||||||
$arr = [System.Collections.ArrayList]@()
|
$arr = [System.Collections.ArrayList]@()
|
||||||
@@ -1370,7 +1519,12 @@ if (Test-Path -LiteralPath $predefPath) {
|
|||||||
|
|
||||||
# Компактная строка для плоских: без узла Type (Catalog) ИЛИ с непустым типом → "(Код) Имя [Наим]: Тип".
|
# Компактная строка для плоских: без узла Type (Catalog) ИЛИ с непустым типом → "(Код) Имя [Наим]: Тип".
|
||||||
# Пустой <Type/> в короткую не влезает (нужен явный маркер) → object-форма с type:''.
|
# Пустой <Type/> в короткую не влезает (нужен явный маркер) → object-форма с type:''.
|
||||||
if (-not $isFolder -and $kids.Count -eq 0 -and ($null -eq $typeStr -or $typeStr -ne '')) {
|
# Сокращение неоднозначно, если значение содержит собственные разделители грамматики
|
||||||
|
# "(Код) Имя [Наим]: Тип": ')' или ':' в коде, пробел/скобка/':' в имени, скобки в наименовании.
|
||||||
|
# Компилятор разбирает код как [^)]*, а имя как \S+ — на "114 (108)" разбор рассыпается,
|
||||||
|
# и элемент терял и имя, и код (6 элементов в БП и столько же в ERP).
|
||||||
|
$ambiguous = ($code -match '[):]') -or ($name -match '[\s:\[\]()]') -or ($desc -match '[\[\]]')
|
||||||
|
if (-not $isFolder -and $kids.Count -eq 0 -and ($null -eq $typeStr -or $typeStr -ne '') -and -not $ambiguous) {
|
||||||
$s = if ($code) { "($code) $name" } else { $name }
|
$s = if ($code) { "($code) $name" } else { $name }
|
||||||
if ($desc -eq '') { $s = "$s []" }
|
if ($desc -eq '') { $s = "$s []" }
|
||||||
elseif ($desc -cne $auto) { $s = "$s [$desc]" }
|
elseif ($desc -cne $auto) { $s = "$s [$desc]" }
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# meta-decompile v0.55 — XML объекта метаданных 1С → JSON-черновик формата meta-compile
|
# meta-decompile v0.64 — XML объекта метаданных 1С → JSON-черновик формата meta-compile
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
#
|
#
|
||||||
# Зеркало meta-decompile.ps1 (КАНОН). Структура 1:1 — те же имена функций, порядок, комментарии.
|
# Зеркало meta-decompile.ps1 (КАНОН). Структура 1:1 — те же имена функций, порядок, комментарии.
|
||||||
@@ -20,6 +20,28 @@ from lxml import etree
|
|||||||
sys.stdout.reconfigure(encoding="utf-8")
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
sys.stderr.reconfigure(encoding="utf-8")
|
||||||
|
|
||||||
|
# Регистронезависимый ввод — паритет с 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)
|
||||||
|
|
||||||
|
|
||||||
# --- Namespaces (зеркало XmlNamespaceManager) ---
|
# --- Namespaces (зеркало XmlNamespaceManager) ---
|
||||||
NS_MD = "http://v8.1c.ru/8.3/MDClasses"
|
NS_MD = "http://v8.1c.ru/8.3/MDClasses"
|
||||||
NS_V8 = "http://v8.1c.ru/8.1/data/core"
|
NS_V8 = "http://v8.1c.ru/8.1/data/core"
|
||||||
@@ -58,6 +80,14 @@ def _text(node):
|
|||||||
return ''.join(node.itertext())
|
return ''.join(node.itertext())
|
||||||
|
|
||||||
|
|
||||||
|
def _ns_of_prefix(node, prefix):
|
||||||
|
"""URI по префиксу — зеркало GetNamespaceOfPrefix из PS. lxml отдаёт nsmap с учётом
|
||||||
|
унаследованных объявлений, поэтому локальный xmlns:dNpM на самом теге тоже виден."""
|
||||||
|
if node is None:
|
||||||
|
return None
|
||||||
|
return node.nsmap.get(prefix)
|
||||||
|
|
||||||
|
|
||||||
def _attr(node, name, ns=None):
|
def _attr(node, name, ns=None):
|
||||||
"""GetAttribute(name[, ns]) — .NET возвращает '' для отсутствующего атрибута, lxml → None."""
|
"""GetAttribute(name[, ns]) — .NET возвращает '' для отсутствующего атрибута, lxml → None."""
|
||||||
if node is None:
|
if node is None:
|
||||||
@@ -290,7 +320,30 @@ def get_type_shorthand(type_node):
|
|||||||
|
|
||||||
|
|
||||||
# Скалярное значение параметра выбора (<Value xsi:type=...>) → JSON-значение (bool/число/строка).
|
# Скалярное значение параметра выбора (<Value xsi:type=...>) → JSON-значение (bool/число/строка).
|
||||||
|
def get_xdto_type_value(node):
|
||||||
|
"""XDTO-тип: префиксное значение (d6p1:Local) -> нотация Кларка "{uri}Local".
|
||||||
|
|
||||||
|
Префиксы платформы (dNpM) произвольны и переносу не подлежат; стандартные (xs:, v8:, xr:)
|
||||||
|
оставляем дословно — компилятор их так и пишет.
|
||||||
|
"""
|
||||||
|
if node is None:
|
||||||
|
return None
|
||||||
|
txt = _text(node)
|
||||||
|
m = re.match(r'^([\w.\-]+):(.+)$', txt or '')
|
||||||
|
if m:
|
||||||
|
prefix, local = m.group(1), m.group(2)
|
||||||
|
if prefix not in ('xs', 'xsi', 'v8', 'xr'):
|
||||||
|
uri = _ns_of_prefix(node, prefix)
|
||||||
|
if uri:
|
||||||
|
return '{%s}%s' % (uri, local)
|
||||||
|
return txt
|
||||||
|
|
||||||
|
|
||||||
def convert_ch_scalar_node(vN):
|
def convert_ch_scalar_node(vN):
|
||||||
|
# nil-элемент массива (<v8:Value xsi:nil="true"/>) -> JSON null. Без этого он приезжал пустой
|
||||||
|
# строкой и компилятор эмитил xs:string вместо nil.
|
||||||
|
if _attr(vN, 'nil', NS_XSI) == 'true':
|
||||||
|
return None
|
||||||
xt = _attr(vN, 'type', NS_XSI)
|
xt = _attr(vN, 'type', NS_XSI)
|
||||||
txt = _text(vN)
|
txt = _text(vN)
|
||||||
if re.search(r'boolean$', xt, re.I):
|
if re.search(r'boolean$', xt, re.I):
|
||||||
@@ -299,6 +352,10 @@ def convert_ch_scalar_node(vN):
|
|||||||
if re.match(r'^-?\d+$', txt):
|
if re.match(r'^-?\d+$', txt):
|
||||||
return int(txt)
|
return int(txt)
|
||||||
return float(txt)
|
return float(txt)
|
||||||
|
# Пустой DesignTimeRef != пустая строка: без маркера тип терялся, и компилятор эмитил xs:string.
|
||||||
|
# Та же конвенция, что у fillValue — маркер emptyRef.
|
||||||
|
if re.search(r'DesignTimeRef$', xt, re.I) and txt == '':
|
||||||
|
return {'emptyRef': True}
|
||||||
return txt
|
return txt
|
||||||
|
|
||||||
|
|
||||||
@@ -682,7 +739,11 @@ def predef_item_to_dsl(item_el):
|
|||||||
auto = split_camel_words(name)
|
auto = split_camel_words(name)
|
||||||
|
|
||||||
# Компактная строка для плоских: без узла Type (Catalog) ИЛИ с непустым типом → "(Код) Имя [Наим]: Тип".
|
# Компактная строка для плоских: без узла Type (Catalog) ИЛИ с непустым типом → "(Код) Имя [Наим]: Тип".
|
||||||
if (not is_folder) and len(kids) == 0 and (type_str is None or type_str != ''):
|
# Сокращение неоднозначно, если значение содержит собственные разделители грамматики:
|
||||||
|
# ')' или ':' в коде, пробел/скобка/':' в имени, скобки в наименовании. Компилятор читает код
|
||||||
|
# как [^)]*, а имя как \S+ — на "114 (108)" разбор рассыпается и элемент терял имя и код.
|
||||||
|
ambiguous = bool(re.search(r'[):]', code or '')) or bool(re.search(r'[\s:\[\]()]', name or '')) or bool(re.search(r'[\[\]]', desc or ''))
|
||||||
|
if (not is_folder) and len(kids) == 0 and (type_str is None or type_str != '') and not ambiguous:
|
||||||
s = ("(%s) %s" % (code, name)) if code else name
|
s = ("(%s) %s" % (code, name)) if code else name
|
||||||
if desc == '':
|
if desc == '':
|
||||||
s = s + " []"
|
s = s + " []"
|
||||||
@@ -1183,6 +1244,28 @@ def build_dsl():
|
|||||||
get_picture_to_dsl(props, dsl)
|
get_picture_to_dsl(props, dsl)
|
||||||
add_enum_prop('category', 'Category', 'NavigationPanel')
|
add_enum_prop('category', 'Category', 'NavigationPanel')
|
||||||
# CommonCommand.
|
# CommonCommand.
|
||||||
|
# WebService — пространство имён, состав XDTO-пакетов, дескриптор.
|
||||||
|
if obj_type == 'WebService':
|
||||||
|
ns_ = P('Namespace')
|
||||||
|
if ns_:
|
||||||
|
dsl['namespace'] = ns_
|
||||||
|
pkg_nodes = _nodes(props, 'md:XDTOPackages/xr:Item/xr:Value')
|
||||||
|
if len(pkg_nodes) > 0:
|
||||||
|
dsl['xdtoPackages'] = [_text(pn) for pn in pkg_nodes]
|
||||||
|
dfn = P('DescriptorFileName')
|
||||||
|
if dfn and dfn != f'{obj_name}.1cws':
|
||||||
|
dsl['descriptorFileName'] = dfn
|
||||||
|
add_enum_prop('reuseSessions', 'ReuseSessions', 'DontUse')
|
||||||
|
add_int_prop('sessionMaxAge', 'SessionMaxAge', 20)
|
||||||
|
# HTTPService — корневой URL, повторное использование сеансов, время жизни сеанса.
|
||||||
|
if obj_type == 'HTTPService':
|
||||||
|
ru = P('RootURL')
|
||||||
|
# Сравнение регистрочувствительное: реальный RootURL часто отличается от дефолта
|
||||||
|
# только регистром (MobileAppReceiptScanner).
|
||||||
|
if ru and ru != obj_name.lower():
|
||||||
|
dsl['rootURL'] = ru
|
||||||
|
add_enum_prop('reuseSessions', 'ReuseSessions', 'DontUse')
|
||||||
|
add_int_prop('sessionMaxAge', 'SessionMaxAge', 20)
|
||||||
if obj_type == 'CommonCommand':
|
if obj_type == 'CommonCommand':
|
||||||
grp = P('Group')
|
grp = P('Group')
|
||||||
if grp:
|
if grp:
|
||||||
@@ -1458,9 +1541,13 @@ def build_dsl():
|
|||||||
'filterField': shorten_char_field(gt('TypesFilterField', ct), t_from),
|
'filterField': shorten_char_field(gt('TypesFilterField', ct), t_from),
|
||||||
'filterValue': None if tfv_nil == 'true' else convert_ch_scalar_node(tfv_node),
|
'filterValue': None if tfv_nil == 'true' else convert_ch_scalar_node(tfv_node),
|
||||||
}
|
}
|
||||||
dpf = giv('DataPathField', ct)
|
# DataPathField полиморфно: обычно -1, но встречается ПУТЬ к полю (8 случаев на
|
||||||
if dpf != -1:
|
# корпус). Жёсткое int() на нём роняло декомпиляцию всего объекта.
|
||||||
types['dataPathField'] = dpf
|
dpf_node = _lx1(ct, "*[local-name()='DataPathField']")
|
||||||
|
dpf_txt = _text(dpf_node) if dpf_node is not None else ''
|
||||||
|
if dpf_txt != '' and dpf_txt != '-1':
|
||||||
|
types['dataPathField'] = (int(dpf_txt) if re.fullmatch(r'-?\d+', dpf_txt)
|
||||||
|
else shorten_char_field(dpf_txt, t_from))
|
||||||
mvu = giv('MultipleValuesUseField', ct)
|
mvu = giv('MultipleValuesUseField', ct)
|
||||||
if mvu != -1:
|
if mvu != -1:
|
||||||
types['multipleValuesUseField'] = mvu
|
types['multipleValuesUseField'] = mvu
|
||||||
@@ -1610,6 +1697,100 @@ def build_dsl():
|
|||||||
# --- ChildObjects: Attributes + TabularSections ---
|
# --- ChildObjects: Attributes + TabularSections ---
|
||||||
child_objs = _single(obj_node, 'md:ChildObjects')
|
child_objs = _single(obj_node, 'md:ChildObjects')
|
||||||
if child_objs is not None:
|
if child_objs is not None:
|
||||||
|
# WebService: операции с параметрами. Строчное сокращение — только тип возврата.
|
||||||
|
op_nodes = _nodes(child_objs, 'md:Operation')
|
||||||
|
if len(op_nodes) > 0:
|
||||||
|
ops = {}
|
||||||
|
for op in op_nodes:
|
||||||
|
op_p = _single(op, 'md:Properties')
|
||||||
|
op_name = _text(_single(op_p, 'md:Name'))
|
||||||
|
o = {}
|
||||||
|
rt = get_xdto_type_value(_single(op_p, 'md:XDTOReturningValueType'))
|
||||||
|
if rt and rt != 'xs:string':
|
||||||
|
o['returnType'] = rt
|
||||||
|
if _text(_single(op_p, 'md:Nillable')) == 'true':
|
||||||
|
o['nillable'] = True
|
||||||
|
if _text(_single(op_p, 'md:Transactioned')) == 'true':
|
||||||
|
o['transactioned'] = True
|
||||||
|
pn = _text(_single(op_p, 'md:ProcedureName'))
|
||||||
|
if pn and pn != op_name:
|
||||||
|
o['procedureName'] = pn
|
||||||
|
dl = _text(_single(op_p, 'md:DataLockControlMode'))
|
||||||
|
if dl and dl != 'Managed':
|
||||||
|
o['dataLockControlMode'] = dl
|
||||||
|
osyn = get_ml_value(_single(op_p, 'md:Synonym'))
|
||||||
|
if osyn is not None and str(osyn) != split_camel_words(op_name):
|
||||||
|
o['synonym'] = osyn
|
||||||
|
ocmt = _text(_single(op_p, 'md:Comment'))
|
||||||
|
if ocmt:
|
||||||
|
o['comment'] = ocmt
|
||||||
|
par_nodes = _nodes(op, 'md:ChildObjects/md:Parameter')
|
||||||
|
if len(par_nodes) > 0:
|
||||||
|
pars = {}
|
||||||
|
for par in par_nodes:
|
||||||
|
pp = _single(par, 'md:Properties')
|
||||||
|
par_name = _text(_single(pp, 'md:Name'))
|
||||||
|
po = {}
|
||||||
|
pt = get_xdto_type_value(_single(pp, 'md:XDTOValueType'))
|
||||||
|
if pt:
|
||||||
|
po['type'] = pt
|
||||||
|
if _text(_single(pp, 'md:Nillable')) == 'false':
|
||||||
|
po['nillable'] = False
|
||||||
|
pdir = _text(_single(pp, 'md:TransferDirection'))
|
||||||
|
if pdir and pdir != 'In':
|
||||||
|
po['direction'] = pdir
|
||||||
|
psyn = get_ml_value(_single(pp, 'md:Synonym'))
|
||||||
|
if psyn is not None and str(psyn) != split_camel_words(par_name):
|
||||||
|
po['synonym'] = psyn
|
||||||
|
pcmt = _text(_single(pp, 'md:Comment'))
|
||||||
|
if pcmt:
|
||||||
|
po['comment'] = pcmt
|
||||||
|
pars[par_name] = po['type'] if list(po.keys()) == ['type'] else po
|
||||||
|
o['parameters'] = pars
|
||||||
|
ops[op_name] = o['returnType'] if list(o.keys()) == ['returnType'] else o
|
||||||
|
dsl['operations'] = ops
|
||||||
|
# HTTPService: шаблоны URL и их методы.
|
||||||
|
tmpl_nodes = _nodes(child_objs, 'md:URLTemplate')
|
||||||
|
if len(tmpl_nodes) > 0:
|
||||||
|
tmpls = {}
|
||||||
|
for t in tmpl_nodes:
|
||||||
|
tp = _single(t, 'md:Properties')
|
||||||
|
t_name = _text(_single(tp, 'md:Name'))
|
||||||
|
t_obj = {}
|
||||||
|
t_template = _text(_single(tp, 'md:Template'))
|
||||||
|
if t_template:
|
||||||
|
t_obj['template'] = t_template
|
||||||
|
t_syn = get_ml_value(_single(tp, 'md:Synonym'))
|
||||||
|
if t_syn is not None and str(t_syn) != split_camel_words(t_name):
|
||||||
|
t_obj['synonym'] = t_syn
|
||||||
|
t_cmt = _text(_single(tp, 'md:Comment'))
|
||||||
|
if t_cmt:
|
||||||
|
t_obj['comment'] = t_cmt
|
||||||
|
m_nodes = _nodes(t, 'md:ChildObjects/md:Method')
|
||||||
|
if len(m_nodes) > 0:
|
||||||
|
methods = {}
|
||||||
|
for m in m_nodes:
|
||||||
|
mp = _single(m, 'md:Properties')
|
||||||
|
m_name = _text(_single(mp, 'md:Name'))
|
||||||
|
http_val = _text(_single(mp, 'md:HTTPMethod')) or 'GET'
|
||||||
|
handler_val = _text(_single(mp, 'md:Handler')) or ''
|
||||||
|
m_syn = get_ml_value(_single(mp, 'md:Synonym'))
|
||||||
|
m_cmt = _text(_single(mp, 'md:Comment'))
|
||||||
|
syn_default = m_syn is None or str(m_syn) == split_camel_words(m_name)
|
||||||
|
if handler_val == f'{t_name}{m_name}' and syn_default and not m_cmt:
|
||||||
|
methods[m_name] = http_val
|
||||||
|
else:
|
||||||
|
mo = {'httpMethod': http_val}
|
||||||
|
if handler_val:
|
||||||
|
mo['handler'] = handler_val
|
||||||
|
if not syn_default:
|
||||||
|
mo['synonym'] = m_syn
|
||||||
|
if m_cmt:
|
||||||
|
mo['comment'] = m_cmt
|
||||||
|
methods[m_name] = mo
|
||||||
|
t_obj['methods'] = methods
|
||||||
|
tmpls[t_name] = t_obj
|
||||||
|
dsl['urlTemplates'] = tmpls
|
||||||
attrs = _nodes(child_objs, 'md:Attribute')
|
attrs = _nodes(child_objs, 'md:Attribute')
|
||||||
if len(attrs) > 0:
|
if len(attrs) > 0:
|
||||||
arr = []
|
arr = []
|
||||||
@@ -1930,7 +2111,7 @@ SUPPORTED_TYPES = (
|
|||||||
'Enum', 'Report', 'DataProcessor', 'Constant', 'DefinedType', 'FunctionalOption', 'DocumentJournal', 'Sequence',
|
'Enum', 'Report', 'DataProcessor', 'Constant', 'DefinedType', 'FunctionalOption', 'DocumentJournal', 'Sequence',
|
||||||
'FilterCriterion', 'DocumentNumerator', 'SettingsStorage', 'CommonModule', 'EventSubscription', 'ScheduledJob',
|
'FilterCriterion', 'DocumentNumerator', 'SettingsStorage', 'CommonModule', 'EventSubscription', 'ScheduledJob',
|
||||||
'CommonForm', 'SessionParameter', 'CommonCommand', 'CommandGroup', 'CommonAttribute', 'FunctionalOptionsParameter',
|
'CommonForm', 'SessionParameter', 'CommonCommand', 'CommandGroup', 'CommonAttribute', 'FunctionalOptionsParameter',
|
||||||
'WSReference', 'CommonPicture', 'CommonTemplate',
|
'WSReference', 'CommonPicture', 'CommonTemplate', 'HTTPService', 'WebService',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -1940,7 +2121,7 @@ def main():
|
|||||||
parser = argparse.ArgumentParser(description='Decompile 1C metadata object XML to JSON DSL (draft)', allow_abbrev=False)
|
parser = argparse.ArgumentParser(description='Decompile 1C metadata object XML to JSON DSL (draft)', allow_abbrev=False)
|
||||||
parser.add_argument('-ObjectPath', '-Path', dest='ObjectPath', type=str, required=True)
|
parser.add_argument('-ObjectPath', '-Path', dest='ObjectPath', type=str, required=True)
|
||||||
parser.add_argument('-OutputPath', dest='OutputPath', type=str, default=None)
|
parser.add_argument('-OutputPath', dest='OutputPath', type=str, default=None)
|
||||||
args = parser.parse_args()
|
args = ci_parse_args(parser)
|
||||||
|
|
||||||
object_path = args.ObjectPath
|
object_path = args.ObjectPath
|
||||||
if not os.path.exists(object_path):
|
if not os.path.exists(object_path):
|
||||||
|
|||||||
@@ -132,8 +132,7 @@ JSON — строки и/или объекты (для групп с вложе
|
|||||||
|
|
||||||
**Свойства** задавайте по имени свойства 1С (PascalCase, как в конфигураторе): `Indexing`, `FillChecking`,
|
**Свойства** задавайте по имени свойства 1С (PascalCase, как в конфигураторе): `Indexing`, `FillChecking`,
|
||||||
`Use`, `FullTextSearch`, `DataHistory`, `PasswordMode`, `MultiLine`, `Mask`, `CreateOnInput`, `QuickChoice` и др.
|
`Use`, `FullTextSearch`, `DataHistory`, `PasswordMode`, `MultiLine`, `Mask`, `CreateOnInput`, `QuickChoice` и др.
|
||||||
Свойство можно задать, даже если у реквизита оно ещё не выставлено. Опечатка в имени свойства → ошибка
|
Свойство можно задать, даже если у реквизита оно ещё не выставлено.
|
||||||
(правка не теряется молча).
|
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
-Operation modify-attribute -Value "СтароеИмя: name=НовоеИмя, type=Строка(500)"
|
-Operation modify-attribute -Value "СтароеИмя: name=НовоеИмя, type=Строка(500)"
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
```
|
```
|
||||||
|
|
||||||
Свойство можно задать, даже если оно ещё не выставлено у объекта (например `FullTextSearch`, `DataHistory`).
|
Свойство можно задать, даже если оно ещё не выставлено у объекта (например `FullTextSearch`, `DataHistory`).
|
||||||
Опечатка в имени свойства → ошибка (правка не теряется молча). Допустимы имена свойств соответствующего типа объекта.
|
Допустимы имена свойств соответствующего типа объекта.
|
||||||
|
|
||||||
### Type — тип значения (Константа, ПВХ)
|
### Type — тип значения (Константа, ПВХ)
|
||||||
|
|
||||||
@@ -21,8 +21,7 @@
|
|||||||
```powershell
|
```powershell
|
||||||
-Operation modify-property -Value "Type=String(100) + Number(15,2) + CatalogRef.Номенклатура"
|
-Operation modify-property -Value "Type=String(100) + Number(15,2) + CatalogRef.Номенклатура"
|
||||||
```
|
```
|
||||||
Структурные свойства (со вложенными узлами) в скалярный текст не превращаются: попытка задать
|
Структурные свойства (со вложенными узлами) через `Ключ=Значение` не задаются — исключение только `Type`.
|
||||||
такое свойство обычным `Ключ=Значение` (кроме `Type`) завершается ошибкой до записи файла.
|
|
||||||
|
|
||||||
## Свойства-списки
|
## Свойства-списки
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user