allcommands.ps1
### DO NOT EDIT THIS FILE DIRECTLY ### #.ExternalHelp HelpOut-Help.xml function ConvertTo-MAML { [CmdletBinding(DefaultParameterSetName='CommandInfo')] [OutputType([string],[xml])] param( # The name of or more commands. [Parameter(ParameterSetName='ByName',Position=0,ValueFromPipelineByPropertyName=$true)] [string[]] $Name, # The name of one or more modules. [Parameter(ParameterSetName='ByModule',ValueFromPipelineByPropertyName=$true)] [string[]] $Module, # The CommandInfo object (returned from Get-Command). [Parameter(Mandatory=$true,ParameterSetName='FromCommandInfo', ValueFromPipeline=$true)] [Management.Automation.CommandInfo[]] $CommandInfo, # If set, the generated MAML will be compact (no extra whitespace or indentation). If not set, the MAML will be indented. [switch] $Compact, # If set, will return the MAML as an XmlDocument. The default is to return the MAML as a string. [switch] $XML, # If set, the generate MAML will not contain a version number. # This slightly reduces the size of the MAML file, and reduces the rate of changes in the MAML file. [Alias('Unversioned')] [switch] $NoVersion) begin { # First, we need to create a list of all commands we encounter (so we can process them at the end) $allCommands = [Collections.ArrayList]::new() # Then, we want to get the type accelerators (so we don't have to keep getting them each time we're interested) $typeAccelerators = [PSOBject].Assembly.GetType('System.Management.Automation.TypeAccelerators')::Get # Next up, we're going to declare a bunch of ScriptBlocks, which we'll call to construct the XML in pieces. # This way we can create a nested structure (in this case, XML), by calling the pieces we want and letting them return the XML in chunks #region Get TypeName $GetTypeName = {param([Type]$t) # We'll want to check to see if there are any accelerators. if (-not $typeAccelerators -and $typeAccelerators.GetEnumerator) { # If there weren't return $t.Fullname # return the fullname. } foreach ($_ in $typeAccelerators.GetEnumerator()) { # Loop through the accelerators. if ($_.Value -eq $t) { # If it's an accelrator for the target type return $_.Key.Substring(0,1).ToUpper() + $_.Key.Substring(1) # return the key (and fix it's casing) } } return $t.Fullname # If we didn't find it in the accelerators list, return the fullname. } #endregion Get TypeName #region Write Type # Both Inputs and Outputs have the same internal tag structure for a value, so one script block handles both cases. $WriteType = {param($t) $typename = $t.type[0].name $descriptionLines = $null if ($in.description) { # If we have a description, $descriptionLines = $in.Description[0].text -split "`n|`r`n" -ne '' # we we're good. } else { # If we didn't, it's probably because comment based help mangles things a bit (it puts everything in a long typename). # Let's fix this by assigning the inType from the first line, and setting the rest as description lines $typename, $descriptionLines = $t.type[0].Name -split "`n|`r`n" -ne '' } $typename = [Security.SecurityElement]::Escape("$typename".Trim()) "<dev:type><maml:name>$typename</maml:name><maml:uri/><maml:description /></dev:type>" # Write the type information if ($descriptionLines) { # If we had a description '<maml:description>' foreach ($line in $descriptionLines) { # Write each line in it's own para tag so that it renders right. $esc = [Security.SecurityElement]::Escape($line) "<maml:para>$esc</maml:para>" } '</maml:description>' } } #endregion Write Type #region Write Command Details $writeCommandDetails = { # The command.details tag has 5 parts we want to provide # * Name, # * Noun # * Verb # * Synopsis # * Version $Version = "<dev:version>$(if ($cmdInfo.Version) { $cmdInfo.Version.ToString() })</dev:version>" "<command:details> <command:name>$([Security.SecurityElement]::Escape($cmdInfo.Name))</command:name> <command:noun>$noun</command:noun> <command:verb>$verb</command:verb> <maml:description> <maml:para>$([Security.SecurityElement]::Escape($commandHelp.Synopsis))</maml:para> </maml:description> $(if (-not $NoVersion) { $Version}) </command:details> <maml:description> $( foreach ($line in @($commandHelp.Description)[0].text -split "`n|`r`n") { if (-not $line) { continue } "<maml:para>$([Security.SecurityElement]::Escape($Line))</maml:para>" } ) </maml:description> " } #endregion Write Command Details #region Write Parameter $WriteParameter = { # Prepare the command.parameter attributes: $position = if ($param.Position -ge 0) { $param.Position } else {"named" } #* Position $fromPipeline = #*FromPipeline if ($param.ValueFromPipeline) { "True (ByValue)" } elseif ($param.ValueFromPipelineByPropertyName) { "True (ByPropertyName)" } else { "False" } $isRequired = if ($param.IsMandatory) { "true" } else { "false" } #*Required # Pick out the help for a given parameter $paramHelp = foreach ($_ in $commandHelp.parameters.parameter) { if ( $_.Name -eq $param.Name ){ $_ break } } $paramTypeName = & $GetTypeName $param.ParameterType # and get the type name of the parameter type. "<command:parameter required='$isRequired' position='$position' pipelineInput='$fromPipeline' aliases='' variableLength='true' globbing='false'>" #* Echo the start tag "<maml:name>$($param.Name)</maml:name>" #* The maml.name tag '<maml:description>' #*The description tag foreach ($d in $paramHelp.Description) { "<maml:para>$([Security.SecurityElement]::Escape($d.Text))</maml:para>" } '</maml:description>' #*The parameterValue tag (which oddly enough, describes the parameter type) "<command:parameterValue required='$isRequired' variableLength='true'>$paramTypeName</command:parameterValue>" #*The type tag (which is also it's type) "<dev:type><maml:name>$paramTypeName</maml:name><maml:uri /></dev:type>" #*and an empty default value. '<dev:defaultValue></dev:defaultValue>' #* Then close the parameter tag. '</command:parameter>' } #endregion Write Parameter #region Write Parameters $WriteCommandParameters = { '<command:parameters>' # *Open the parameters tag; foreach ($param in ($cmdMd.Parameters.Values | Sort-Object Name)) { #*Loop through the command's parameters alphabetically & $WriteParameter #*Write each parameter. } '</command:parameters>' #*Close the parameters tag } #endregion Write Parameters #region Write Examples $WriteExamples = { # If there were no examples, return. if (-not $commandHelp.Examples.example) { return } "<command:examples>" foreach ($ex in $commandHelp.Examples.Example) { # For each example: '<command:example>' #*Start an example tag '<maml:title>' $ex.Title #*Put it's title in a maml:title tag '</maml:title>' '<maml:introduction>'#* Put it's introduction in a maml:introduction tag foreach ($i in $ex.Introduction) { '<maml:para>' [Security.SecurityElement]::Escape($i.Text) '</maml:para>' } '</maml:introduction>' '<dev:code>' #* Put it's code in a dev:code tag [Security.SecurityElement]::Escape($ex.Code) '</dev:code>' '<dev:remarks>' #* Put it's remarks in a dev:remarks tag foreach ($i in $ex.Remarks) { if (-not $i -or -not $i.Text.Trim()) { continue } '<maml:para>' [Security.SecurityElement]::Escape($i.Text) '</maml:para>' } '</dev:remarks>' '</command:example>' } '</command:examples>' } #endregion Write Examples #region Write Inputs $WriteInputs = { if (-not $commandHelp.inputTypes) { return } # If there were no input types, return. '<command:inputTypes>' #*Open the inputTypes Tag. foreach ($in in $commandHelp.inputTypes[0].inputType) { #*Walk thru each type in help. '<command:inputType>' & $WriteType $in #*Write the type information (in an inputType tag). '</command:inputType>' } '</command:inputTypes>' #*Close the Input Types Tag. } #endregion Write Inputs #region Write Outputs $WriteOutputs = { if (-not $commandHelp.returnValues) { return } # If there were no return values, return. '<command:returnValues>' # *Open the returnValues tag foreach ($rt in $commandHelp.returnValues[0].returnValue) { # *Walk thru each return value '<command:returnValue>' & $WriteType $rt # *write the type information (in an returnValue tag) '</command:returnValue>' } '</command:returnValues>' #*Close the returnValues tag } #endregion Write Outputs #region Write Notes $WriteNotes = { if (-not $commandHelp.alertSet) { return } # If there were no notes, return. "<maml:alertSet><maml:title></maml:title>" #*Open the alertSet tag and emit an empty title foreach ($note in $commandHelp.alertSet[0].alert) { #*Walk thru each note "<maml:alert><maml:para>" $([Security.SecurityElement]::Escape($note.Text)) #*Put each note in a maml:alert element "</maml:para></maml:alert>" } "</maml:alertSet>" #*Close the alertSet tag } #endregion Write Notes #region Write Syntax $WriteSyntax = { if (-not $cmdInfo.ParameterSets) { return } # If this command didn't have parameters, return "<command:syntax>" #*Open the syntax tag foreach ($syn in $cmdInfo.ParameterSets) {#*Walk thru each parameter set "<command:syntaxItem><maml:name>$($cmdInfo.Name)</maml:name>" #*Create a syntaxItem tag, with the name of the command. foreach ($param in $syn.Parameters) { #* Skip parameters that are not directly declared (e.g. -ErrorAction) if (-not $cmdMd.Parameters.ContainsKey($param.Name)) { continue } & $WriteParameter #* Write help for each parameter } "</command:syntaxItem>" #*Close the syntax item tag } "</command:syntax>"#*Close the syntax tag } #endregion Write Syntax #region Write Links $WriteLinks = { # If the command didn't have any links, return. if (-not $commandHelp.relatedLinks.navigationLink) { return } '<maml:relatedLinks>' #* Open a related Links tag foreach ($l in $commandHelp.relatedLinks.navigationLink) { #*Walk thru each link $linkText, $LinkUrl = "$($l.linkText)".Trim(), "$($l.Uri)".Trim() # and write it's tag. '<maml:navigationLink>' "<maml:linkText>$linkText</maml:linkText>" "<maml:uri>$LinkUrl</maml:uri>" '</maml:navigationLink>' } '</maml:relatedLinks>' #* Close the related Links tag } #endregion Write Links #- - - Now that we've declared all of these little ScriptBlock parts, we'll put them in a list in the order they'll run. $WriteMaml = $writeCommandDetails, $writeSyntax,$WriteCommandParameters,$WriteInputs,$writeOutputs, $writeNotes, $WriteExamples, $writeLinks #- - - } process { if ($PSCmdlet.ParameterSetName -eq 'ByName') { # If we're getting comamnds by name, $CommandInfo = @(foreach ($n in $name) { $ExecutionContext.InvokeCommand.GetCommands($N,'Function,Cmdlet', $true) # find each command (treating Name like a wildcard). }) } if ($PSCmdlet.ParameterSetName -eq 'ByModule') { # If we're getting commands by module $CommandInfo = @(foreach ($m in $module) { # find each module (Get-Module -Name $m).ExportedCommands.Values # and get it's exports. }) } $filteredCmds = @(foreach ($ci in $CommandInfo) { # Filter the list of commands if ($ci -is [Management.Automation.AliasInfo] -or # (throw out aliases and applications). $ci -is [Management.Automation.ApplicationInfo]) { continue } $ci }) if ($filteredCmds) { $null = $allCommands.AddRange($filteredCmds) } } end { $c, $t, $id, $maml = # Create some variables for our progress bar, 0, $allCommands.Count, [Random]::new().Next(), [Text.StringBuilder]::new('<helpItems schema="maml">') # and initialize our MAML. foreach ($cmdInfo in $allCommands) { # Walk thru each command. $commandHelp = $null $c++ $p = $c * 100 / $t Write-Progress 'Converting to MAML' "$cmdInfo [$c of $t]" -PercentComplete $p -Id $id # Write a progress message $commandHelp = $cmdInfo | Get-Help # get it's help $cmdMd = [Management.Automation.CommandMetaData]$cmdInfo # get it's command metadata if (-not $commandHelp -or $commandHelp -is [string]) { # (error if we couldn't Get-Help) Write-Error "$cmdInfo Must have a help topic to convert to MAML" return } $verb, $noun = $cmdInfo.Name -split "-" # and split out the noun and verb. # Now we're ready to run all of those script blocks we declared in begin. # All we need to do is append the command node, run each of the script blocks in $WriteMaml, and close the node. $mamlCommand = "<command:command xmlns:maml='http://schemas.microsoft.com/maml/2004/10' xmlns:command='http://schemas.microsoft.com/maml/dev/command/2004/10' xmlns:dev='http://schemas.microsoft.com/maml/dev/2004/10'> $(foreach ($_ in $WriteMaml) { & $_ }) </command:command>" $null = $maml.AppendLine($mamlCommand) } Write-Progress "Exporting Maml" " " -Completed -Id $id # Then we indicate we're done, $null = $maml.Append("</helpItems>") # close the opening tag. $mamlAsXml = [xml]"$maml" # and convert the whole thing to XML. if (-not $mamlAsXml) { return } # If we couldn't, return. if ($XML) { return $mamlAsXml } # If we wanted the XML, return it. $strWrite = [IO.StringWriter]::new() # Now for a little XML magic: # If we create a [IO.StringWriter], we can save it as pretty or compacted XML. $mamlAsXml.PreserveWhitespace = $Compact # Oddly enough, if we're compacting we're setting preserveWhiteSpace to true, which in turn strips all of the whitespace except that inside of your nodes. $mamlAsXml.Save($strWrite) # Anyways, we can save this to the string writer, and it will either make our XML perfectly balanced and indented or compact and free of most whitespace. # Unfortunately, it will not get it's encoding declaration "right". This is because $strWrite is Unicode, and in most cases we'll want our XML to be UTF8. # The next step of the pipeline needs to convert it as it is saved, which is as easy as | Out-File -Encoding UTF8. "$strWrite".Replace('<?xml version="1.0" encoding="utf-16"?>','<?xml version="1.0" encoding="utf-8"?>') $strWrite.Close() $strWrite.Dispose() } } #.ExternalHelp HelpOut-Help.xml function Install-MAML { [OutputType([Nullable], [IO.FileInfo])] param( # The name of one or more modules. [Parameter(Mandatory=$true,Position=0,ParameterSetName='Module',ValueFromPipelineByPropertyName=$true)] [string[]] $Module, # If set, will refresh the documentation for the module before generating the commands file. [Parameter(ValueFromPipelineByPropertyName=$true)] [switch] $NoRefresh, # If set, will compact the generated MAML. This will be ignored if -Refresh is not passed, since no new MAML will be generated. [Parameter(ValueFromPipelineByPropertyName=$true)] [switch] $Compact, # The name of the combined script. By default, allcommands.ps1. [Parameter(Position=1,ValueFromPipelineByPropertyName=$true)] [string] $ScriptName = 'allcommands.ps1', # The root directories containing functions. If not provided, the function root will be the module root. [Parameter(ValueFromPipelineByPropertyName=$true)] [string[]] $FunctionRoot, # If set, the function roots will not be recursively searched. [Parameter(ValueFromPipelineByPropertyName=$true)] [switch] $NoRecurse, # The encoding of the combined script. By default, UTF8. [Parameter(Position=2,ValueFromPipelineByPropertyName=$true)] [ValidateNotNull()] [Text.Encoding] $Encoding = [Text.Encoding]::UTF8, # A list of wildcards to exclude. This list will always contain the ScriptName. [Parameter(ValueFromPipelineByPropertyName=$true)] [string[]] $Exclude, # If set, the generate MAML will not contain a version number. # This slightly reduces the size of the MAML file, and reduces the rate of changes in the MAML file. [Parameter(ValueFromPipelineByPropertyName=$true)] [Alias('Unversioned')] [switch] $NoVersion, # If provided, will save the MAML to a different directory than the current UI culture. [Parameter(ValueFromPipelineByPropertyName=$true)] [Globalization.CultureInfo] $Culture, # If set, will return the files that were generated. [Parameter(ValueFromPipelineByPropertyName=$true)] [switch] $PassThru ) process { if ($ScriptName -notlike '*.ps1') { # First, let's check that the scriptname is a .PS1. $ScriptName += '.ps1' # If it wasn't, add the extension. } $Exclude += $ScriptName # Then, add the script name to the list of exclusions. if (-not $Culture) { # If no culture was specified, $Culture = [Globalization.CultureInfo]::CurrentUICulture # use the current UI culture. } foreach ($m in $Module) { $theModule = Get-Module $m # Resolve the module. if (-not $theModule) { continue } # If we couldn't, continue to the next. $theModuleRoot = $theModule | Split-Path # Find the module's root. if ($PSBoundParameters.FunctionRoot) { # If we provided a function root parameter $functionRoot = foreach ($f in $FunctionRoot) { # then turn each root into an absolute path. if ([IO.File]::Exists($F)) { $f } else { Join-Path $theModuleRoot $f } } } else { $FunctionRoot = "$theModuleRoot" # otherwise, just use the module root. } $fileList = @(foreach ($f in $FunctionRoot) { # Walk thru each function root. Get-ChildItem -Path $f -Recurse:$(-not $Recurse) -Filter *.ps1 | # recursively find all .PS1s & { process { if ($_.Name -notlike '*-*' -or $_.Name -like '*.*.*') { return } foreach ($ex in $Exclude) { if ($_.Name -like $ex) { return } } return $_ } } }) #region Save the MAMLs if (-not $NoRefresh) { # If we're refreshing the MAML, $saveMamlCmd = # find the command Save-MAML if ($MyInvocation.MyCommand.ScriptBlock.Module) { $MyInvocation.MyCommand.ScriptBlock.Module.ExportedCommands['Save-MAML'] } else { $ExecutionContext.SessionState.InvokeCommand.GetCommand('Save-MAML', 'Function') } $saveMamlSplat = @{} + $PSBoundParameters # and pick out the parameters that this function and Save-MAML have in common. foreach ($k in @($saveMamlSplat.Keys)) { if (-not $saveMamlCmd.Parameters.ContainsKey($k)) { $saveMamlSplat.Remove($k) } } $saveMamlSplat.Module = $m # then, set the module Save-MAML @saveMamlSplat # and call Save-MAML } #endregion Save the MAMLs #region Generate the Combined Script # Prepare a regex to find function definitions. $regex = [Regex]::new(' (?<![-\s\#]{1,}) # not preceeded by a -, or whitespace, or a comment function # function keyword \s{1,1} # a single space or tab (?<Name>[^\-]{1,1}\S+) # any non-whitespace, starting with a non-dash \s{0,} # optional whitespace [\(\{] # opening parenthesis or brackets ', 'MultiLine,IgnoreCase,IgnorePatternWhitespace') $newFileContent = # We'll assign new file content by foreach ($f in $fileList) { # walking thru each file. $fileBytes = [IO.File]::ReadAllBytes($f.Fullname) # We'll read the file content in as bytes $ms = [IO.MemoryStream]::new($fileBytes) # so we can use an [IO.MemoryStream] and $sr = [IO.StreamReader]::new($ms, $true) # an [IO.Streamreader] to peek at the encoding $fileContent = $sr.ReadToEnd() # and read it as a string. $start = 0 do { $matched = $regex.Match($fileContent,$start) # See if we find a functon. if ($matched.Success) { # If we found one, $insert = ([Environment]::NewLine + "#.ExternalHelp $M-Help.xml" + [Environment]::NewLine) # insert a line for help. $fileContent = if ($matched.Index) { $fileContent.Insert($matched.Index - 1, $insert) } else { $insert + $fileContent } $start += $matched.Index + $matched.Length $start += $insert.Length # and update our starting position. } # Keep doing this until we've reached the end of the file or the end of the matches. } while ($start -le $filecontent.Length -and $matched.Success) # Then output the file content, stripped of block comments. $fileContent -replace '\<\#(?<Block>(.|\s)+?(?=\#>))\#\>', '' $sr.Close() $sr.Dispose() } # Last but not least, we $combinedCommandsPath = Join-Path $theModuleRoot $ScriptName # determine the path for our combined commands file. "### DO NOT EDIT THIS FILE DIRECTLY ###" | Set-Content -Path $combinedCommandsPath -Encoding $Encoding.HeaderName.Replace('-','') # add a header [IO.File]::AppendAllText($combinedCommandsPath, $newFileContent, $Encoding) # and add our content. #endregion Generate the Combined Script if ($PassThru) { Get-Item -Path $combinedCommandsPath } } } } #.ExternalHelp HelpOut-Help.xml function Save-MAML { [CmdletBinding(DefaultParameterSetName='CommandInfo',SupportsShouldProcess=$true)] [OutputType([Nullable])] param( # The name of one or more modules. [Parameter(ParameterSetName='ByModule',ValueFromPipelineByPropertyName=$true)] [string[]] $Module, # If set, the generated MAML will be compact (no extra whitespace or indentation). If not set, the MAML will be indented. [Parameter(ValueFromPipelineByPropertyName=$true)] [switch] $Compact, # If provided, will save the MAML to a different directory than the current UI culture. [Parameter(ValueFromPipelineByPropertyName=$true)] [Globalization.CultureInfo] $Culture, # If set, the generate MAML will not contain a version number. # This slightly reduces the size of the MAML file, and reduces the rate of changes in the MAML file. [Alias('Unversioned')] [switch] $NoVersion, # If set, will return the files that were generated. [switch] $PassThru) begin { # First, let's cache a reference to ConvertTo-MAML $convertToMaml = if ($MyInvocation.MyCommand.ScriptBlock.Module) { $MyInvocation.MyCommand.ScriptBlock.Module.ExportedCommands['ConvertTo-MAML'] } else { $ExecutionContext.SessionState.InvokeCommand.GetCommand('ConvertTo-MAML', 'Function') } } process { if (-not $convertToMaml) { # If for whatever reason we don't have ConvertTo-Maml Write-Error "Could not Find ConvertTo-MAML" -Category ObjectNotFound -ErrorId ConvertTo-MAML.NotFound # error out. return } $c, $t, $id = 0, $Module.Length, [Random]::new().Next() $splat = @{} + $PSBoundParameters # Copy our parameters foreach ($k in @($splat.Keys)) { # then strip out any parameter if (-not $convertToMaml.Parameters.ContainsKey($k)) { # that wasn't in ConvertTo-MAML. $splat.Remove($k) } } if (-not $Culture) { # If -Culture wasn't provided, use the current culture $Culture = [Globalization.CultureInfo]::CurrentCulture } #region Save the MAMLs foreach ($m in $Module) { # Walk thru the list of module names. $splat.Module = $m if ($t -gt 1) { $c++ Write-Progress 'Saving MAML' $m -PercentComplete $p -Id $id } $theModule = Get-Module $m # Find the module if (-not $theModule) { continue } # (continue if we couldn't). $theModuleRoot = $theModule | Split-Path # Find the module's root, $theModuleCultureDir = Join-Path $theModuleRoot $Culture.Name # then find the culture folder. if (-not (Test-Path $theModuleCultureDir)) { # If that folder didn't exist, $null = New-Item -ItemType Directory -Path $theModuleCultureDir # create it. } $theModuleHelpFile = Join-Path $theModuleCultureDir "$m-Help.xml" # Construct the path to the module help file (e.g. en-us\Module-Help.xml) & $convertToMaml @splat | # Convert the module help to MAML, Set-Content -Encoding UTF8 -Path $theModuleHelpFile # and write the file. } if ($t -gt 1) { Write-Progress 'Saving MAML' 'Complete' -Completed -Id $id } #endregion Save the MAMLs } } |