Documentarian.ModuleAuthor.psm1
|
# Copyright (c) Microsoft Corporation. # Licensed under the MIT License. using module .\Documentarian.ModuleAuthor.Private.psm1 #region Enums.Public [Flags()] enum ProviderFlags { Registry = 0x01 Alias = 0x02 Environment = 0x04 FileSystem = 0x08 Function = 0x10 Variable = 0x20 Certificate = 0x40 WSMan = 0x80 } enum ParameterAttributeKind { DontShow Experimental HasValidation SupportsWildcards ValueFromPipeline ValueFromRemaining DefaultValue IsCredential IsObsolete } #endregion Enums.Public #region Classes.Public class ParameterInfo { # The name of the parameter [string] $Name # The parameter's help description. [string] $HelpText # The parameter's full type name. [string] $Type # The comma-separated list of parameter sets the parameter belongs to. [string] $ParameterSet # The comma-separated list of aliases for the parameter. [string] $Aliases # Whether the parameter is mandatory. [bool[]] $Required # The position of the parameter in each parameter set. [string[]] $Position # Whether and how the parameter accepts pipeline input by parameter set. [string[]] $Pipeline # Whether the parameter supports wildcard characters. [bool] $Wildcard # Whether the parameter is a dynamic parameter. [bool] $Dynamic # Whether the parameter accepts values from remaining arguments. [bool[]] $FromRemaining # Whether the parameter should be hidden from the user. [bool[]] $DontShow # The default value of the parameter by parameter set. [string[]] $DefaultValue # Whether the parameter is a credential parameter. [bool[]] $IsCredential # Whether the parameter is obsolete. [psobject] $IsObsolete # The provider flags for the parameter, indicating which providers it's # valid for. [ProviderFlags] $ProviderFlags ParameterInfo( [System.Management.Automation.ParameterMetadata]$param, [ProviderFlags]$ProviderFlags ) { $this.Name = $param.Name $this.HelpText = $param.Attributes.HelpMessage | Select-Object -First 1 if ($this.HelpText.Length -eq 0) { $this.HelpText = '{{Placeholder}}' } $this.Type = $param.ParameterType.FullName $this.ParameterSet = if ($param.Attributes.ParameterSetName -eq '__AllParameterSets') { '(All)' } else { $param.Attributes.ParameterSetName -join ', ' } $this.Aliases = $param.Aliases -join ', ' $this.Required = $param.Attributes.Mandatory $this.Position = if ($param.Attributes.Position -lt 0) { 'Named' } else { $param.Attributes.Position } $this.Pipeline = 'ByValue ({0}), ByName ({1})' -f @( ($param.Attributes.ValueFromPipeline -join ','), ($param.Attributes.ValueFromPipelineByPropertyName -join ',') ) $this.Wildcard = $param.Attributes.TypeId.Name -contains 'SupportsWildcardsAttribute' $this.Dynamic = $param.IsDynamic $this.FromRemaining = $param.Attributes.ValueFromRemainingArguments $this.DontShow = $param.Attributes.DontShow if ($param.Attributes.TypeId.Name -contains 'PSDefaultValueAttribute') { $this.DefaultValue = $param.Attributes.Help -join ', ' } else { $this.DefaultValue = 'None' } if ($param.Attributes.TypeId.Name -contains 'CredentialAttribute') { $this.IsCredential = $true } else { $this.IsCredential = $false } if ($param.Attributes.TypeId.Name -contains 'ObsoleteAttribute') { $this.IsObsolete = [pscustomobject]@{ Message = $param.Attributes.Message IsError = $param.Attributes.IsError } } else { $this.IsObsolete = $false } $this.ProviderFlags = $ProviderFlags } [string]ToMarkdown([bool]$showAll) { <# .SYNOPSIS Converts the parameter info to a Markdown section. .DESCRIPTION Converts the parameter info to a Markdown section as expected by the **PlatyPS** module. It includes an H3 for the parameter (with the leading hyphen), the parameter's help text, and a YAML code block containing the parameter's metadata. When the $showAll parameter is $true, the parameter's non-PlatyPS compliant metadata is included. .PARAMETER showAll Whether to include the parameter's non-PlatyPS compliant metadata. #> $builder = New-Builder $Builder | Add-Heading -Level 3 -Content $this.Name $Builder | Add-Line -Content $this.HelpText $Builder | Start-CodeFence -Language yaml $Builder | Add-Line -Content "Type: $($this.Type)" $Builder | Add-Line -Content "Parameter Sets: $($this.ParameterSet)" $Builder | Add-Line -Content "Aliases: $($this.Aliases)" $Builder | Add-Line $Builder | Add-Line -Content "Required: $($this.Required -join ', ')" $Builder | Add-Line -Content "Position: $($this.Position -join ', ')" if ($this.Type -is [System.Management.Automation.SwitchParameter]) { $Builder | Add-Line -Content 'Default value: False' } else { $Builder | Add-Line -Content "Default value: $($this.DefaultValue)" } $Builder | Add-Line -Content "Accept pipeline input: $($this.Pipeline)" $Builder | Add-Line -Content "Accept wildcard characters: $($this.Wildcard)" if ($showAll) { $Builder | Add-Line -Content "Dynamic: $($this.Dynamic)" if ($this.Dynamic -and $this.ProviderFlags) { $ProviderName = if ($this.ProviderFlags -eq 0xFF) { 'All' } else { $this.ProviderFlags.ToString() } $Builder | Add-Line -Content "Providers: $ProviderName" } $Builder | Add-Line -Content "Values from remaining args: $($this.FromRemaining -join ', ')" $Builder | Add-Line -Content "Do not show: $($this.DontShow -join ', ')" $Builder | Add-Line -Content "Is credential: $($this.IsCredential)" if ($this.IsObsolete -ne $false) { $Builder | Add-Line -Content 'Is obsolete: True' $Builder | Add-Line -Content " - Message: $($this.IsObsolete.Message)" $Builder | Add-Line -Content " - IsError: $($this.IsObsolete.IsError)" } else { $Builder | Add-Line -Content 'Is obsolete: False' } } $Builder | Stop-CodeFence $Builder | Add-Line return $Builder.ToString() } } class CredentialAttributeInfo { [string]$Cmdlet [string]$Parameter [string]$ParameterType [bool]$IsCredential [string]$ParameterSetName [string]$Module [string] ToString () { return @( "$($this.Module)/" "$($this.Cmdlet), " "Parameter: $($this.Parameter) <" "$($this.ParameterType)>" "IsCredential: $($this.IsCredential)" ) -join '' } } class DontShowAttributeInfo { [string] $Cmdlet [string] $Parameter [string] $ParameterType [bool] $DontShow [string] $ParameterSetName [string] $Module [string] ToString () { return @( "$($this.Module)/" "$($this.Cmdlet), " "Parameter: $($this.Parameter) <" "$($this.ParameterType)>, " "DontShow: $($this.DontShow)" ) -join '' } } class ExperimentalAttributeInfo { [string]$Cmdlet [string]$Parameter [string]$ParameterType [string]$ExperimentName [string]$ParameterSetName [string]$Module [string] ToString () { return @( "$($this.Module)/" "$($this.Cmdlet), " "Parameter: $($this.Parameter) <" "$($this.ParameterType)>, " "Experiment: $($this.ExperimentName)" ) -join '' } } class HasValidationAttributeInfo { [string]$Cmdlet [string]$Parameter [string]$ParameterType [string]$ValidationAttribute [string]$ParameterSetName [string]$Module [string] ToString () { return @( "$($this.Module)/" "$($this.Cmdlet), " "Parameter: $($this.Parameter) <" "$($this.ParameterType)>, " "ValidationAttribute: $($this.ValidationAttribute)" ) -join '' } } class ObsoleteAttributeInfo { [string]$Cmdlet [string]$Parameter [string]$ParameterType [bool]$IsObsolete [string]$ParameterSetName [string]$Module [string] ToString () { return @( "$($this.Module)/" "$($this.Cmdlet), " "Parameter: $($this.Parameter) <" "$($this.ParameterType), >" "IsObsolete: $($this.IsObsolete)" ) -join '' } } class PSDefaultValueAttributeInfo { [string]$Cmdlet [string]$Parameter [string]$ParameterType [string]$DefaultValue [string]$ParameterSetName [string]$Module [string] ToString () { return @( "$($this.Module)/" "$($this.Cmdlet), " "Parameter: $($this.Parameter) <" "$($this.ParameterType), >" "DefaultValue: $($this.DefaultValue)" ) -join '' } } class SupportsWildcardsAttributeInfo { [string]$Cmdlet [string]$Parameter [string]$ParameterType [bool]$SupportsWildcards [string]$ParameterSetName [string]$Module [string] ToString () { return @( "$($this.Module)/" "$($this.Cmdlet), " "Parameter: $($this.Parameter) <" "$($this.ParameterType)>" "SupportsWildcards: $($this.SupportsWildcards)" ) -join '' } } class ValueFromPipelineAttributeInfo { [string]$Cmdlet [string]$Parameter [string]$ParameterType [string]$ValueFromPipeline [string]$ParameterSetName [string]$Module [string] ToString () { return @( "$($this.Module)/" "$($this.Cmdlet), " "Parameter: $($this.Parameter) <" "$($this.ParameterType)>, " "ValueFromPipeline: $($this.ValueFromPipeline)" ) -join '' } } class ValueFromRemainingAttributeInfo { [string]$Cmdlet [string]$Parameter [string]$ParameterType [bool]$ValueFromRemaining [string]$ParameterSetName [string]$Module [string] ToString () { return @( "$($this.Module)/" "$($this.Cmdlet), " "Parameter: $($this.Parameter) <" "$($this.ParameterType)>, " "ValueFromRemaining: $($this.ValueFromRemaining)" ) -join '' } } #endregion Classes.Public #region Functions.Private <# .SYNOPSIS Returns a list of parameter headers from a cmdlet markdown file. .DESCRIPTION Returns a list of parameter headers from a cmdlet markdown file. .PARAMETER mdheaders An array of objects returned by `Select-String -Pattern '^#' -Path $file` .NOTES Used by `Update-ParameterOrder` to sort the parameters in a cmdlet markdown file. #> function Get-ParameterMdHeaders { param($mdheaders) $paramlist = @() $inParams = $false foreach ($hdr in $mdheaders) { # Find the start of the parameters section if ($hdr.Line -eq '## Parameters') { $inParams = $true } if ($inParams) { # Find the start of each parameter if ($hdr.Line -match '^### -') { $param = [PSCustomObject]@{ Line = $hdr.Line.Trim() StartLine = $hdr.LineNumber - 1 EndLine = -1 } $paramlist += $param } # Find the end of the last parameter if ((($hdr.Line -match '^## ' -and $hdr.Line -ne '## Parameters') -or ($hdr.Line -eq '### CommonParameters')) -and ($paramlist.Count -gt 0)) { $inParams = $false $paramlist[-1].EndLine = $hdr.LineNumber - 2 } } } # Find the end each last parameter if ($paramlist.Count -gt 0) { for ($x = 0; $x -lt $paramlist.Count; $x++) { if ($paramlist[$x].EndLine -eq -1) { $paramlist[$x].EndLine = $paramlist[($x + 1)].StartLine - 1 } } } $paramlist } <# .SYNOPSIS Wraps text to a specified line length. .DESCRIPTION Wraps text to a specified line length. Useful for formatting descriptions in markdown files. .PARAMETER line The line of text to wrap. .PARAMETER pad The number of spaces to indent wrapped lines. Default is 0. .PARAMETER max The maximum line length. Default is 79. #> function WrapText { param( [string]$line, [uint]$pad = 0, [uint]$max = 79 ) $lines = @() [string[]]$parts = $line -split ' ' [string]$newLine = ' ' * $pad foreach ($p in $parts) { if ($newLine.Length + $p.Length + 1 -lt $max) { $newLine += "$p " } else { $lines += $newLine.TrimEnd(' ') $newLine = ' ' * $pad + "$p " } } if ($newLine.Trim() -ne '') { $lines += $newLine.TrimEnd(' ') } $lines } #endregion Functions.Private #region Functions.Public function Find-ParameterWithAttribute { [CmdletBinding()] [OutputType( [DontShowAttributeInfo], [ExperimentalAttributeInfo], [HasValidationAttributeInfo], [SupportsWildcardsAttributeInfo], [ValueFromPipelineAttributeInfo], [ValueFromRemainingAttributeInfo], [PSDefaultValueAttributeInfo], [CredentialAttributeInfo], [ObsoleteAttributeInfo] )] param( [Parameter(Mandatory, Position = 0)] [ParameterAttributeKind]$AttributeKind, [Parameter(Position = 1)] [SupportsWildcards()] [PSDefaultValue(Help='*', Value='*')] [string[]]$CommandName = '*', [ValidateSet('Cmdlet', 'Module', 'None')] [PSDefaultValue(Help='None', Value='None')] [string]$GroupBy = 'None' ) begin { $cmdlets = Get-Command $CommandName -Type Cmdlet, ExternalScript, Filter, Function, Script } process { foreach ($cmd in $cmdlets) { foreach ($param in $cmd.Parameters.Values) { $result = $null foreach ($attr in $param.Attributes) { if ($attr.TypeId.ToString() -eq 'System.Management.Automation.ParameterAttribute' -and $AttributeKind -in 'DontShow', 'Experimental', 'ValueFromPipeline', 'ValueFromRemaining') { switch ($AttributeKind) { DontShow { if ($attr.DontShow) { $result = [DontShowAttributeInfo]@{ Cmdlet = $cmd.Name Parameter = $param.Name ParameterType = $param.ParameterType.Name DontShow = $attr.DontShow ParameterSetName = $param.ParameterSets.Keys -join ', ' Module = $cmd.Source } } break } Experimental { if ($attr.ExperimentName) { $result = [ExperimentalAttributeInfo]@{ Cmdlet = $cmd.Name Parameter = $param.Name ParameterType = $param.ParameterType.Name DontShow = $attr.ExperimentName ParameterSetName = $param.ParameterSets.Keys -join ', ' Module = $cmd.Source } } break } ValueFromPipeline { if ($attr.ValueFromPipeline -or $attr.ValueFromPipelineByPropertyName) { $result = [ValueFromPipelineAttributeInfo]@{ Cmdlet = $cmd.Name Parameter = $param.Name ParameterType = $param.ParameterType.Name ValueFromPipeline = ('ByValue({0}), ByName({1})' -f $attr.ValueFromPipeline, $attr.ValueFromPipelineByPropertyName) ParameterSetName = $param.ParameterSets.Keys -join ', ' Module = $cmd.Source } } break } ValueFromRemaining { if ($attr.ValueFromRemainingArguments) { $result = [ValueFromRemainingAttributeInfo]@{ Cmdlet = $cmd.Name Parameter = $param.Name ParameterType = $param.ParameterType.Name ValueFromRemaining = $attr.ValueFromRemainingArguments ParameterSetName = $param.ParameterSets.Keys -join ', ' Module = $cmd.Source } } break } } } elseif ($attr.TypeId.ToString() -like 'System.Management.Automation.Validate*Attribute' -and $AttributeKind -eq 'HasValidation') { $result = [HasValidationAttributeInfo]@{ Cmdlet = $cmd.Name Parameter = $param.Name ParameterType = $param.ParameterType.Name ValidationAttribute = $attr.TypeId.ToString().Split('.')[ - 1].Replace('Attribute', '') ParameterSetName = $param.ParameterSets.Keys -join ', ' Module = $cmd.Source } } elseif ($attr.TypeId.ToString() -eq 'System.Management.Automation.SupportsWildcardsAttribute' -and $AttributeKind -eq 'SupportsWildcards') { $result = [SupportsWildcardsAttributeInfo]@{ Cmdlet = $cmd.Name Parameter = $param.Name ParameterType = $param.ParameterType.Name SupportsWildcards = $true ParameterSetName = $param.ParameterSets.Keys -join ', ' Module = $cmd.Source } } elseif ($attr.TypeId.ToString() -eq 'System.Management.Automation.CredentialAttribute' -and $AttributeKind -eq 'IsCredential') { $result = [CredentialAttributeInfo]@{ Cmdlet = $cmd.Name Parameter = $param.Name ParameterType = $param.ParameterType.Name IsCredential = $true ParameterSetName = $param.ParameterSets.Keys -join ', ' Module = $cmd.Source } } elseif ($attr.TypeId.ToString() -eq 'System.Management.Automation.PSDefaultValueAttribute' -and $AttributeKind -eq 'DefaultValue') { $result = [PSDefaultValueAttributeInfo]@{ Cmdlet = $cmd.Name Parameter = $param.Name ParameterType = $param.ParameterType.Name DefaultValue = $attr.Help ParameterSetName = $param.ParameterSets.Keys -join ', ' Module = $cmd.Source } } elseif ($attr.TypeId.ToString() -eq 'System.ObsoleteAttribute' -and $AttributeKind -eq 'IsObsolete') { $result = [ObsoleteAttributeInfo]@{ Cmdlet = $cmd.Name Parameter = $param.Name ParameterType = $param.ParameterType.Name IsObsolete = if ($param.IsObsolete -eq $false) { $false } else { $true } ParameterSetName = $param.ParameterSets.Keys -join ', ' Module = $cmd.Source } } } if ($result) { # Add a type name to the object so that the correct format gets chosen switch ($GroupBy) { 'Cmdlet' { $typename = $result.GetType().Name + '#ByCmdlet' $result.psobject.TypeNames.Insert(0, $typename) break } 'Module' { $typename = $result.GetType().Name + '#ByModule' $result.psobject.TypeNames.Insert(0, $typename) break } } $result } } } } } function Get-ParameterInfo { [CmdletBinding(DefaultParameterSetName = 'AsMarkdown')] [OutputType([ParameterInfo])] [OutputType([System.String])] param( [Parameter(Mandatory, Position = 0, ParameterSetName = 'AsMarkdown')] [Parameter(Mandatory, Position = 0, ParameterSetName = 'AsObject')] [string[]]$ParameterName, [Parameter(Mandatory, Position = 1, ParameterSetName = 'AsMarkdown')] [Parameter(Mandatory, Position = 1, ParameterSetName = 'AsObject')] [string]$CmdletName, [Parameter(ParameterSetName = 'AsMarkdown')] [switch]$ShowAll, [Parameter(Mandatory, ParameterSetName = 'AsObject')] [switch]$AsObject ) $cmdlet = Get-Command -Name $CmdletName -ErrorAction Stop $null = Get-PSDrive # Load all providers $providerList = Get-PSProvider foreach ($pname in $ParameterName) { try { $paraminfo = $null $param = $null foreach ($provider in $providerList) { Push-Location $($provider.Drives[0].Name + ':') $param = $cmdlet.Parameters.Values | Where-Object Name -EQ $pname if ($param) { if ($paraminfo) { $paraminfo.ProviderFlags = $paraminfo.ProviderFlags -bor [ProviderFlags]($provider.Name) } else { $paraminfo = [ParameterInfo]::new( $param, [ProviderFlags]($provider.Name) ) } } Pop-Location } } catch { Write-Error "Cmdlet $CmdletName not found." return } if ($paraminfo) { if ($AsObject) { $paraminfo } else { $paraminfo.ToMarkdown($ShowAll) } } else { Write-Error "Parameter $pname not found." } } } function Get-ShortDescription { $crlf = "`r`n" Get-ChildItem *.md | ForEach-Object { if ($_.directory.basename -ne $_.basename) { $filename = $_.Name $name = $_.BaseName $headers = Select-String -Path $filename -Pattern '^## \w*' -AllMatches $mdtext = Get-Content $filename $start = $headers[0].LineNumber $end = $headers[1].LineNumber - 2 $short = $mdtext[($start)..($end)] -join ' ' if ($short -eq '') { $short = '{{Placeholder}}' } '### [{0}]({1}){3}{2}{3}' -f $name, $filename, $short.Trim(), $crlf } } } function Get-Syntax { [CmdletBinding()] param( [Parameter(Mandatory = $true, Position = 0)] [string] $CmdletName, [switch] $Markdown ) function formatString { param( $cmd, $pstring ) $parts = $pstring -split ' ' $parameters = @() for ($x = 0; $x -lt $parts.Count; $x++) { $p = $parts[$x] if ($x -lt $parts.Count - 1) { if (!$parts[$x + 1].StartsWith('[')) { $p += ' ' + $parts[$x + 1] $x++ } $parameters += , $p } else { $parameters += , $p } } $line = $cmd + ' ' $temp = '' for ($x = 0; $x -lt $parameters.Count; $x++) { if ($line.Length + $parameters[$x].Length + 1 -lt 100) { $line += $parameters[$x] + ' ' } else { $temp += $line + "`r`n" $line = ' ' + $parameters[$x] + ' ' } } $temp + $line.TrimEnd() } try { $cmdlet = Get-Command $cmdletname -ea Stop if ($cmdlet.CommandType -eq 'Alias') { $cmdlet = Get-Command $cmdlet.Definition } if ($cmdlet.CommandType -eq 'ExternalScript') { $name = $CmdletName } else { $name = $cmdlet.Name } $syntax = (Get-Command $name).ParameterSets | Select-Object -Property @( @{Name = 'Cmdlet' ; Expression = { $cmdlet.Name } }, @{Name = 'ParameterSetName' ; Expression = { $_.name } }, 'IsDefault', @{Name = 'Parameters' ; Expression = { $_.ToString() } } ) } catch [System.Management.Automation.CommandNotFoundException] { $_.Exception.Message } $mdHere = @' ### {0}{1} ``` {2} ``` '@ if ($Markdown) { foreach ($s in $syntax) { $string = $s.Cmdlet, $s.Parameters -join ' ' if ($s.IsDefault) { $default = ' (Default)' } else { $default = '' } if ($string.Length -gt 100) { $string = formatString $s.Cmdlet $s.Parameters } $mdHere -f $s.ParameterSetName, $default, $string } } else { $syntax } } function Invoke-NewMDHelp { ### Runs New-MarkdownHelp with the parameters we use most often. param( [Parameter(Mandatory)] [string]$Module, [Parameter(Mandatory)] [string]$OutPath ) $parameters = @{ Module = $Module OutputFolder = $OutPath AlphabeticParamsOrder = $true UseFullTypeName = $true WithModulePage = $true ExcludeDontShow = $false Encoding = [System.Text.Encoding]::UTF8 } New-MarkdownHelp @parameters } function Invoke-Pandoc { [CmdletBinding()] [OutputType([void])] param( [Parameter(Mandatory)] [string[]]$Path, [string]$OutputPath = '.', [switch]$Recurse ) # Initialize recurse parameter if not specified if (-not $PSBoundParameters.ContainsKey('Recurse')) { $Recurse = $false } # Ensure pandoc is available $pandocExe = Get-Command pandoc.exe if ($null -eq $pandocExe) { Write-Error 'pandoc.exe not found in PATH. See https://pandoc.org/installing.html' return } # Define Pandoc template, filter, and arguments $aboutTemplate = @( 'TOPIC' ' $title$' '' 'SYNOPSIS' ' $description$' '' '$body$' ) -join [Environment]::NewLine $luaFilter = @( 'function Header(elem)' ' local text = pandoc.utils.stringify(elem.content)' ' local underline_char = { "=", "=" }' ' local level = elem.level' ' local underline = string.rep(underline_char[level] or "-", #text)' ' return pandoc.Para{pandoc.Str(text), pandoc.LineBreak(), pandoc.Str(underline)}' 'end' ) -join [Environment]::NewLine $templatePath = "$env:TEMP\pandoc-template.txt" $luaFilterPath = "$env:TEMP\pandoc-filter.lua" $pandocArgs = @( '--from=gfm', '--to=plain+multiline_tables-yaml_metadata_block', "--lua-filter=$luaFilterPath", "--template=$templatePath", '--columns=79', '--quiet' ) Write-Verbose "Pandoc: $($pandocExe.Source)" Write-Verbose "Args: $($pandocArgs -join ' ')" # Create Pandoc assets $aboutTemplate | Out-File $templatePath -Encoding utf8 $luaFilter | Out-File $luaFilterPath -Encoding utf8 # Process files $files = Get-ChildItem $Path -Recurse:$Recurse foreach ($f in $files) { $outfile = Join-Path $OutputPath "$($f.BaseName).help.txt" Write-Verbose "Converting $($f.Name) to $outfile" $metadata = Get-Metadata -Path $f if ($null -eq $metadata) { Write-Error "No metadata found in $($f.Name)" continue # Skip to next file } if (-not $metadata.Contains('title') -or $metadata.title -eq '') { Write-Error "Missing metadata.title in $($f.Name)" continue # Skip to next file } if (-not $metadata.Contains('description') -or $metadata.description -eq '') { Write-Error "Missing metadata.description in $($f.Name)" continue # Skip to next file } # Remove H1 from markdown content $markdown = (Get-Content $f -Raw) -replace "# $($metadata.title)\r?\n", '' $markdown | & $pandocExe @pandocArgs | Out-File $outfile -Encoding utf8 } } function Test-HelpInfoUri { [CmdletBinding(DefaultParameterSetName = 'ByName')] param( [Parameter(Mandatory, Position = 0, ParameterSetName = 'ByName')] [string[]]$Module, [Parameter(ValueFromPipeline, ParameterSetName = 'ByObject')] [object]$InputObject, [Parameter(Position = 1)] [uri]$HelpInfoUri, [string]$OutPath ) begin { if ($PSVersionTable.PSVersion.Major -lt 7) { throw 'This function requires PowerShell 7 or higher' } # Used for short-circuiting checks against already-checked modules $TestedModules = @() } process { if ($PSCmdlet.ParameterSetName -eq 'ByObject') { # We can infer the Module from the input object. If it's a # CommandInfo, it might have the module info or name. If it's # a PSModuleInfo, use it. Otherwise, cast to string. foreach ($Object in $InputObject) { switch ($Object) { { $_ -is [System.Management.Automation.CommandInfo] } { if ($Object.Module) { $Module = $Object.Module } elseif ($Object.ModuleName) { $Module = $Object.ModuleName } else { $Module = $Object.ToString() } } { $_ -is [System.Management.Automation.PSModuleInfo] } { $Module = $Object.Name } default { $Module = $Object.ToString() } } } } # Don't bother searching the same module more than # once since we always check latest version $Module = $Module | Select-Object -Unique foreach ($modname in $Module) { # To support pipeline, skip if a module has already # been tested, otherwise add to the test list. if ($modname -in $TestedModules) { continue } else { $TestedModules += $modname } $output = [pscustomobject]@{ pstypename = 'TestHelpInfoUriResult' Module = $modname Code = $null Message = $null } $mod = Get-Module -Name $modname -ListAvailable | Select-Object -First 1 # If the input wasn't a module, return the failed result immediately if ($null -eq $mod) { $output.Message = 'Module not found' $output.Code = 0x2 $output continue } # else we have a module object $output.Module = $mod.Name # The HelpInfoUri parameter overrides $mod.HelpInfoUri, allowing you to test a new URI # before updating the module. If the parameter is empty, $mod.HelpInfoUri. if ( $PSBoundParameters.Keys -notcontains 'HelpInfoUri') { $HelpInfoUri = $mod.HelpInfoUri } # If the input was a module, we have the component parts needed to check it Write-Verbose "$($mod.Name) - $($mod.Guid) - $HelpInfoUri" # If $HelpInfoUri is empty, we know it can't be reached if ($null -eq $HelpInfoUri) { $output.Message = 'HelpInfoUri is null or empty' $output.Code = 0x00002ef8 # ERROR_INTERNET_NO_CONTEXT $output continue } # Resolve the URI from the manifest, which may include redirection. try { # If successful, then the URI probably points to a browseable directory $response = Invoke-WebRequest -Uri $HelpInfoUri -ErrorAction Stop continue } catch { if ($_.Exception.Response.StatusCode.value__ -eq 404) { # If the response is 404, then the URI is probably valid, especially when # hosted in an Azure blobstore. You can't browse to the URI, but you can # download the file using a fully qualified URI to the file. A true 404 problem # will be caught later. $baseUri = $_.TargetObject.RequestUri.AbsoluteUri } else { # If the response is anything other than 404, then the URI is probably invalid $output.Message = $_.Exception.Response.StatusCode $output.Code = $_.Exception.Response.StatusCode.value__ $output continue } } # Construct the URI: the last segment is always <Name>_<Guid>_HelpInfo.xml $HelpInfoUri = [uri]($baseUri, $mod.Name, '_', $mod.Guid, '_HelpInfo.xml' -join '') Write-Verbose "HelpInfoUri: $HelpInfoUri" # Try to get the HelpInfo.xml to determine validity try { $params = @{ Uri = $HelpInfoUri Method = 'Get' ErrorAction = 'Stop' } if ($OutPath) { # Add the OutFile parameter to Invoke-WebRequest if an output path is specified $params.Add( 'OutFile', (Join-Path -Path $OutPath -ChildPath $HelpInfoUri.Segments[-1]) ) } $response = Invoke-WebRequest @params $output.Code = $response.StatusCode $output.Message = 'HelpInfoUri is valid' $output } catch { $output.Code = $_.Exception.Response.StatusCode.value__ $output.Message = $_.Exception.Response.StatusCode $output } } } } function Update-Headings { [CmdletBinding()] param( [Parameter(Mandatory)] [SupportsWildcards()] [string] $Path, [switch] $Recurse ) $headings = @( '## Synopsis' '## Syntax' '## Description' '## Examples' '## Parameters' '### CommonParameters' '## Inputs' '## Outputs' '## Notes' '## Related links' '## Short description' '## Long description' '## See also' ) Get-ChildItem $Path -Recurse:$Recurse | ForEach-Object { $_.name $md = Get-Content -Encoding utf8 -Path $_ foreach ($h in $headings) { $md = $md -replace "^$h$", $h } Set-Content -Encoding utf8 -Value $md -Path $_ -Force } } function Update-ParameterOrder { [CmdletBinding()] [OutputType([System.IO.FileInfo])] param ( [Parameter(Mandatory, Position = 0)] [SupportsWildcards()] [string[]] $Path ) $mdfiles = Get-ChildItem $path foreach ($file in $mdfiles) { $mdtext = Get-Content $file -Encoding utf8 $mdheaders = Select-String -Pattern '^#' -Path $file $unsorted = Get-ParameterMdHeaders $mdheaders if ($unsorted.Count -gt 0) { $sorted = $unsorted | Sort-Object Line $newtext = $mdtext[0..($unsorted[0].StartLine - 1)] $confirmWhatIf = @() foreach ($paramblock in $sorted) { if ( '### -Confirm', '### -WhatIf' -notcontains $paramblock.Line) { $newtext += $mdtext[$paramblock.StartLine..$paramblock.EndLine] } else { $confirmWhatIf += $paramblock } } foreach ($paramblock in $confirmWhatIf) { $newtext += $mdtext[$paramblock.StartLine..$paramblock.EndLine] } $newtext += $mdtext[($unsorted[-1].EndLine + 1)..($mdtext.Count - 1)] Set-Content -Value $newtext -Path $file.FullName -Encoding utf8 -Force $file } } } #endregion Functions.Public # Define the types to export with type accelerators. $ExportableTypes =@( [ProviderFlags] [ParameterAttributeKind] [ParameterInfo] [CredentialAttributeInfo] [DontShowAttributeInfo] [ExperimentalAttributeInfo] [HasValidationAttributeInfo] [ObsoleteAttributeInfo] [PSDefaultValueAttributeInfo] [SupportsWildcardsAttributeInfo] [ValueFromPipelineAttributeInfo] [ValueFromRemainingAttributeInfo] ) # Get the internal TypeAccelerators class to use its static methods. $TypeAcceleratorsClass = [psobject].Assembly.GetType( 'System.Management.Automation.TypeAccelerators' ) # Ensure none of the types would clobber an existing type accelerator. # If a type accelerator with the same name exists, throw an exception. $ExistingTypeAccelerators = $TypeAcceleratorsClass::Get foreach ($Type in $ExportableTypes) { if ($Type -in $ExistingTypeAccelerators.Keys) { $Message = @( "Unable to register type accelerator '$($Type.FullName)'" 'Accelerator already exists' ) -join ' ' throw [System.Management.Automation.ErrorRecord]::new( [System.InvalidOperationException]::new($Message), 'TypeAcceleratorAlreadyExists', [System.Management.Automation.ErrorCategory]::InvalidOperation, $Type.FullName ) } } # Add the type accelerators for every exportable type. foreach ($Type in $ExportableTypes) { Write-Verbose "Registering type accelerator for [$Type]" $TypeAcceleratorsClass::Add($Type.FullName, $Type) } # Remove type accelerators when the module is removed. $MyInvocation.MyCommand.ScriptBlock.Module.OnRemove = { foreach ($Type in $ExportableTypes) { Write-Verbose "Unregistering type accelerator for [$Type]" $null = $TypeAcceleratorsClass::Remove($Type.FullName) } }.GetNewClosure() $ExportableFunctions = @( 'Find-ParameterWithAttribute' 'Get-ParameterInfo' 'Get-ShortDescription' 'Get-Syntax' 'Invoke-NewMDHelp' 'Invoke-Pandoc' 'Test-HelpInfoUri' 'Update-Headings' 'Update-ParameterOrder' ) Export-ModuleMember -Alias * -Function $ExportableFunctions |