Get-MAML.ps1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 |
function Get-MAML { <# .Synopsis Gets MAML help .Description Gets help for a given command, as MAML (Microsoft Assistance Markup Language) xml. .Example Get-MAML -Name Get-MAML .Example Get-Command Get-MAML | Get-MAML .Example Get-MAML -Name Get-MAML -Compact .Example Get-MAML -Name Get-MAML -XML .Link Get-Help .Link Save-MAML .INPUTS [Management.Automation.CommandInfo] Accepts a command .Outputs [String] The MAML, as a String. This is the default. .Outputs [Xml] The MAML, as an XmlDocument (when -XML is passed in) #> [CmdletBinding(DefaultParameterSetName='CommandInfo')] [OutputType([string],[xml])] [Alias('ConvertTo-MAML')] 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() } } |