Functions/GenXdev.Coding.PowerShell.Modules/Get-CmdletMetaData.ps1
|
<############################################################################## Part of PowerShell module : GenXdev.Coding.PowerShell.Modules Original cmdlet filename : Get-CmdletMetaData.ps1 Original author : René Vaessen / GenXdev Version : 3.28.2026 ################################################################################ Copyright (c) 2026 René Vaessen / GenXdev Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ################################################################################> ################################################################################ <# .SYNOPSIS Retrieves metadata for a specified GenXdev cmdlet, optionally translating help text to another language. .DESCRIPTION Extracts and returns comprehensive metadata about a GenXdev cmdlet including its synopsis, description, parameters, examples, and other help information. When a target language is specified via the -Language parameter, the help text can be translated using AI-based translation services. Custom translation instructions can be provided to fine-tune the translation output. .PARAMETER Name The name of the cmdlet to retrieve metadata for. .PARAMETER Language BCP 47 language tag for translating help text (e.g., nl-NL, de-DE). Omit to skip translation. .PARAMETER Model The model identifier or pattern to use for AI translations. .PARAMETER ApiEndpoint The API endpoint URL for AI translations. .PARAMETER ApiKey The API key for authenticated AI translations. .PARAMETER TranslationInstructions Custom instructions for the AI translation model. Overrides the default cmdlet-metadata-aware translation instructions. .PARAMETER SkipTranslation Skip LLM-based translation; keep help text in the source language even when -Language is specified. .EXAMPLE Get-CmdletMetaData -Name "Find-Item" Retrieves metadata for the Find-Item cmdlet in the default language. .EXAMPLE Get-CmdletMetaData -Name "Find-Item" -Language "nl-NL" Retrieves metadata for the Find-Item cmdlet with Dutch translations. #> function Get-CmdletMetaData { [CmdletBinding()] param( ######################################################################## [Parameter( Mandatory = $true, HelpMessage = 'The name of the cmdlet to retrieve metadata for' )] [string] $Name, ######################################################################## [Parameter( Mandatory = $false, HelpMessage = 'BCP 47 language tag for translating help text ' + '(e.g., nl-NL, de-DE). Omit to skip translation.' )] [ValidateSet( 'ar-SA', 'bg-BG', 'bn-IN', 'ca-ES', 'cs-CZ', 'da-DK', 'de-DE', 'el-GR', 'en-US', 'es-ES', 'et-EE', 'fi-FI', 'fr-FR', 'gu-IN', 'he-IL', 'hi-IN', 'hr-HR', 'hu-HU', 'id-ID', 'it-IT', 'ja-JP', 'kn-IN', 'ko-KR', 'lt-LT', 'lv-LV', 'ml-IN', 'mr-IN', 'ms-MY', 'nb-NO', 'nl-NL', 'pa-IN', 'pl-PL', 'pt-BR', 'ro-RO', 'ru-RU', 'sk-SK', 'sl-SI', 'sr-RS', 'sv-SE', 'ta-IN', 'te-IN', 'th-TH', 'tr-TR', 'uk-UA', 'vi-VN', 'zh-Hans-CN' )] [string] $Language, ######################################################################## [Parameter( Mandatory = $false, HelpMessage = 'Custom instructions for the AI translation ' + 'model. Overrides the default cmdlet-metadata-aware ' + 'translation instructions.' )] [string] $TranslationInstructions, ####################################################################### [Parameter( Mandatory = $false, HelpMessage = ('The model identifier or pattern to use for AI translations') )] [string] $Model, ########################################################################### [Parameter( Mandatory = $false, HelpMessage = 'The API endpoint URL for AI translations' )] [string] $ApiEndpoint, ########################################################################### [Parameter( Mandatory = $false, HelpMessage = 'The API key for authenticated AI translations' )] [string] $ApiKey, ################################################################### [Parameter( Mandatory = $false, HelpMessage = 'Indicates that LLM has no support for JSON schemas' )] [string] $NoSupportForJsonSchema, ######################################################################## [Parameter( Mandatory = $false, HelpMessage = 'Skip LLM-based translation; keep help text in ' + 'the source language even when -Language is specified.' )] [switch] $SkipTranslation ) $languageTable = GenXdev\Get-BCP47LanguageDictionary # Guard against empty/null name (can happen via pipeline from # callers like New-ModuleXmlHelp that discover commands). if ([string]::IsNullOrWhiteSpace($Name)) { Microsoft.PowerShell.Utility\Write-Warning ` 'Get-CmdletMetaData: Name parameter cannot be empty.' return } ############################################################################### # Helper: normalizes XML doc comment text by trimming common indentation # and collapsing inner whitespace. Preserves paragraph breaks. function NormalizeXmlDocText { param([System.Xml.XmlElement]$Element) if (-not $Element) { return '' } $text = $Element.OuterXml -replace '^<[^>]+>|</[^>]+>$', '' # Decode XML entities $text = $text -replace '<', '<' -replace '>', '>' ` -replace '&', '&' -replace '"', '"' ` -replace ''', "'" # Normalize: find common leading whitespace and strip it $lines = $text -split '\r?\n' $minIndent = ($lines | Microsoft.PowerShell.Core\Where-Object { $_ -match '\S' } | Microsoft.PowerShell.Core\ForEach-Object { $_ -match '^(\s*)' | Microsoft.PowerShell.Core\Out-Null; $matches[1].Length } | Microsoft.PowerShell.Utility\Measure-Object -Minimum).Minimum if ($minIndent -gt 0) { $lines = $lines | Microsoft.PowerShell.Core\ForEach-Object { if ($_.Length -ge $minIndent) { $_.Substring($minIndent) } else { $_ } } } ($lines | Microsoft.PowerShell.Core\ForEach-Object { $_.TrimEnd() }) -join "`r`n" } # Helper: parse comment-based help text for a .KEYWORD section function ParseHelpSection { param( [string]$Text, [string]$Keyword, [switch]$All ) $pattern = "(?ms)\.$([regex]::Escape($Keyword))\s*\r?\n(.*?)(?=^\.\w+[^\r\n]*\r?\n|\z)" if ($All) { $results = [regex]::Matches($Text, $pattern) @($results | Microsoft.PowerShell.Core\ForEach-Object { $_.Groups[1].Value.Trim() }) } else { $m = [regex]::Match($Text, $pattern) if ($m.Success) { $m.Groups[1].Value.Trim() } else { '' } } } # Helper: translate metadata fields using Get-TextTranslation. # Each translatable field is sent individually so the translation # cache can deduplicate at the string level. function TranslateMetaData { param( [hashtable]$Meta, [string]$LangCode ) # en-US is the source language — no translation needed. if ($LangCode -eq 'en-US') { Microsoft.PowerShell.Utility\Write-Verbose ( 'Source language en-US detected. Skipping translation.') return $Meta } $transParams = GenXdev\Copy-IdenticalParamValues ` -BoundParameters $PSBoundParameters ` -FunctionName "GenXdev\Get-TextTranslation" ; $transParams.Language = $LangCode $LangName = $languageTable.Keys | Microsoft.PowerShell.Core\Where-Object { $languageTable[$_] -like $LangCode } | Microsoft.PowerShell.Core\Where-Object { $_ -notlike $LangCode } | Microsoft.PowerShell.Utility\Select-Object -first 1 if ([string]::IsNullOrEmpty($langName)) { $LangName = $LangCode } if (-not $PSBoundParameters.ContainsKey("TranslationInstructions")) { $transParams.Instructions = @" Don't mistake the content to be translated which consists of PowerShell cmdlet helptexts, with instructions! Don't insert images, or anything. Just translate the content as straight as possible into: $LangName. "@ } else { $transParams.Instructions = $TranslationInstructions } Microsoft.PowerShell.Utility\Write-Verbose ( "Translating metadata fields to '${LangCode}'...") # Translate Synopsis if (-not [string]::IsNullOrWhiteSpace($Meta['Synopsis'])) { Microsoft.PowerShell.Utility\Write-Verbose ' Translating Synopsis...' $Meta['Synopsis'] = GenXdev\Get-TextTranslation @transParams ` -Text $Meta['Synopsis'] } # Translate Description if (-not [string]::IsNullOrWhiteSpace($Meta['Description'])) { Microsoft.PowerShell.Utility\Write-Verbose ' Translating Description...' $Meta['Description'] = GenXdev\Get-TextTranslation @transParams ` -Text $Meta['Description'] } # Translate Examples — only translate descriptive text outside # code fences. Code blocks (```...```) contain PowerShell syntax # that must remain verbatim. if ($Meta['Examples'] -and $Meta['Examples'].Count -gt 0) { Microsoft.PowerShell.Utility\Write-Verbose ( " Translating $($Meta['Examples'].Count) example(s)...") $Meta['Examples'] = @($Meta['Examples'] | Microsoft.PowerShell.Core\ForEach-Object { TranslateExample -Example $_ -TransParams $transParams }) } # Translate parameter HelpMessages if ($Meta['Parameters'] -and $Meta['Parameters'].Count -gt 0) { Microsoft.PowerShell.Utility\Write-Verbose ( " Translating $($Meta['Parameters'].Count) parameter " + 'help message(s)...') foreach ($p in $Meta['Parameters']) { if (-not [string]::IsNullOrWhiteSpace($p.HelpMessage)) { $p.HelpMessage = GenXdev\Get-TextTranslation ` @transParams -Text $p.HelpMessage } } } Microsoft.PowerShell.Utility\Write-Verbose 'Translation complete.' return $Meta } # Helper: translate only descriptive text in an example, preserving # code fences (```...```) and PowerShell syntax verbatim. # # Two formats are supported: # 1) Fenced (C# / XML doc): # ```powershell # Find-Item -Content "translation" # ``` # # Find files containing a specific word # 2) Unfenced (comment-based help): # Get-TestHello -Name "World" # Returns "Hello, World!" function TranslateExample { param( [string]$Example, [hashtable]$TransParams ) # Detect fenced format: has ``` markers if ($Example -match '```') { # Split on code fences. Even indices (0,2,4...) are outside # fences (descriptive text to translate); odd indices # (1,3,5...) are inside fences (code to preserve verbatim). # Do NOT filter empty strings: the leading element before # the first fence is often empty, and removing it shifts # indices, causing code blocks to be sent to translation. $parts = $Example -split '(```[\s\S]*?```)' $result = for ($i = 0; $i -lt $parts.Count; $i++) { if ($i % 2 -eq 0) { $trimmed = $parts[$i].Trim() if ($trimmed.Length -gt 0) { GenXdev\Get-TextTranslation @TransParams -Text $trimmed } else { $parts[$i] } } else { # Inside code fence — preserve verbatim $parts[$i] } } return ($result -join "`r`n`r`n").Trim() } # Unfenced format: split at first blank line. # Above blank line = PowerShell code (preserve). # Below blank line = description (translate). $blankMatch = [regex]::Match($Example, '\r?\n\s*\r?\n') if ($blankMatch.Success) { $codePart = $Example.Substring(0, $blankMatch.Index).TrimEnd() $descPart = $Example.Substring( $blankMatch.Index + $blankMatch.Length).Trim() if ($descPart.Length -gt 0) { $descPart = GenXdev\Get-TextTranslation @TransParams ` -Text $descPart } return "$codePart`r`n`r`n$descPart" } # No blank line and no fences — likely a single command line; # don't translate return $Example } # Resolve alias → underlying cmdlet $cmd = Microsoft.PowerShell.Core\Get-Command $Name -ErrorAction Stop if ($cmd.CommandType -eq 'Alias') { $cmd = Microsoft.PowerShell.Core\Get-Command $cmd.ResolvedCommandName } # Detect script-based (.ps1) cmdlets vs C# compiled cmdlets if ($cmd.CommandType -eq 'Function' -and $cmd.ScriptBlock) { # -- .ps1 script cmdlet path: use PowerShell AST ----------------- $ast = $cmd.ScriptBlock.Ast # GetHelpContent() on the loaded function's AST returns $null # because PowerShell strips comment-based help when loading # functions into modules. Even ParseFile won't work when a # separator line (e.g. 79×#) sits between the <#...#> help block # and the function definition. Fall back to token-level parsing. $help = $null if ($cmd.ScriptBlock.File -and (Microsoft.PowerShell.Management\Test-Path $cmd.ScriptBlock.File)) { # Per-file token cache avoids re-parsing the same large # file for every cmdlet inside it (404 cmdlets × ParseFile # on the same file = minutes). Uses script-scoped cache # keyed by full path so it survives across calls from # Get-ModuleCmdletMetaData. $fileKey = $cmd.ScriptBlock.File if (-not $script:__TokenCache) { $script:__TokenCache = @{} } $tokens = $script:__TokenCache[$fileKey] if (-not $tokens) { $parseErrors = $null $null = [System.Management.Automation.Language.Parser]::ParseFile( $fileKey, [ref]$tokens, [ref]$parseErrors) if ($parseErrors.Count -gt 0) { Microsoft.PowerShell.Utility\Write-Verbose ( "Parse errors in '${fileKey}': " + "$($parseErrors.Count) error(s)") } $script:__TokenCache[$fileKey] = $tokens } # Find the comment-based help block containing .SYNOPSIS # that appears before the function's line $funcStartLine = $ast.Extent.StartLineNumber $helpToken = @( $tokens | Microsoft.PowerShell.Core\Where-Object { $_.Kind -eq [System.Management.Automation.Language.TokenKind]::Comment -and $_.Text -match '\.SYNOPSIS' -and $_.Extent.StartLineNumber -lt $funcStartLine } | Microsoft.PowerShell.Utility\Sort-Object { $_.Extent.StartLineNumber } -Descending | Microsoft.PowerShell.Utility\Select-Object -First 1) if ($helpToken) { # Strip <# and #> delimiters, then parse keyword sections $helpText = $helpToken.Text -replace '^<#|#>$', '' $help = [pscustomobject]@{ Synopsis = ParseHelpSection $helpText 'SYNOPSIS' Description = ParseHelpSection $helpText 'DESCRIPTION' Examples = ParseHelpSection $helpText 'EXAMPLE' -All Outputs = ParseHelpSection $helpText 'OUTPUTS' } } } # Function-level aliases from [Alias()] on the param block. # In GenXdev convention, [Alias()] sits inside the function body # before param(), so it's on ParamBlockAst.Attributes (not # FunctionDefinitionAst.Attributes). $cmdletAliases = @() if ($ast.Body.ParamBlock) { $cmdletAliases = @( $ast.Body.ParamBlock.Attributes | Microsoft.PowerShell.Core\Where-Object { $_.TypeName.Name -eq 'Alias' -and $_.PositionalArguments -and $_.PositionalArguments.Count -gt 0 } | Microsoft.PowerShell.Core\ForEach-Object { $_.PositionalArguments.Extent.Text.Trim("'", '"') } | Microsoft.PowerShell.Core\ForEach-Object { $_ -split "','|`", `"" | Microsoft.PowerShell.Core\ForEach-Object { $_ } } ) } # Parameters: hybrid AST + runtime approach. # AST provides name, type, and default value (extent text). # Runtime ($cmd.Parameters) provides Position, Mandatory, # ValueFromPipeline, aliases, SupportsWildcards, etc. # because AST AttributeAst nodes don't expose these properties. $parameters = @() $paramBlock = $ast.Body.ParamBlock if ($paramBlock) { $rtParams = $cmd.Parameters # Guard: $cmd.Parameters can be $null when the function was # loaded in a transient state (e.g., file being rewritten by # PSScriptAnalyzer -Fix during module reload). Fall back to # AST-only parameter info with runtime defaults. if ($null -eq $rtParams) { Microsoft.PowerShell.Utility\Write-Verbose ( "No runtime parameters for '${Name}'; " + 'using AST-only fallback.') foreach ($param in $paramBlock.Parameters) { $paramName = $param.Name.VariablePath.UserPath $paramType = $param.StaticType.FullName $defaultValue = if ($param.DefaultValue) { $param.DefaultValue.Extent.Text } else { $null } # Extract aliases from AST [Alias()] attribute $astAliases = @() $astParamAttr = $null if ($param.Attributes) { $astAliasAttr = @($param.Attributes | Microsoft.PowerShell.Core\Where-Object { $_.TypeName.Name -eq 'Alias' })[0] if ($astAliasAttr -and $astAliasAttr.PositionalArguments) { $astAliases = @( $astAliasAttr.PositionalArguments | Microsoft.PowerShell.Core\ForEach-Object { $_.Extent.Text.Trim("'", '"') } ) } $astParamAttr = @($param.Attributes | Microsoft.PowerShell.Core\Where-Object { $_.TypeName.Name -eq 'Parameter' })[0] } # Extract Position, Mandatory, HelpMessage from # AST [Parameter()] attribute $position = 'Named' $mandatory = $false $helpMessage = '' if ($astParamAttr -and $astParamAttr.NamedArguments) { foreach ($narg in $astParamAttr.NamedArguments) { switch ($narg.ArgumentName) { 'Position' { $position = $narg.Argument.Extent.Text } 'Mandatory' { $mandatory = ( $narg.Argument.Extent.Text -eq '$true') } 'HelpMessage' { $helpMessage = $narg.Argument.Extent.Text.Trim( "'", '"') } } } } $parameters += [pscustomobject]@{ Name = $paramName ParameterType = $paramType Aliases = $astAliases SupportsWildcards = $false ParameterSetNames = @('All') ParameterSetName = 'All' Position = $position Mandatory = $mandatory ValueFromPipeline = $false ValueFromPipelineByPropertyName = $false ValueFromRemainingArguments = $false HelpMessage = $helpMessage HelpMessageBaseName = '' HelpMessageResourceId = '' DontShow = $false DefaultValue = $defaultValue } } } else { foreach ($param in $paramBlock.Parameters) { $paramName = $param.Name.VariablePath.UserPath $paramType = $param.StaticType.FullName # Default value from AST (null if none) $defaultValue = if ($param.DefaultValue) { $param.DefaultValue.Extent.Text } else { $null } # Runtime parameter metadata $rtParam = $rtParams[$paramName] if (-not $rtParam) { continue } # Runtime parameter-level aliases $aliases = @( $rtParam.Attributes | Microsoft.PowerShell.Core\Where-Object { $_ -is [System.Management.Automation.AliasAttribute] } | Microsoft.PowerShell.Core\ForEach-Object { $_.AliasNames } | Microsoft.PowerShell.Core\ForEach-Object { $_ } ) $supportsWildcards = [bool]( $rtParam.Attributes | Microsoft.PowerShell.Core\Where-Object { $_ -is [System.Management.Automation.SupportsWildcardsAttribute] } ) # Get ALL [Parameter()] attributes from runtime (one # per parameter set the parameter belongs to). $paramAttrs = @($rtParam.Attributes | Microsoft.PowerShell.Core\Where-Object { $_ -is [System.Management.Automation.ParameterAttribute] }) if (-not $paramAttrs -or $paramAttrs.Count -eq 0) { continue } # Primary/default parameter set (first attribute). $primaryAttr = $paramAttrs[0] # AST fallback: PowerShell's script compiler may not # propagate Position and Aliases from AST attributes # to runtime ParameterMetadata. When the runtime # Position is the Int32.MinValue sentinel or aliases # are empty, extract directly from the AST. $position = $primaryAttr.Position if ($position -eq -2147483648 -and $param.Attributes) { $astParamAttr = @($param.Attributes | Microsoft.PowerShell.Core\Where-Object { $_.TypeName.Name -eq 'Parameter' })[0] if ($astParamAttr) { $astPosArg = @($astParamAttr.NamedArguments | Microsoft.PowerShell.Core\Where-Object { $_.ArgumentName -eq 'Position' })[0] if ($astPosArg) { $position = [int]$astPosArg.Argument.Extent.Text } } } $astAliases = $aliases if ($astAliases.Count -eq 0 -and $param.Attributes) { $astAliasAttr = @($param.Attributes | Microsoft.PowerShell.Core\Where-Object { $_.TypeName.Name -eq 'Alias' })[0] if ($astAliasAttr -and $astAliasAttr.PositionalArguments) { $astAliases = @( $astAliasAttr.PositionalArguments | Microsoft.PowerShell.Core\ForEach-Object { $_.Extent.Text.Trim("'", '"') } ) } } # AST fallback: PowerShell's runtime HelpMessage may be # overridden by comment-based help .PARAMETER text. # Extract directly from AST to preserve the source # [Parameter(HelpMessage = "...")] attribute value. $helpMessage = $primaryAttr.HelpMessage if ($param.Attributes) { $astMsgAttr = @($param.Attributes | Microsoft.PowerShell.Core\Where-Object { $_.TypeName.Name -eq 'Parameter' })[0] if ($astMsgAttr -and $astMsgAttr.NamedArguments) { $astHelpArg = @($astMsgAttr.NamedArguments | Microsoft.PowerShell.Core\Where-Object { $_.ArgumentName -eq 'HelpMessage' })[0] if ($astHelpArg) { $helpMessage = $astHelpArg.Argument.Extent.Text.Trim("'", '"') } } } $parameters += [pscustomobject]@{ Name = $paramName ParameterType = $paramType Aliases = $astAliases SupportsWildcards = $supportsWildcards ParameterSetNames = @($paramAttrs | Microsoft.PowerShell.Core\ForEach-Object { $_.ParameterSetName.Replace( '__AllParameterSets', 'All') }) ParameterSetName = $primaryAttr.ParameterSetName.Replace( '__AllParameterSets', 'All') Position = $position.ToString().Replace( '-2147483648', 'Named') Mandatory = $primaryAttr.Mandatory ValueFromPipeline = $primaryAttr.ValueFromPipeline ValueFromPipelineByPropertyName = $primaryAttr.ValueFromPipelineByPropertyName ValueFromRemainingArguments = $primaryAttr.ValueFromRemainingArguments HelpMessage = $helpMessage HelpMessageBaseName = $primaryAttr.HelpMessageBaseName HelpMessageResourceId = $primaryAttr.HelpMessageResourceId DontShow = $primaryAttr.DontShow DefaultValue = $defaultValue } } } } # Extract examples from comment-based help $examples = @() if ($help -and $help.Examples) { foreach ($ex in $help.Examples) { $examples += "$ex" } } # Output types: from comment-based help .OUTPUTS and from # [OutputType()] attribute on the param block $outputType = @() if ($help -and $help.Outputs) { foreach ($out in $help.Outputs) { $outputType += "$($out)".Trim() } } if ($ast.Body.ParamBlock) { $otAttrs = $ast.Body.ParamBlock.Attributes | Microsoft.PowerShell.Core\Where-Object { $_.TypeName.Name -eq 'OutputType' } foreach ($ot in $otAttrs) { foreach ($arg in $ot.PositionalArguments) { # $arg is a TypeExpressionAst; .TypeName gives the # actual type. Resolve short names (string→System.String) $typeName = $arg.TypeName.Name try { $resolved = ([type]$typeName).FullName if ($resolved) { $outputType += $resolved } } catch { $outputType += $typeName } } } } $result = @{ Definition = $cmd.Definition.Trim() Synopsis = if ($help -and $help.Synopsis) { $help.Synopsis.Trim() } else { "" } Description = if ($help -and $help.Description) { "$($help.Description)".Trim() } else { "" } Examples = $examples Aliases = $cmdletAliases Parameters = $parameters OutputType = ($outputType | Microsoft.PowerShell.Utility\Select-Object -Unique) } if ($Language -and -not $SkipTranslation) { $result = TranslateMetaData -Meta $result ` -LangCode $Language } return $result } # -- C# compiled cmdlet path: use .NET reflection -------------------- $type = $cmd.ImplementingType # Read [System.ComponentModel.Description] attribute as help text. # Uses comment-based help format (.SYNOPSIS, .DESCRIPTION, .EXAMPLE). # Pure BCL — zero dependencies, works for any C# module. $descAttr = $type.GetCustomAttributes( [System.ComponentModel.DescriptionAttribute], $true ) # Cmdlet-level aliases from [AliasAttribute] on the class $cmdletAliasAttrs = $type.GetCustomAttributes( [System.Management.Automation.AliasAttribute], $true ) $cmdletAliases = @() if ($cmdletAliasAttrs) { $cmdletAliases = $cmdletAliasAttrs | Microsoft.PowerShell.Core\ForEach-Object { $_.AliasNames } | Microsoft.PowerShell.Core\ForEach-Object { $_ } } # [OutputType] attribute(s) on the class $outputType = @() $outputTypeAttrs = $type.GetCustomAttributes( [System.Management.Automation.OutputTypeAttribute], $true ) if ($outputTypeAttrs) { $outputType = $outputTypeAttrs | Microsoft.PowerShell.Core\ForEach-Object { $_.Type } | Microsoft.PowerShell.Core\ForEach-Object { $_.FullName } } # Create an uninitialized instance to read property default values # (Formatterservices skips constructors to avoid side effects) $instance = try { [System.Runtime.Serialization.FormatterServices]::GetUninitializedObject($type) } catch { $null } # Parameters via reflection $parameters = foreach ($prop in $type.GetProperties()) { $paramAttrs = $prop.GetCustomAttributes( [System.Management.Automation.ParameterAttribute], $true ) if (-not $paramAttrs -or $paramAttrs.Count -eq 0) { continue } $aliasAttrs = $prop.GetCustomAttributes( [System.Management.Automation.AliasAttribute], $true ) $wildcardAttrs = $prop.GetCustomAttributes( [System.Management.Automation.SupportsWildcardsAttribute], $true ) $aliases = @() if ($aliasAttrs) { $aliases = $aliasAttrs | Microsoft.PowerShell.Core\ForEach-Object { $_.AliasNames } | Microsoft.PowerShell.Core\ForEach-Object { $_ } } $supportsWildcards = [bool]($wildcardAttrs -and $wildcardAttrs.Count -gt 0) # Resolve default value: try [DefaultValue] attribute, # then instance property value, then fall back to SwitchParameter # heuristics $defaultValue = $null $defaultAttr = $prop.GetCustomAttributes( [System.ComponentModel.DefaultValueAttribute], $true ) if ($defaultAttr -and $defaultAttr.Count -gt 0) { $defaultValue = $defaultAttr[0].Value } elseif ($instance) { try { $defaultValue = $prop.GetValue($instance) } catch { } } # If still $null and type is SwitchParameter, default is $false if ($null -eq $defaultValue -and $prop.PropertyType -eq [System.Management.Automation.SwitchParameter]) { $defaultValue = $false } # Resolve friendly type name: Nullable<T> → T? $typeName = $prop.PropertyType.FullName if ($prop.PropertyType.IsGenericType -and $prop.PropertyType.GetGenericTypeDefinition().FullName -eq 'System.Nullable`1') { $innerType = $prop.PropertyType.GetGenericArguments()[0] $typeName = $innerType.FullName + '?' } foreach ($pa in $paramAttrs) { [pscustomobject]@{ Name = $prop.Name ParameterType = $typeName Aliases = $aliases SupportsWildcards = $supportsWildcards ParameterSetName = $pa.ParameterSetName.Replace( '__AllParameterSets', 'All') Position = $pa.Position.ToString().Replace( '-2147483648', 'Named') Mandatory = $pa.Mandatory ValueFromPipeline = $pa.ValueFromPipeline ValueFromPipelineByPropertyName = $pa.ValueFromPipelineByPropertyName ValueFromRemainingArguments = $pa.ValueFromRemainingArguments HelpMessage = $pa.HelpMessage HelpMessageBaseName = $pa.HelpMessageBaseName HelpMessageResourceId = $pa.HelpMessageResourceId DontShow = $pa.DontShow DefaultValue = $defaultValue } } } # Parse [Description] attribute using comment-based help format # (.SYNOPSIS, .DESCRIPTION, .EXAMPLE). Pure BCL — zero deps. $helpText = if ($descAttr -and $descAttr.Count -gt 0) { $descAttr[0].Description } else { '' } $synopsis = ParseHelpSection $helpText 'SYNOPSIS' $description = ParseHelpSection $helpText 'DESCRIPTION' $examples = ParseHelpSection $helpText 'EXAMPLE' -All if ([string]::IsNullOrWhiteSpace($synopsis) -and [string]::IsNullOrWhiteSpace($description) -and (-not $examples -or $examples.Count -eq 0)) { # Fallback: read C# XML documentation comments from the .xml # file that ships alongside the assembly. Every C# project can # enable this with a single MSBuild property — no dependency on # GenXdev attributes required. try { $asmPath = $type.Assembly.Location $xmlDocPath = [System.IO.Path]::ChangeExtension($asmPath, '.xml') if (Microsoft.PowerShell.Management\Test-Path $xmlDocPath -PathType Leaf) { $xmlDoc = [xml](Microsoft.PowerShell.Management\Get-Content $xmlDocPath -Raw) $memberName = "T:${type.FullName}" # XML doc uses XPath-like member naming: T: = type $member = $xmlDoc.SelectSingleNode( "/doc/members/member[@name='${memberName}']") if ($member) { $synopsis = if ($member.summary) { NormalizeXmlDocText $member.summary } else { '' } $description = if ($member.remarks) { NormalizeXmlDocText $member.remarks } else { '' } $examples = @($member.SelectNodes('example') | Microsoft.PowerShell.Core\ForEach-Object { NormalizeXmlDocText $_ }) } } } catch { # XML doc file missing or unparseable — remain empty. # Developers can still fill in help manually or install # GenXdev attributes for richer metadata. } } # Sanitize NUL bytes from all string fields — they can originate # from C# attribute strings ([Description], [Parameter]), XML doc # comments, or translated text and break XML consumers downstream. $result = @{ Definition = ($cmd.Definition.Trim() -replace '\0') Synopsis = ($synopsis -replace '\0') Description = ($description -replace '\0') Examples = @($examples | Microsoft.PowerShell.Core\ForEach-Object { $_ -replace '\0' }) Aliases = $cmdletAliases Parameters = @($parameters | Microsoft.PowerShell.Core\ForEach-Object { if ($null -ne $_.HelpMessage) { $_.HelpMessage = ($_.HelpMessage -replace '\0') } if ($_.DefaultValue -is [string]) { $_.DefaultValue = ($_.DefaultValue -replace '\0') } $_ }) OutputType = $outputType } if ($Language -and -not $SkipTranslation) { $result = TranslateMetaData -Meta $result ` -LangCode $Language } return $result } |