Save-MarkdownCommandDocumentation.ps1
<#PSScriptInfo
.VERSION 0.0.2 .GUID 3751b890-2eed-413b-976f-e4becb9170f8 .AUTHOR Baptiste Cabrera .COMPANYNAME Bca .COPYRIGHT (c) 2020 Bca. All rights reserved. .TAGS markdown documentation Linux Windows MacOS .LICENSEURI https://github.com/baptistecabrera/bca-savemarkdowncommanddocumentation/blob/master/LICENSE .PROJECTURI https://github.com/baptistecabrera/bca-savemarkdowncommanddocumentation .ICONURI https://www.powershellgallery.com/Content/Images/Branding/packageDefaultIcon.png .EXTERNALMODULEDEPENDENCIES .REQUIREDSCRIPTS .EXTERNALSCRIPTDEPENDENCIES .RELEASENOTES - Some format enhancements #> <# .SYNOPSIS Saves help in markdown format. .DESCRIPTION Saves commands help in markdown format, using info from Get-Help, Get-Command, Test-ScriptFileInfo (if a script is specified) and Get-Module (if a module is specified). .PARAMETER ModuleName A string containing the name of the module. The module name must be listable, already imported, or the ModulePath must be specified. .PARAMETER ModulePath A string containing the path of the module file. .PARAMETER ScriptPath A string containing the path of the script file. .PARAMETER Path A string containing the output folder path. .PARAMETER OutputLayout A string containing the output layout. - 'OneFilePerCommand' will save one file per command named ReadMe.md - 'OneFilePerCommandWithIndex' will save one index file named ReadMe.md that lists the commands and link to them, and one file per command named after the command and placed under a subloder named 'commands'. .EXAMPLE .\Save-MarkdownCommandDocumentation.ps1 -ScriptPath C:\MyProject\MyScript.ps1 -OutputLayout OneFilePerCommand -Path C:\MyProject\doc Description ----------- This example will save a command file from C:\MyProject\MyScript.ps1 in C:\MyProject\doc. .EXAMPLE .\Save-MarkdownCommandDocumentation.ps1 -ModuleName MyModule -OutputLayout OneFilePerCommandWithIndex -Path C:\MyProject\doc Description ----------- This example will save an index of the commands exported by the module MyModule, and one file per command in C:\MyProject\doc. .EXAMPLE .\Save-MarkdownCommandDocumentation.ps1 -ModulePath C:\MyProkect\MyModule\MyModule.psd1 -OutputLayout OneFilePerCommandWithIndex -Path C:\MyProject\doc Description ----------- This example will save an index of the commands exported by the module MyModule from path C:\MyProkect\MyModule\MyModule.psd1, and one file per command in C:\MyProject\doc. .NOTES If you have no comment-based help and no command manifest (ModuleManifest or ScriptFileInfo), the documentation will be limited. #> [CmdletBinding()] param ( [Parameter(ParameterSetName = "FromModulePath", Mandatory = $false)] [Parameter(ParameterSetName = "FromModuleName", Mandatory = $true)] [string] $ModuleName, [Parameter(ParameterSetName = "FromModulePath", Mandatory = $true)] [string] $ModulePath, [Parameter(ParameterSetName = "FromScriptPath", Mandatory = $true)] [ValidateScript( {Test-Path $_ } )] [string] $ScriptPath, [Parameter(ParameterSetName = "FromModulePath", Mandatory = $false)] [Parameter(ParameterSetName = "FromModuleName", Mandatory = $false)] [Parameter(ParameterSetName = "FromScriptPath", Mandatory = $false)] [ValidateNotNullOrEmpty()] [ValidateScript( { Test-Path $_ } )] [string] $Path = $PSScriptRoot, [Parameter(ParameterSetName = "FromModulePath", Mandatory = $false)] [Parameter(ParameterSetName = "FromModuleName", Mandatory = $false)] [Parameter(ParameterSetName = "FromScriptPath", Mandatory = $false)] [ValidateSet("OneFilePerCommand", "OneFilePerCommandWithIndex")] [string] $OutputLayout = "OneFilePerCommand" ) begin { $CommandPath = $Path switch -Regex ($PSCmdlet.ParameterSetName) { "FromModule" { $Modules = Get-Module if ($ModulePath) { Import-Module $ModulePath -Force if (!$ModuleName) { $ModuleName = (Split-Path $ModulePath -Leaf).Replace(".psd1", "").Replace(".psm1", "") } } $Parent = Get-Module -Name $ModuleName $Command = Get-Command -Module $ModuleName } "FromScriptPath" { try { $Parent = Test-ScriptFileInfo -Path $ScriptPath } catch {} $Command = Get-Command $ScriptPath } } if ($OutputLayout -eq "OneFilePerCommandWithIndex") { $IndexFile = Join-Path $Path "ReadMe.md" $CommandPath = Join-Path $Path "commands" If (!(Test-Path $CommandPath)) { New-Item -Path $CommandPath -ItemType Directory -Force | Out-Null } '# {0} `{1}`' -f $Parent.Name, $Parent.Version | Set-Content -Path $IndexFile if ($Parent.Tags) { 'Tags: `{0}`' -f (($Parent.Tags | Sort-Object -Unique) -join '` `') | Add-Content -Path $IndexFile } if ($Parent.PowerShellVersion) { "`r`n`Minimum PowerShell version: ``{0}``" -f $Parent.PowerShellVersion | Add-Content -Path $IndexFile } "`r`n{0}" -f $Parent.Description | Add-Content -Path $IndexFile "`r`n## Commands" | Add-Content -Path $IndexFile } if ($Host.UI.RawUI) { $rawUI = $Host.UI.RawUI $oldSize = $rawUI.BufferSize $typeName = $oldSize.GetType().FullName $newSize = New-Object $typeName (500, $oldSize.Height) $rawUI.BufferSize = $newSize } } process { $Command | ForEach-Object { $CurrentCommand = $_ if ($CurrentCommand.CommandType -eq "ExternalScript") { $Help = Get-Help $CurrentCommand.Path -Full } Else { $Help = Get-Help $CurrentCommand.Name -Full } $CommandFile = Join-Path $CommandPath ("{0}.md" -f $CurrentCommand.Name) $CommandNameHeader = '# {0}' -f $CurrentCommand.Name if ($Parent.Version) { $CommandNameHeader += ' `{0}`' -f $Parent.Version } $CommandNameHeader | Set-Content -Path $CommandFile if ($OutputLayout -ne "OneFilePerCommandWithIndex") { if ($Parent.Tags) { 'Tags: `{0}`' -f (($Parent.Tags | Sort-Object -Unique) -join '` `') | Add-Content -Path $CommandFile } if ($Parent.PowerShellVersion) { "`r`nMinimum PowerShell Version: ``{0}``" -f $Parent.PowerShellVersion | Add-Content -Path $CommandFile } } else { ("# {0}" -f $CurrentCommand.Name) | Set-Content -Path $CommandFile } "`r`nType: {0}" -f $CurrentCommand.CommandType | Add-Content -Path $CommandFile if ($CurrentCommand.ModuleName) { ("`r`nModule: [{0}]({1})" -f $CurrentCommand.ModuleName, "../ReadMe.md") | Add-Content -Path $CommandFile } if ($OutputLayout -eq "OneFilePerCommandWithIndex") { ("- [{0}](commands/{0}.md)" -f $CurrentCommand.Name) | Add-Content -Path $IndexFile } if ($Help.Synopsis -notlike "$($Help.Name)*") { "`r`n{0}" -f $Help.Synopsis | Add-Content $CommandFile } if ($Help.Description) { "## Description" | Add-Content -Path $CommandFile ($Help.Description | Out-String).Trim() | Add-Content $CommandFile } "## Syntax" | Add-Content -Path $CommandFile $CurrentCommand.ParameterSets | ForEach-Object { if ($_.Name -ne "__AllParameterSets") { $ParameterSetTitle = "### {0}" -f $_.Name if ($_.IsDefault) { $ParameterSetTitle += " (default)" } $ParameterSetTitle | Add-Content $CommandFile } "``````powershell`r`n{0} {1}`r`n``````" -f $CurrentCommand.Name, $_.ToString() | Add-Content $CommandFile } if ($Help.examples) { "## Examples" | Add-Content -Path $CommandFile $Help.examples.example | ForEach-Object { $Title = $_.title.Replace("EXAMPLE", "Example").Replace("-------------------------- ", "").Replace(" --------------------------", "") ("### {0}" -f $Title) | Add-Content -Path $CommandFile $Example = "{0}`r`n{1}" -f $_.code, ($_.remarks | Out-String).Trim() $Code = ($Example -split "`r`nDescription`r`n-----------`r`n")[0] $Description = ($Example -split "`r`nDescription`r`n-----------`r`n")[1] "``````powershell`r`n{0}`r`n```````r`n{1}" -f $Code, $Description | Add-Content -Path $CommandFile } } if ($CurrentCommand.Parameters) { "## Parameters" | Add-Content -Path $CommandFile $CurrentCommand.Parameters.Keys | Where-Object { $CurrentCommand.Parameters.$_.Name -notin ([System.Management.Automation.PSCmdlet]::CommonParameters + [System.Management.Automation.PSCmdlet]::OptionalCommonParameters) } | ForEach-Object { $ParameterMd = @() $CommandParameter = $CurrentCommand.Parameters.$_ $HelpParameter = $Help.parameters.parameter | Where-Object { $_.name -eq $CommandParameter.Name } $Obsolete = $CommandParameter.Attributes | Where-Object { $_.TypeId.Name -eq "ObsoleteAttribute" } $ParameterAttribute = $CommandParameter.Attributes | Where-Object { $_.TypeId.Name -eq "ParameterAttribute" } $ParameterSetAttributesMd = @() $ParameterAttribute | ForEach-Object { $AcceptPipeline = $false if ($_.ValueFromPipelineByPropertyName -or $_.ValueFromPipeline) { $AcceptPipeline = $true } $_ | Add-Member -MemberType NoteProperty -Name AcceptPipeline -Value $AcceptPipeline -PassThru -Force | Out-Null } $ParameterMd += '### `-{0}`' -f $CommandParameter.Name if ($Obsolete) { $ParameterMd += ":warning: This parameter is obsolete: {0}" -f $Obsolete.Message } if ($HelpParameter.description) { $ParameterMd += ($HelpParameter.description | Out-String).Trim() } $ParameterMd += "`r`n| | |" $ParameterMd += "|:-|:-|" $ParameterMd += "|Type:|{0}|" -f $CommandParameter.ParameterType.Name if ($CommandParameter.Aliases) { $ParameterMd += "|Aliases|{0}|" -f ($CommandParameter.Aliases -join ", ") } if ($HelpParameter.defaultValue) { $ParameterMd += '|Default value:|`{0}`|' -f $HelpParameter.defaultValue } if ($CommandParameter.ParameterSets.Keys -ne "__AllParameterSets") { $ParameterMd += "|Parameter sets:|{0}|" -f ($CommandParameter.ParameterSets.Keys -join ", ") } $PositionMd = $false $MandatoryMd = $false $PipelineMd = $false if (($ParameterAttribute.Position | Get-Unique).Count -le 1) { $Position = $ParameterAttribute.Position | Get-Unique if ($Position -lt 0) { $Position = "Named" } $ParameterMd += "|Position:|{0}|" -f $Position $PositionMd = $true } if (($ParameterAttribute.Mandatory | Get-Unique).Count -le 1) { $ParameterMd += "|Required:|{0}|" -f ($ParameterAttribute.Mandatory | Get-Unique) $MandatoryMd = $true } if (($ParameterAttribute.AcceptPipeline | Get-Unique).Count -le 1) { if (!$ParameterAttribute.AcceptPipeline) { $ParameterMd += "|Accepts pipepline input:|{0}|" -f ($ParameterAttribute.AcceptPipeline | Get-Unique) $PipelineMd = $true } elseif ((($ParameterAttribute.ValueFromPipelineByPropertyName | Get-Unique).Count -le 1) -and $ParameterAttribute.ValueFromPipelineByPropertyName[0]) { $ParameterMd += "|Accepts pipepline input:|{0} (by property name)|" -f ($ParameterAttribute.ValueFromPipelineByPropertyName | Get-Unique) $PipelineMd = $true } elseif (($ParameterAttribute.ValueFromPipeline | Get-Unique).Count -le 1) { $ParameterMd += "|Accepts pipepline input:|{0}|" -f ($ParameterAttribute.ValueFromPipeline | Get-Unique) $PipelineMd = $true } } $CommandParameter.Attributes | ForEach-Object { $Attribute = $_ switch -Regex ($Attribute.TypeId.Name) { "ParameterAttribute" { $ParameterAttributeMd = @() $Position = $Attribute.Position if ($Position -lt 0) { $Position = "Named" } if (!$PositionMd) { $ParameterAttributeMd += "|Position:|{0}|" -f $Position } if (!$MandatoryMd) { $ParameterAttributeMd += "|Required:|{0}|" -f $Attribute.Mandatory } if (!$PipelineMd) { if (!$Attribute.ValueFromPipelineByPropertyName) { $ParameterAttributeMd += "|Accepts pipepline input:|{0}|" -f ($Attribute.AcceptPipeline | Get-Unique) } else { $ParameterAttributeMd += "|Accepts pipepline input:|{0} (by property name)|" -f ($Attribute.ValueFromPipelineByPropertyName | Get-Unique) } } if ($Attribute.ParameterSetName -eq "__AllParameterSets") { $ParameterMd += $ParameterAttributeMd } else { if ($ParameterAttributeMd) { $ParameterSetAttributesMd += "`r`n|{0}| |" -f $Attribute.ParameterSetName $ParameterSetAttributesMd += "|:-|:-|" $ParameterSetAttributesMd += $ParameterAttributeMd } } } "^Validate.*Attribute$" { $Attribute | Get-Member -MemberType Property | Where-Object { $_.Name -notin "TypeId", "IgnoreCase" } | ForEach-Object { $Value = $Attribute."$($_.Name)" if ($Value.GetType().Name -eq "String[]") { $Value = $Value -join ", " } if ($_.Name -in "ScriptBlock", "RegexPattern") { $Value = '`{0}`' -f $Value.ToString().Replace("`r`n", " ") While ($Value.Contains(" ")) { $Value = $Value.Replace(" ", " ") } } $ParameterMd += "|Validation ({0}):|{1}|" -f $_.Name, $Value } } } } $ParameterMd -join "`r`n" | Add-Content $CommandFile $ParameterSetAttributesMd -join "`r`n" | Add-Content $CommandFile } if ($CurrentCommand.Parameters.Keys -contains "WhatIf") { '### `-{0}`' -f "WhatIf" | Add-Content $CommandFile "This command supports the WhatIf parameter to simulate the action before executing it." | Add-Content $CommandFile } if ($CurrentCommand.Parameters.Keys -contains "Confirm") { '### `-{0}`' -f "Confirm" | Add-Content $CommandFile "This command supports the Confirm parameter to require a user confirmation before executing it." | Add-Content $CommandFile } if ($CurrentCommand.CmdletBinding) { '### `-{0}`' -f "<CommonParameters>" | Add-Content $CommandFile "This command supports the common parameters: Verbose, Debug, ErrorAction, ErrorVariable, WarningAction, WarningVariable, OutBuffer, PipelineVariable, and OutVariable.`r`nFor more information, see [about_CommonParameters](https:/go.microsoft.com/fwlink/?LinkID=113216)." | Add-Content $CommandFile } } if ($Help.inputTypes) { "## Inputs" | Add-Content -Path $CommandFile $help.inputTypes.inputType.type.name | ForEach-Object { "`r`n**{0}**`r`n`r`n{1}" -f $_.Split("`r`n")[0], $_.Split("`r`n")[1] | Add-Content $CommandFile } } if ($Help.returnValues) { "## Outputs" | Add-Content -Path $CommandFile $help.returnValues.returnValue.type.name | ForEach-Object { "`r`n**{0}**`r`n`r`n{1}" -f $_.Split("`r`n")[0], $_.Split("`r`n")[1] | Add-Content $CommandFile } } if ($Help.alertSet) { "## Notes" | Add-Content -Path $CommandFile ($Help.alertSet.alert | Out-String).Trim() | Add-Content -Path $CommandFile } if ($Help.relatedLinks.navigationLink.linkText) { "## Related Links" | Add-Content -Path $CommandFile $help.relatedLinks.navigationLink | Where-Object { $_ } | ForEach-Object { if ($_.linkText -in $Command.Name) { "- [{0}]({0}.md)" -f $_.linkText | Add-Content $CommandFile } elseif ($_.uri) { "- [{0}]({0})" -f $_.uri | Add-Content $CommandFile } else { "- {0}" -f $_.linkText | Add-Content $CommandFile } } } } if ($OutputLayout -eq "OneFilePerCommandWithIndex") { if ($Parent.ReleaseNotes) { "`r`n## Release Notes`r`n{0}" -f $Parent.ReleaseNotes | Add-Content -Path $IndexFile } "---`r`n[{0}]({1})" -f $Parent.Name, $Parent.ProjectUri | Add-Content -Path $IndexFile } else { if ($Parent.ReleaseNotes) { "`r`n## Release Notes`r`n{0}" -f $Parent.ReleaseNotes | Add-Content -Path $CommandFile } } } end { if ($Modules) { (Compare-Object -ReferenceObject $Modules -DifferenceObject (Get-Module)).InputObject | Where-Object { $_ } | ForEach-Object { Remove-Module $_.Name } } if ($Host.UI.RawUI) { $rawUI = $Host.UI.RawUI $rawUI.BufferSize = $oldSize } } |