Files
cc-1c-skills/.github/skills/subsystem-info/scripts/subsystem-info.ps1
T
2026-05-17 11:22:33 +00:00

515 lines
18 KiB
PowerShell

# subsystem-info v1.0 — Compact summary of 1C subsystem structure
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory=$true)][Alias('Path')][string]$SubsystemPath,
[ValidateSet("overview","content","ci","tree","full")]
[string]$Mode = "overview",
[string]$Name,
[int]$Limit = 150,
[int]$Offset = 0,
[string]$OutFile
)
$ErrorActionPreference = 'Stop'
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# --- Output helper ---
$script:lines = @()
function Out([string]$text) { $script:lines += $text }
# --- Resolve path ---
if (-not [System.IO.Path]::IsPathRooted($SubsystemPath)) {
$SubsystemPath = Join-Path (Get-Location).Path $SubsystemPath
}
# --- Helper: get LocalString text ---
function Get-MLText($node) {
if (-not $node -or -not $node.HasChildNodes) { return "" }
foreach ($item in $node.ChildNodes) {
if ($item.NodeType -ne 'Element') { continue }
$lang = ""; $content = ""
foreach ($c in $item.ChildNodes) {
if ($c.NodeType -ne 'Element') { continue }
if ($c.LocalName -eq "lang") { $lang = $c.InnerText }
if ($c.LocalName -eq "content") { $content = $c.InnerText }
}
if ($lang -eq "ru" -and $content) { return $content }
}
# fallback: first item
foreach ($item in $node.ChildNodes) {
if ($item.NodeType -ne 'Element') { continue }
foreach ($c in $item.ChildNodes) {
if ($c.NodeType -ne 'Element') { continue }
if ($c.LocalName -eq "content" -and $c.InnerText) { return $c.InnerText }
}
}
return ""
}
# --- Helper: load subsystem XML ---
function Load-SubsystemXml([string]$xmlPath) {
[xml]$doc = Get-Content -Path $xmlPath -Encoding UTF8
$ns = New-Object System.Xml.XmlNamespaceManager($doc.NameTable)
$ns.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
$ns.AddNamespace("v8", "http://v8.1c.ru/8.1/data/core")
$ns.AddNamespace("xr", "http://v8.1c.ru/8.3/xcf/readable")
$ns.AddNamespace("xsi", "http://www.w3.org/2001/XMLSchema-instance")
$sub = $doc.SelectSingleNode("/md:MetaDataObject/md:Subsystem", $ns)
if (-not $sub) {
Write-Host "[ERROR] Not a valid subsystem XML: $xmlPath"
exit 1
}
return @{ Doc=$doc; Ns=$ns; Sub=$sub }
}
# --- Helper: get content items ---
function Get-ContentItems($props, $ns) {
$items = @()
$contentNode = $props.SelectSingleNode("md:Content", $ns)
if (-not $contentNode -or -not $contentNode.HasChildNodes) { return $items }
foreach ($item in $contentNode.SelectNodes("xr:Item", $ns)) {
$items += $item.InnerText
}
return $items
}
# --- Helper: get child subsystem names ---
function Get-ChildNames($sub, $ns) {
$names = @()
$co = $sub.SelectSingleNode("md:ChildObjects", $ns)
if (-not $co -or -not $co.HasChildNodes) { return $names }
foreach ($child in $co.ChildNodes) {
if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "Subsystem") {
$names += $child.InnerText
}
}
return $names
}
# --- Helper: group content by type ---
function Group-ContentByType($items) {
$groups = [ordered]@{}
foreach ($item in $items) {
if ($item -match '^([^.]+)\.(.+)$') {
$type = $Matches[1]
$name = $Matches[2]
} elseif ($item -match '^[0-9a-fA-F]{8}-') {
$type = "[UUID]"
$name = $item
} else {
$type = "[Other]"
$name = $item
}
if (-not $groups.Contains($type)) { $groups[$type] = @() }
$groups[$type] += $name
}
return $groups
}
# --- Helper: find subsystem dir from XML path ---
function Get-SubsystemDir([string]$xmlPath) {
$dir = [System.IO.Path]::GetDirectoryName($xmlPath)
$baseName = [System.IO.Path]::GetFileNameWithoutExtension($xmlPath)
return Join-Path $dir $baseName
}
# --- Show functions for full mode ---
function Show-Overview {
Out "Подсистема: $subName"
if ($synonym -and $synonym -ne $subName) { Out "Синоним: $synonym" }
if ($commentText) { Out "Комментарий: $commentText" }
Out "ВключатьВКомандныйИнтерфейс: $inclCI"
Out "ИспользоватьОднуКоманду: $useOneCmd"
if ($explanation) { Out "Пояснение: $explanation" }
if ($picText) { Out "Картинка: $picText" }
if ($contentItems.Count -gt 0) {
$parts = @()
foreach ($type in $groups.Keys) {
$parts += "$type`: $($groups[$type].Count)"
}
Out "Состав: $($contentItems.Count) объектов ($($parts -join ', '))"
} else {
Out "Состав: пусто"
}
if ($childNames.Count -gt 0) {
Out "Дочерние подсистемы ($($childNames.Count)): $($childNames -join ', ')"
}
if ($hasCI) {
Out "Командный интерфейс: есть"
}
}
function Show-Content {
Out "Состав подсистемы $subName ($($contentItems.Count) объектов):"
Out ""
if ($Name) {
if ($groups.Contains($Name)) {
$filtered = $groups[$Name]
Out "$Name ($($filtered.Count)):"
foreach ($n in $filtered) { Out " $n" }
} else {
Out "[INFO] Тип '$Name' не найден в составе."
Out "Доступные типы: $($groups.Keys -join ', ')"
}
} else {
foreach ($type in $groups.Keys) {
Out "$type ($($groups[$type].Count)):"
foreach ($n in $groups[$type]) { Out " $n" }
Out ""
}
}
}
function Show-CI {
$localSubDir = Get-SubsystemDir $SubsystemPath
$localCiPath = Join-Path (Join-Path $localSubDir "Ext") "CommandInterface.xml"
if (-not (Test-Path $localCiPath)) {
Out "Командный интерфейс: $subName"
Out ""
Out "Файл CommandInterface.xml не найден."
Out "Путь: $localCiPath"
} else {
[xml]$ciDoc = Get-Content -Path $localCiPath -Encoding UTF8
$ciNs = New-Object System.Xml.XmlNamespaceManager($ciDoc.NameTable)
$ciNs.AddNamespace("ci", "http://v8.1c.ru/8.3/xcf/extrnprops")
$ciNs.AddNamespace("xr", "http://v8.1c.ru/8.3/xcf/readable")
$ciRoot = $ciDoc.DocumentElement
Out "Командный интерфейс: $subName"
Out ""
# --- CommandsVisibility ---
$visSection = $ciRoot.SelectSingleNode("ci:CommandsVisibility", $ciNs)
if ($visSection) {
$hidden = @(); $shown = @()
foreach ($cmd in $visSection.SelectNodes("ci:Command", $ciNs)) {
$cmdName = $cmd.GetAttribute("name")
$vis = $cmd.SelectSingleNode("ci:Visibility/xr:Common", $ciNs)
if ($vis -and $vis.InnerText -eq "false") { $hidden += $cmdName }
else { $shown += $cmdName }
}
$total = $hidden.Count + $shown.Count
if (-not $Name -or $Name -eq "visibility") {
Out "Видимость ($total):"
if ($hidden.Count -gt 0) {
Out " СКРЫТО ($($hidden.Count)):"
foreach ($h in $hidden) { Out " $h" }
}
if ($shown.Count -gt 0) {
Out " ПОКАЗАНО ($($shown.Count)):"
foreach ($s in $shown) { Out " $s" }
}
Out ""
}
}
# --- CommandsPlacement ---
$placeSection = $ciRoot.SelectSingleNode("ci:CommandsPlacement", $ciNs)
if ($placeSection) {
$placements = @()
foreach ($cmd in $placeSection.SelectNodes("ci:Command", $ciNs)) {
$cmdName = $cmd.GetAttribute("name")
$grp = $cmd.SelectSingleNode("ci:CommandGroup", $ciNs)
$pl = $cmd.SelectSingleNode("ci:Placement", $ciNs)
$grpText = if ($grp) { $grp.InnerText } else { "?" }
$plText = if ($pl) { $pl.InnerText } else { "?" }
$placements += @{ Name=$cmdName; Group=$grpText; Placement=$plText }
}
if ((-not $Name -or $Name -eq "placement") -and $placements.Count -gt 0) {
Out "Размещение ($($placements.Count)):"
$arrow = [char]0x2192
foreach ($p in $placements) {
Out " $($p.Name) $arrow $($p.Group) ($($p.Placement))"
}
Out ""
}
}
# --- CommandsOrder ---
$orderSection = $ciRoot.SelectSingleNode("ci:CommandsOrder", $ciNs)
if ($orderSection) {
$orderGroups = [ordered]@{}
foreach ($cmd in $orderSection.SelectNodes("ci:Command", $ciNs)) {
$cmdName = $cmd.GetAttribute("name")
$grp = $cmd.SelectSingleNode("ci:CommandGroup", $ciNs)
$grpText = if ($grp) { $grp.InnerText } else { "?" }
if (-not $orderGroups.Contains($grpText)) { $orderGroups[$grpText] = @() }
$orderGroups[$grpText] += $cmdName
}
$totalOrder = 0
foreach ($k in $orderGroups.Keys) { $totalOrder += $orderGroups[$k].Count }
if ((-not $Name -or $Name -eq "order") -and $totalOrder -gt 0) {
Out "Порядок команд ($totalOrder):"
foreach ($grpName in $orderGroups.Keys) {
Out " [$grpName]:"
foreach ($c in $orderGroups[$grpName]) { Out " $c" }
}
Out ""
}
}
# --- SubsystemsOrder ---
$subOrderSection = $ciRoot.SelectSingleNode("ci:SubsystemsOrder", $ciNs)
if ($subOrderSection) {
$subOrder = @()
foreach ($s in $subOrderSection.SelectNodes("ci:Subsystem", $ciNs)) {
$subOrder += $s.InnerText
}
if ((-not $Name -or $Name -eq "subsystems") -and $subOrder.Count -gt 0) {
Out "Порядок подсистем ($($subOrder.Count)):"
for ($i = 0; $i -lt $subOrder.Count; $i++) {
Out " $($i+1). $($subOrder[$i])"
}
Out ""
}
}
# --- GroupsOrder ---
$grpOrderSection = $ciRoot.SelectSingleNode("ci:GroupsOrder", $ciNs)
if ($grpOrderSection) {
$grpOrder = @()
foreach ($g in $grpOrderSection.SelectNodes("ci:Group", $ciNs)) {
$grpOrder += $g.InnerText
}
if ((-not $Name -or $Name -eq "groups") -and $grpOrder.Count -gt 0) {
Out "Порядок групп ($($grpOrder.Count)):"
foreach ($g in $grpOrder) { Out " $g" }
}
}
}
}
# ============================================================
# Mode: tree
# ============================================================
if ($Mode -eq "tree") {
$isDir = Test-Path $SubsystemPath -PathType Container
$rootDir = $null
$rootXml = $null
if ($isDir) {
# Subsystems/ directory — show all top-level subsystems
$rootDir = $SubsystemPath
} else {
# Specific subsystem XML — show tree from this subsystem
if (-not (Test-Path $SubsystemPath)) {
Write-Host "[ERROR] File not found: $SubsystemPath"
exit 1
}
$rootXml = $SubsystemPath
}
# Box-drawing chars (PS 5.1 compatible)
$script:T_BRANCH = [char]0x251C + [char]0x2500 + [char]0x2500 + " " # ├──
$script:T_LAST = [char]0x2514 + [char]0x2500 + [char]0x2500 + " " # └──
$script:T_PIPE = [char]0x2502 + " " # │
$script:T_ARROW = [char]0x2192 # →
function Get-TreeLine([string]$xmlPath) {
$parsed = Load-SubsystemXml $xmlPath
$sub = $parsed.Sub; $ns = $parsed.Ns
$props = $sub.SelectSingleNode("md:Properties", $ns)
$name = $props.SelectSingleNode("md:Name", $ns).InnerText
$markers = @()
$subDir = Get-SubsystemDir $xmlPath
$ciPath = Join-Path (Join-Path $subDir "Ext") "CommandInterface.xml"
if (Test-Path $ciPath) { $markers += "CI" }
$useOne = $props.SelectSingleNode("md:UseOneCommand", $ns)
if ($useOne -and $useOne.InnerText -eq "true") { $markers += "OneCmd" }
$inclCI = $props.SelectSingleNode("md:IncludeInCommandInterface", $ns)
if ($inclCI -and $inclCI.InnerText -eq "false") { $markers += "Скрыт" }
$markerStr = if ($markers.Count -gt 0) { " [$($markers -join ', ')]" } else { "" }
$contentItems = @(Get-ContentItems $props $ns)
$childNames = @(Get-ChildNames $sub $ns)
$childStr = if ($childNames.Count -gt 0) { ", $($childNames.Count) дочерних" } else { "" }
return @{
Label = "$name$markerStr ($($contentItems.Count) объектов$childStr)"
SubDir = $subDir
ChildNames = $childNames
}
}
function Build-TreeEntry([string]$xmlPath, [string]$prefix, [bool]$isLast, [bool]$isRoot) {
$info = Get-TreeLine $xmlPath
$connector = if ($isRoot) { "" } elseif ($isLast) { $script:T_LAST } else { $script:T_BRANCH }
Out "$prefix$connector$($info.Label)"
if ($info.ChildNames.Count -gt 0) {
$childPrefix = if ($isRoot) { "" } elseif ($isLast) { "$prefix " } else { "$prefix$($script:T_PIPE)" }
$subsDir = Join-Path $info.SubDir "Subsystems"
for ($i = 0; $i -lt $info.ChildNames.Count; $i++) {
$childXml = Join-Path $subsDir "$($info.ChildNames[$i]).xml"
$childIsLast = ($i -eq $info.ChildNames.Count - 1)
if (Test-Path $childXml) {
Build-TreeEntry $childXml $childPrefix $childIsLast $false
} else {
$conn2 = if ($childIsLast) { $script:T_LAST } else { $script:T_BRANCH }
Out "$childPrefix$conn2$($info.ChildNames[$i]) [NOT FOUND]"
}
}
}
}
if ($rootDir) {
$label = Split-Path $rootDir -Leaf
Out "Дерево подсистем от: $label/"
Out ""
$xmlFiles = @(Get-ChildItem $rootDir -Filter "*.xml" -File | Sort-Object Name)
if ($Name) {
$xmlFiles = @($xmlFiles | Where-Object { $_.BaseName -eq $Name })
if ($xmlFiles.Count -eq 0) {
Write-Host "[ERROR] Subsystem '$Name' not found in $rootDir"
exit 1
}
}
for ($i = 0; $i -lt $xmlFiles.Count; $i++) {
Build-TreeEntry $xmlFiles[$i].FullName "" ($i -eq $xmlFiles.Count - 1) $true
}
} else {
Build-TreeEntry $rootXml "" $true $true
}
} elseif ($Mode -eq "ci") {
# ============================================================
# Mode: ci — CommandInterface.xml
# ============================================================
if (Test-Path $SubsystemPath -PathType Container) {
Write-Host "[ERROR] ci mode requires a subsystem .xml file, not a directory"
exit 1
}
# File not found — check Dir/Name/Name.xml → Dir/Name.xml
if (-not (Test-Path $SubsystemPath)) {
$fn = [System.IO.Path]::GetFileNameWithoutExtension($SubsystemPath)
$pd = Split-Path $SubsystemPath
if ($fn -eq (Split-Path $pd -Leaf)) {
$c = Join-Path (Split-Path $pd) "$fn.xml"
if (Test-Path $c) { $SubsystemPath = $c }
}
}
if (-not (Test-Path $SubsystemPath)) {
Write-Host "[ERROR] File not found: $SubsystemPath"
exit 1
}
$parsed = Load-SubsystemXml $SubsystemPath
$sub = $parsed.Sub; $ns = $parsed.Ns
$props = $sub.SelectSingleNode("md:Properties", $ns)
$subName = $props.SelectSingleNode("md:Name", $ns).InnerText
Show-CI
} else {
# ============================================================
# Mode: overview / content — requires a subsystem XML file
# ============================================================
if (Test-Path $SubsystemPath -PathType Container) {
$dirName = Split-Path $SubsystemPath -Leaf
$candidate = Join-Path $SubsystemPath "$dirName.xml"
$sibling = Join-Path (Split-Path $SubsystemPath) "$dirName.xml"
if (Test-Path $candidate) {
$SubsystemPath = $candidate
} elseif (Test-Path $sibling) {
$SubsystemPath = $sibling
} else {
Write-Host "[ERROR] No $dirName.xml found in directory. Use -Mode tree for directory listing."
exit 1
}
}
# File not found — check Dir/Name/Name.xml → Dir/Name.xml
if (-not (Test-Path $SubsystemPath)) {
$fn = [System.IO.Path]::GetFileNameWithoutExtension($SubsystemPath)
$pd = Split-Path $SubsystemPath
if ($fn -eq (Split-Path $pd -Leaf)) {
$c = Join-Path (Split-Path $pd) "$fn.xml"
if (Test-Path $c) { $SubsystemPath = $c }
}
}
if (-not (Test-Path $SubsystemPath)) {
Write-Host "[ERROR] File not found: $SubsystemPath"
exit 1
}
$parsed = Load-SubsystemXml $SubsystemPath
$sub = $parsed.Sub; $ns = $parsed.Ns
$props = $sub.SelectSingleNode("md:Properties", $ns)
$subName = $props.SelectSingleNode("md:Name", $ns).InnerText
$synonym = Get-MLText $props.SelectSingleNode("md:Synonym", $ns)
$comment = $props.SelectSingleNode("md:Comment", $ns)
$commentText = if ($comment -and $comment.InnerText) { $comment.InnerText } else { "" }
$inclHelp = $props.SelectSingleNode("md:IncludeHelpInContents", $ns).InnerText
$inclCI = $props.SelectSingleNode("md:IncludeInCommandInterface", $ns).InnerText
$useOneCmd = $props.SelectSingleNode("md:UseOneCommand", $ns).InnerText
$explanation = Get-MLText $props.SelectSingleNode("md:Explanation", $ns)
# Picture
$picNode = $props.SelectSingleNode("md:Picture", $ns)
$picText = ""
if ($picNode -and $picNode.HasChildNodes) {
$picRef = $picNode.SelectSingleNode("xr:Ref", $ns)
if ($picRef -and $picRef.InnerText) { $picText = $picRef.InnerText }
}
# Content
$contentItems = @(Get-ContentItems $props $ns)
$groups = Group-ContentByType $contentItems
# Children
$childNames = @(Get-ChildNames $sub $ns)
# CI presence
$subDir = Get-SubsystemDir $SubsystemPath
$ciPath = Join-Path (Join-Path $subDir "Ext") "CommandInterface.xml"
$hasCI = Test-Path $ciPath
if ($Mode -eq "overview") {
Show-Overview
} elseif ($Mode -eq "content") {
Show-Content
} elseif ($Mode -eq "full") {
Show-Overview
Out ""; Out "--- content ---"; Out ""
Show-Content
Out ""; Out "--- ci ---"; Out ""
Show-CI
}
}
# --- Pagination and output ---
$totalLines = $script:lines.Count
$outLines = $script:lines
if ($Offset -gt 0) {
if ($Offset -ge $totalLines) {
Write-Host "[INFO] Offset $Offset exceeds total lines ($totalLines). Nothing to show."
exit 0
}
$outLines = $outLines[$Offset..($totalLines - 1)]
}
if ($Limit -gt 0 -and $outLines.Count -gt $Limit) {
$shown = $outLines[0..($Limit - 1)]
$remaining = $totalLines - $Offset - $Limit
$shown += ""
$shown += "[ОБРЕЗАНО] Показано $Limit из $totalLines строк. Используйте -Offset $($Offset + $Limit) для продолжения."
$outLines = $shown
}
if ($OutFile) {
if (-not [System.IO.Path]::IsPathRooted($OutFile)) {
$OutFile = Join-Path (Get-Location).Path $OutFile
}
$utf8 = New-Object System.Text.UTF8Encoding($true)
[System.IO.File]::WriteAllLines($OutFile, $outLines, $utf8)
Write-Host "Output written to $OutFile"
} else {
foreach ($l in $outLines) { Write-Host $l }
}