HelpParser.psm1
<#
.DESCRIPTION Parses a single line of help data for flag and option parameters. #> function Get-ParsedHelpLineElement { [CmdletBinding()] param ( # The help data to parse. [Parameter(ValueFromPipeline)] [string]$HelpLine, # The regular expression to use to extract options and flags. [Parameter()] [string]$RegEx = "^\s*((-{1,2}|/)[a-zA-Z0-9#]{1}[a-zA-Z0-9\-#\[\]]*)[=\s,]?", # A pre-condition for qualifying the $HelpLine as a line containing # parameter elements. [Parameter()] [string]$PreConditionRegEx = "^\s*(-{1,2}|/)", # An output that indicates how many spaces the parameter was indented by. # This is mainly intended to provide a convinient way to remove indentation # of lines that follow the parameter's first line that provides additional # detail/context. [Parameter()] [ref]$IndentationCount ) process { if (($HelpLine | Select-String $PreConditionRegEx).Matches.Count -eq 0) { return } if ($null -ne $IndentationCount) { $indent = ($HelpLine | Select-String '^\s+') if ($null -eq $indent) { $IndentationCount.Value = 0 } else { $IndentationCount.Value = $indent.Matches[0].Length } } $elems = @($HelpLine.TrimStart().Split().TrimEnd(',; ')) if ($elems.Count -eq 0) { return } $endIndex = $elems.IndexOf('') if ($endIndex -lt 0) { $endIndex = $elems.Count - 1 } $elems[0..$endIndex] | Select-String -Pattern $RegEx -AllMatches | ForEach-Object { $_.Matches } } } <# .DESCRIPTION Parses the provided help data for flag and option parameters. #> function Get-ParsedHelpParam { [CmdletBinding()] param ( # The help data to parse. [Parameter(ValueFromPipeline)] [string]$HelpLine ) begin { $lineNumber = 0 $parsedParams = @() } process { $indentCount = 0 $paramLineElems = Get-ParsedHelpLineElement -HelpLine $HelpLine -IndentationCount ([ref]$indentCount) if ($null -ne $paramLineElems) { # TODO: check if the last processed paramLineElems had siblings, if so, copy the last sibling's $Tail to each of the other siblings. $sibling = [ref]$null $paramLineElems.Value | ForEach-Object { $param = $_ $paramAlt = $null if ($param.IndexOf('[') -ge 0 -and $param.IndexOf(']') -ge 0) { # NOTE: This is a basic solution and may not cover more complex # scenarios where there are multiple sets of brackets in a param. $match = $param | Select-String -Pattern '\[([a-zA-Z0-9\-#]+)\]' $paramAlt = $param.Replace($match.Matches[0].Groups[0].Value, $match.Matches[0].Groups[1].Value) $param = $param.Replace($match.Matches[0].Groups[0].Value, '') } $item = [PSCustomObject]@{ Param = $param.TrimEnd('[').Replace('[=','') Values = @() LineNumber = $lineNumber Line = $HelpLine Tail = @() TailEnd = $false Indent = $indentCount Sibling = $sibling } # TODO: support function argument to customize this? # CMAKE has format such as: "--log-level=<ERROR|WARNING|NOTICE|STATUS|VERBOSE|DEBUG|TRACE>" # Others use --option=[VALUE1|VALUE2] or --option[=VALUE1|VALUE2] $paramValues = @($item.Line | Select-String "(=\[|\[=)[a-zA-Z0-9\|<>]+\]") if ($paramValues.Count -gt 0) { $item.Values = @($paramValues.Matches[0].Value.Substring(2).TrimEnd(']').Split('|')) } $parsedParams += $item $sibling = [ref]($item) if ($null -ne $paramAlt) { $item = [PSCustomObject]@{ Param = $paramAlt.TrimEnd('[').Replace('[=','') Values = @() LineNumber = $lineNumber Line = $HelpLine Tail = @() TailEnd = $false Indent = $indentCount Sibling = $sibling } $parsedParams += $item $sibling = [ref]($item) } } } elseif ($parsedParams.Count -gt 0) { if ([string]::IsNullOrWhiteSpace($HelpLine)) { $parsedParams[-1].TailEnd = $true } elseif (-not($parsedParams[-1].TailEnd)) { $parsedParams[-1].Tail += $HelpLine } } $lineNumber++ } end { $parsedParams | Sort-Object -Property "Param" -CaseSensitive } } <# .DESCRIPTION Parses the provided help data for flag parameters. #> function Get-ParsedHelpFlag { [CmdletBinding()] [OutputType([PSCustomObject])] param ( # The help data to parse. [Parameter()] [string[]]$HelpData ) process { $HelpData | Get-ParsedHelpParam | Where-object { -not $_.Param.StartsWith('--') } } } <# .DESCRIPTION Parses the provided help data for option parameters. #> function Get-ParsedHelpOption { [CmdletBinding()] [OutputType([PSCustomObject])] param ( # The help data to parse. [Parameter()] [string[]]$HelpData ) process { $HelpData | Get-ParsedHelpParam | Where-object { $_.Param.StartsWith('--') } } } <# .DESCRIPTION Creates a new tab completion ParameterName result using the provided parameter information. #> function New-ParsedHelpParamCompletionResult { [CmdletBinding(SupportsShouldProcess)] [OutputType([System.Management.Automation.CompletionResult])] param ( # Metadata for the currently processed parameter to determine whether it # is a relevant tab-completion result. [Parameter(ValueFromPipeline)] [object]$ParamInfo, # The word to provide tab-completion results for. [Parameter(Mandatory)] [string]$WordToComplete ) process { if ( $ParamInfo.Param -like "$WordToComplete*" ) { $toolTip = $ParamInfo.Line $toolTip += "$( if ($ParamInfo.Tail.Count -gt 0) { [System.Environment]::NewLine + ($ParamInfo.Tail -join [System.Environment]::NewLine) } )" # This is a workaround for when two flags are identical except for their casing. # This should be improved on further for multi-character flags/options share # the same issue. In this case, it may be best to store a list of all parameter # strings and determine if another entry exists that would introduce this problem. $listItem = $ParamInfo.Param if ($ParamInfo.Param.StartsWith('-') -and $ParamInfo.Param.Length -eq 2 -and $ParamInfo.Param[1] -cmatch '[A-Z]') { $listItem += ' ' } if ($PSCmdlet.ShouldProcess("New $($ParamInfo.Param) CompletionResult")) { [System.Management.Automation.CompletionResult]::new( $ParamInfo.Param, $listItem, "ParameterName", $toolTip) } } } } <# .DESCRIPTION Gets the previous parameter in the command's abstract syntax tree object. #> function Get-ParsedHelpPrevParam { param ( # The AST for the completion result's current command line state. $CommandAst, # The current cusror position. $CursorPosition ) $c = $CommandAst.ToString() $prev = $CommandAst.CommandElements[-1].ToString() if ($CursorPosition -le $c.Length) { $r = $c.LastIndexOf(" ", $CursorPosition) $l = $c.LastIndexOf(" ", $r - 1) while ($c[$r - 1] -eq ' ') { $r = $r - 1 } $prev = $c.Substring($l + 1, $r - $l - 1) } $prev } <# .DESCRIPTION Creates a new tab completion ParameterValue result using the provided parameter information. #> function New-ParsedHelpValueCompletionResult { [CmdletBinding(SupportsShouldProcess)] [OutputType([System.Management.Automation.CompletionResult])] param ( # The value to create a completion result for. [Parameter(ValueFromPipeline)] [string]$ParamValue, # Represents the data to paste prior to the accepted completion result. # This may for instance be the parameter name followed by '=' [Parameter()] [string]$ResultPrefix ) process { if ($PSCmdlet.ShouldProcess("New $($ParamInfo.Param) CompletionResult")) { [System.Management.Automation.CompletionResult]::new( "$ResultPrefix$ParamValue", $ParamValue, "ParameterValue", 'Enumerated Parameter Value') } } } <# .DESCRIPTION Parses the provided help data for accepted values for previously specified parameter in the command line's AST. #> function Get-ParsedHelpParamValue { [CmdletBinding()] param ( # Help line(s) to parse for parameter values. [Parameter(ValueFromPipeline)] [string]$HelpLine, # The word to complete for tab-completion. [Parameter()] [string]$WordToComplete, # The command abstract syntax tree used to extract the previous token # in order to resolve the parameter name context. [Parameter()] [object]$CommandAst, # Where the cursor is positioned in the $WordToComplete. [Parameter()] [int]$CursorPosition, # To be set when the word to complete contains the equals symbol. # This indicates that the parameter value completion exists in the same # token as the parameter name. [Parameter()] [switch]$ParamValueAssignment, # Reference which will be updated with the appropriate string to be passed # into New-ParsedHelpValueCompletionResult. [Parameter()] [ref]$ResultPrefix ) begin { if ($ParamValueAssignment) { $paramElems = $WordToComplete.Split('=') $paramName = $paramElems[0] $paramValue = $paramElems[-1] if ($null -ne $ResultPrefix) { $ResultPrefix.Value = "$paramName=" } } else { $paramName = Get-ParsedHelpPrevParam -CommandAst $CommandAst -CursorPosition $CursorPosition $paramValue = $wordToComplete if ($null -ne $ResultPrefix) { $ResultPrefix.Value = "" } } } process { if ($paramName.Contains('=')) { return } $HelpLine | Get-ParsedHelpParam | Where-Object { ($_.Param -match "^$paramName=?$") -and ($_.Values.Count -gt 0) } | ForEach-Object { $_.Values } | Where-Object { $_ -like "$paramValue*" } } } |