├── .build ├── _init.ps1 ├── build.ps1 └── packages.config ├── .gitattributes ├── .gitignore ├── LICENSE.md ├── LocalNuGetPackageSource ├── README.md └── RedGate.ThirdParty.Jolt.Core.0.4.0.1.nupkg ├── NuGet.Config ├── README.md ├── SolutionInfo.cs ├── XmlDoc2CmdletDoc.Core ├── Comments │ ├── CachingCommentReader.cs │ ├── ICommentReader.cs │ ├── JoltCommentReader.cs │ ├── LoggingCommentReader.cs │ └── RewritingCommentReader.cs ├── Domain │ ├── Command.cs │ ├── Parameter.cs │ ├── ReflectionParameter.cs │ └── RuntimeParameter.cs ├── Engine.cs ├── EngineException.cs ├── EngineExitCode.cs ├── Extensions │ ├── CommentReaderExtensions.cs │ └── EnumerableExtensions.cs ├── Options.cs ├── Properties │ └── AssemblyInfo.cs ├── XmlDoc2CmdletDoc.Core.csproj └── XmlDoc2CmdletDoc.Core.v2.ncrunchproject ├── XmlDoc2CmdletDoc.TestModule ├── DefaultValue │ └── TestDefaultValueCommand.cs ├── DynamicParameters │ ├── TestNestedTypeDynamicParametersCommand.cs │ └── TestRuntimeDynamicParametersCommand.cs ├── InputTypes │ ├── InputTypeClass.cs │ └── TestInputTypesCommand.cs ├── Maml │ ├── MamlClass.cs │ └── TestMamlElementsCommand.cs ├── Manual │ ├── ManualClass.cs │ └── TestManualElementsCommand.cs ├── Parameterless │ └── TestParameterlessCommand.cs ├── PositionedParameters │ └── TestPositionedParametersCommand.cs ├── Properties │ └── AssemblyInfo.cs ├── References │ └── TestReferencesCommand.cs ├── Undocumented │ ├── TestUndocumentedCommand.cs │ └── UndocumentedClass.cs ├── Wildcards │ └── TestWildcardSupportedCommand.cs ├── WriteOnly │ └── TestWriteOnlyParameterCommand.cs ├── XmlDoc2CmdletDoc.TestModule.csproj └── XmlDoc2CmdletDoc.TestModule.v2.ncrunchproject ├── XmlDoc2CmdletDoc.Tests ├── AcceptanceTests.cs ├── Properties │ └── AssemblyInfo.cs ├── XElementExtensions.cs ├── XmlDoc2CmdletDoc.Tests.csproj └── XmlDoc2CmdletDoc.Tests.v2.ncrunchproject ├── XmlDoc2CmdletDoc.sln ├── XmlDoc2CmdletDoc.sln.DotSettings ├── XmlDoc2CmdletDoc.v2.ncrunchsolution ├── XmlDoc2CmdletDoc ├── Program.cs ├── Properties │ └── AssemblyInfo.cs ├── XmlDoc2CmdletDoc.csproj ├── XmlDoc2CmdletDoc.nuspec ├── XmlDoc2CmdletDoc.targets └── XmlDoc2CmdletDoc.v2.ncrunchproject ├── build.ps1 └── release-notes.md /.build/_init.ps1: -------------------------------------------------------------------------------- 1 | # This file is a bootstrapper for the real build file. It's purpose is as follows: 2 | # 3 | # 1. Define some top-level fubctions (build, clean, rebuild) that can be used to kick off the build from the command-line. 4 | # 2. Download nuget.exe and then obtain some NuGet packages that the real build script relies on. 5 | # 3. Import the RedGate.Build module to make available some convenient build cmdlets. 6 | 7 | $VerbosePreference = 'Continue' # Want useful output in our build log files. 8 | $ProgressPreference = 'SilentlyContinue' # Progress logging slows down TeamCity when downloading files with Invoke-WebRequest. 9 | $ErrorActionPreference = 'Stop' # Abort quickly on error. 10 | 11 | function global:Build 12 | { 13 | [CmdletBinding()] 14 | param( 15 | [string[]] $Task = @('Default'), 16 | 17 | [ValidateSet('Release', 'Debug')] 18 | [string] $Configuration = 'Release' 19 | ) 20 | 21 | Push-Location $PsScriptRoot -Verbose 22 | try 23 | { 24 | # Obtain nuget.exe 25 | $NuGetVersion = [version] '4.9.3' 26 | $NuGetPath = '.\nuget.exe' 27 | if (-not (Test-Path $NuGetPath) -or (Get-Item $NuGetPath).VersionInfo.ProductVersion -ne $NuGetVersion) 28 | { 29 | $NuGetUrl = "https://dist.nuget.org/win-x86-commandline/v$NuGetVersion/nuget.exe" 30 | Write-Host "Downloading $NuGetUrl" 31 | Invoke-WebRequest $NuGetUrl -OutFile $NuGetPath 32 | } 33 | 34 | # Restore the 'build-level' nuget packages into .build/packages if necessary. 35 | $NuGetConfigXml = [xml](Get-Content 'packages.config') 36 | $NuGetConfigXml.packages.package | ForEach-Object { 37 | & $NuGetPath install $_.id ` 38 | -Version $_.version ` 39 | -OutputDirectory 'packages' ` 40 | -ExcludeVersion ` 41 | -PackageSaveMode nuspec 42 | } 43 | 44 | # Import the RedGate.Build module. 45 | Import-Module '.\packages\RedGate.Build\tools\RedGate.Build.psm1' -Force 46 | 47 | # Call the actual build script. 48 | & '.\packages\Invoke-Build\tools\Invoke-Build.ps1' -File .\build.ps1 -Task $Task -Configuration $Configuration 49 | } 50 | finally 51 | { 52 | Pop-Location 53 | } 54 | } 55 | 56 | function global:Clean 57 | { 58 | Build -Task Clean 59 | } 60 | 61 | function global:Rebuild 62 | { 63 | [CmdletBinding()] 64 | param([ValidateSet('Release', 'Debug')] [string] $Configuration = 'Release') 65 | 66 | Build -Task Rebuild -Configuration $Configuration 67 | } 68 | 69 | Write-Host 'This is the XmlDoc2CmdletDoc repo. Here are the available commands:' -ForegroundColor Magenta 70 | Write-Host " Build [-Task ] [-Configuration ]" -ForegroundColor Green 71 | Write-Host " Clean" -ForegroundColor Green 72 | Write-Host " Rebuild [-Configuration ]" -ForegroundColor Green 73 | -------------------------------------------------------------------------------- /.build/build.ps1: -------------------------------------------------------------------------------- 1 | # This file cannot be invoked directly; it simply contains a bunch of Invoke-Build tasks. To use it, invoke 2 | # _init.ps1 which declares three global functions (build, clean, rebuild), then invoke one of those functions. 3 | 4 | [CmdletBinding()] 5 | param([string]$Configuration = 'Release') 6 | 7 | 8 | # Useful paths used by multiple tasks. 9 | $RepositoryRoot = "$PsScriptRoot\.." | Resolve-Path 10 | $SolutionPath = "$RepositoryRoot\XmlDoc2CmdletDoc.sln" | Resolve-Path 11 | $NuGetPath = "$PsScriptRoot\nuget.exe" | Resolve-Path 12 | $DistPath = "$RepositoryRoot\dist" 13 | 14 | 15 | # Helper function for clearer logging of each task. 16 | function Write-Info { 17 | [CmdletBinding()] 18 | param ([string] $Message) 19 | 20 | Write-Host "## $Message ##" -ForegroundColor Magenta 21 | } 22 | 23 | 24 | # Environment-specific configuration should happen here (and only here!) 25 | task Init { 26 | Write-Info 'Establishing build properties' 27 | 28 | # Establish IsAutobuild property. 29 | $script:IsAutomatedBuild = $env:BRANCH_NAME -and $env:BUILD_NUMBER 30 | Write-Host "Is automated build = $IsAutomatedBuild" 31 | 32 | # Load the release notes and parse the version number. 33 | $Notes = Get-ReleaseNotes 34 | $script:ReleaseNotes = [string] $Notes.Content 35 | $script:SemanticVersion = [version] $Notes.Version 36 | Write-Host "Semantic version = '$SemanticVersion'" 37 | 38 | # Establish assembly version number 39 | $script:AssemblyVersion = [version] "$($SemanticVersion.Major).0.0.0" 40 | $script:AssemblyFileVersion = [version] "$SemanticVersion.0" 41 | if ($env:BUILD_NUMBER) { 42 | $VersionSuffix = $env:BUILD_NUMBER # Build server override. 43 | $script:AssemblyFileVersion = [version] "$SemanticVersion.$VersionSuffix" 44 | } 45 | Write-Host "Assembly version = '$AssemblyVersion'" 46 | Write-Host "Assembly file version = '$AssemblyFileVersion'" 47 | TeamCity-SetBuildNumber $AssemblyFileVersion 48 | 49 | # Establish NuGet package version. 50 | $BranchName = Get-BranchName 51 | $IsDefaultBranch = $BranchName -eq 'master' 52 | $script:NuGetPackageVersion = New-SemanticNuGetPackageVersion -Version $AssemblyFileVersion -BranchName $BranchName -IsDefaultBranch $IsDefaultBranch 53 | Write-Host "NuGet package version = $NuGetPackageVersion" 54 | 55 | # Establish whether or not to sign the assemblies. 56 | if ($env:SigningServiceUrl) { # We sign if and only if the SigningServiceUrl environment variable is set. 57 | $script:AssemblySigningEnabled = $True 58 | Write-Host 'Assembly signing enabled' 59 | } else { 60 | $script:AssemblySigningEnabled = $False 61 | Write-Host 'Assembly signing disabled (SigningServiceUrl environment variable is not set)' 62 | } 63 | } 64 | 65 | function Get-ReleaseNotes { 66 | $ReleaseNotesPath = "$RepositoryRoot\release-notes.md" | Resolve-Path 67 | $Lines = [System.IO.File]::ReadAllLines($ReleaseNotesPath, [System.Text.Encoding]::UTF8) 68 | $Result = @() 69 | $Version = $Null 70 | $Lines | ForEach-Object { 71 | $Line = $_.Trim() 72 | if (-not $Version) { 73 | $Match = [regex]::Match($Line, '[0-9]+\.[0-9]+\.[0-9]+') 74 | if ($Match.Success) { 75 | $Version = $Match.Value 76 | } 77 | } 78 | if ($Version) { 79 | $Result += $Line 80 | } 81 | } 82 | if (-not $Version) { 83 | throw "Failed to parse release notes: $ReleaseNotesPath" 84 | } 85 | return @{ 86 | Content = $Result -join [System.Environment]::NewLine 87 | Version = [version] $Version 88 | } 89 | } 90 | 91 | function Get-BranchName { 92 | # If the branch name is specified via an environment variable (i.e. on TeamCity), use it. 93 | if ($env:BRANCH_NAME) { 94 | return $env:BRANCH_NAME 95 | } 96 | 97 | # If the .git folder is present, try to get the current branch using Git. 98 | $DotGitDirPath = "$RepositoryRoot\.git" 99 | if (Test-Path $DotGitDirPath) { 100 | Add-Type -Path ("$PsScriptRoot\packages\GitSharp\lib\GitSharp.dll" | Resolve-Path) 101 | Add-Type -Path ("$PsScriptRoot\packages\SharpZipLib\lib\20\ICSharpCode.SharpZipLib.dll" | Resolve-Path) 102 | Add-Type -Path ("$PsScriptRoot\packages\Tamir.SharpSSH\lib\Tamir.SharpSSH.dll" | Resolve-Path) 103 | Add-Type -Path ("$PsScriptRoot\packages\Winterdom.IO.FileMap\lib\Winterdom.IO.FileMap.dll" | Resolve-Path) 104 | 105 | $Repository = New-Object 'GitSharp.Repository' $DotGitDirPath 106 | return $Repository.CurrentBranch.Name 107 | } 108 | 109 | # Otherwise, assume 'master' 110 | Write-Warning "Unable to determine the current branch name using either git or the BRANCH_NAME environment variable. Defaulting to 'master'." 111 | return 'master' 112 | } 113 | 114 | 115 | # Clean task, deletes all build output folders. 116 | task Clean { 117 | Write-Info 'Cleaning build output' 118 | 119 | Get-ChildItem $RepositoryRoot -Exclude @('packages') -Include @('dist', 'bin', 'obj') -Directory -Recurse | ForEach-Object { 120 | Write-Host "Deleting $_" 121 | Remove-Item $_ -Force -Recurse 122 | } 123 | } 124 | 125 | 126 | # RestorePackages task, restores all the NuGet packages. 127 | task RestorePackages { 128 | Write-Info "Restoring NuGet packages for solution $SolutionPath" 129 | 130 | & $NuGetPath @('restore', $SolutionPath) 131 | } 132 | 133 | 134 | # UpdateAssemblyInfo task, updates the AssemblyVersion, AssemblyFileVersion and AssemblyInformationlVersion attributes in the source code. 135 | task UpdateAssemblyInfo Init, { 136 | Write-Info 'Updating assembly information' 137 | 138 | "$RepositoryRoot\SolutionInfo.cs" | Resolve-Path | Update-AssemblyVersion -Version $SemanticVersion -InformationalVersion $NuGetPackageVersion 139 | } 140 | 141 | 142 | # Compile task, runs MSBuild to build the solution. 143 | task Compile UpdateAssemblyInfo, RestorePackages, { 144 | Write-Info "Compiling solution $SolutionPath" 145 | 146 | $MSBuildPath = Resolve-MSBuild -MinimumVersion 16.0 147 | $Parameters = @( 148 | $SolutionPath, 149 | '/nodeReuse:False', 150 | '/target:Build', 151 | "/property:Configuration=$Configuration" 152 | ) 153 | exec { 154 | & $MSBuildPath $Parameters 155 | } 156 | } 157 | 158 | # Create a forced 32-bit version of the tool. 159 | task CorFlags Compile, { 160 | Write-Info 'Using CorFlags.exe to create a 32-bit forced version of XmlDoc2CmdletDoc.exe' 161 | 162 | copy -Force "$RepositoryRoot\XmlDoc2CmdletDoc\bin\$Configuration\XmlDoc2CmdletDoc.exe" "$RepositoryRoot\XmlDoc2CmdletDoc\bin\$Configuration\XmlDoc2CmdletDoc32.exe" 163 | 164 | $CorFlagsPath = Get-CorFlagsPath 165 | Write-Host "CorFlagsPath = $CorFlagsPath" 166 | $Parameters = @( 167 | "$RepositoryRoot\XmlDoc2CmdletDoc\bin\$Configuration\XmlDoc2CmdletDoc32.exe" 168 | '/32BITREQ+' 169 | ) 170 | 171 | exec { 172 | & $CorFlagsPath $Parameters 173 | } 174 | } 175 | 176 | function Get-CorFlagsPath { 177 | $ProgramFiles = ${env:ProgramFiles(x86)} 178 | $ProgramFiles = if ($ProgramFiles) { $ProgramFiles } else { $env:ProgramFiles } 179 | $Root = "$ProgramFiles\Microsoft SDKs\Windows" 180 | if (-not (Test-Path $Root)) { throw "Path not found: $Root" } 181 | 182 | $Files = $Root | 183 | Get-ChildItem -File -Recurse -Filter CorFlags.exe | 184 | Sort-Object -Descending { $_.VersionInfo.ProductVersion } 185 | if ($Files.Count -eq 0) { throw 'Failed to locate CorFlags.exe' } 186 | 187 | return $Files[0].FullName 188 | } 189 | 190 | 191 | # Sign the files (note that this is signing, not strong-naming) 192 | task Sign CorFlags, { 193 | if (-not $AssemblySigningEnabled) { 194 | Write-Info 'Skipping assembly signing' 195 | } else { 196 | Write-Info 'Signing Redgate assemblies' 197 | 198 | "$RepositoryRoot\XmlDoc2CmdletDoc\bin\$Configuration" | 199 | Get-ChildItem -File | 200 | Where-Object { $_.Extension -eq '.dll' -or $_.Extension -eq '.exe'} | 201 | ForEach-Object { 202 | $_.FullName | Invoke-SigningService 203 | } 204 | } 205 | } 206 | 207 | 208 | # Test task, runs the NUnit tests. 209 | task Test Sign, { 210 | Write-Info 'Running tests' 211 | 212 | $AssemblyPath = "$RepositoryRoot\XmlDoc2CmdletDoc.Tests\bin\$Configuration\XmlDoc2CmdletDoc.Tests.dll" | Resolve-Path 213 | Invoke-NUnitForAssembly -AssemblyPath $AssemblyPath ` 214 | -NUnitVersion '2.6.3' ` 215 | -FrameworkVersion 'net-4.5' 216 | } 217 | 218 | # Package task, create the NuGet package. 219 | task Package Test, { 220 | Write-Info 'Generating NuGet package' 221 | 222 | # Make sure the output folder exists. 223 | if (-not (Test-Path $DistPath)) { 224 | $Null = mkdir $DistPath 225 | } 226 | 227 | # Run NuGet pack. 228 | $NuSpecPath = "$RepositoryRoot\XmlDoc2CmdletDoc\XmlDoc2CmdletDoc.nuspec" | Resolve-Path 229 | $Parameters = @( 230 | 'pack', 231 | "$NuSpecPath", 232 | '-Version', $NuGetPackageVersion, 233 | '-OutputDirectory', $DistPath, 234 | '-Properties', "configuration=$Configuration;releaseNotes=$ReleaseNotes" 235 | ) 236 | & $NuGetPath $Parameters 237 | } 238 | 239 | 240 | task Build Package 241 | task Rebuild Clean, Build 242 | task Default Build -------------------------------------------------------------------------------- /.build/packages.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | *.sln merge=union 7 | *.csproj merge=union 8 | *.vbproj merge=union 9 | *.fsproj merge=union 10 | *.dbproj merge=union 11 | 12 | # Standard to msysgit 13 | *.doc diff=astextplain 14 | *.DOC diff=astextplain 15 | *.docx diff=astextplain 16 | *.DOCX diff=astextplain 17 | *.dot diff=astextplain 18 | *.DOT diff=astextplain 19 | *.pdf diff=astextplain 20 | *.PDF diff=astextplain 21 | *.rtf diff=astextplain 22 | *.RTF diff=astextplain 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Build results 2 | [Bb]in/ 3 | [Oo]bj/ 4 | [Dd]ebug/ 5 | [Rr]elease/ 6 | publish 7 | /source/Build 8 | 9 | # Cache files 10 | *.cache 11 | 12 | # Visual Studio temporary files 13 | *.aps 14 | *.bsc 15 | *.cspkg 16 | *.dep 17 | *.docstates 18 | *.exp 19 | *.idb 20 | *.ilk 21 | *.ncb 22 | *.obj 23 | *.opt 24 | *.pch 25 | *.pdb 26 | *.plg 27 | *.res 28 | *.sbr 29 | *.suo 30 | *.user 31 | *.vbw 32 | *.vs10x 33 | 34 | # Visual Studio bin deployment 35 | _bin_deployable[Aa]ssemblies/ 36 | 37 | # Unwanted log files 38 | *.log 39 | 40 | # ReSharper 41 | _ReSharper*/ 42 | *.resharper* 43 | 44 | # Rider 45 | .idea/ 46 | 47 | # NCrunch 48 | _[Nn][Cc]runch*/ 49 | 50 | # Other 51 | ~$* 52 | *.Tmp 53 | 54 | # Project-specific excluded content 55 | /.vs 56 | /packages 57 | /.build/packages 58 | /.build/nuget.exe 59 | /dist 60 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | License: New BSD License 2 | Copyright (c) 2014-2020, Red Gate Software Ltd and other contributors. 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 8 | 9 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 10 | 11 | * Neither the name of Red Gate Software Limited nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /LocalNuGetPackageSource/README.md: -------------------------------------------------------------------------------- 1 | # Local NuGet package source 2 | 3 | This folder contains referenced packages that aren't available from the public NuGet feed. These packages are: 4 | 5 | - **RedGate.ThirdParty.Jolt.Core** A fork of the core library from [Jolt.NET](https://jolt.codeplex.com/). That library is no longer actively maintained, and a fix was required for [this issue](https://jolt.codeplex.com/workitem/8161). The source code can be found [here](https://github.com/red-gate/JoltNet-core). -------------------------------------------------------------------------------- /LocalNuGetPackageSource/RedGate.ThirdParty.Jolt.Core.0.4.0.1.nupkg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/red-gate/XmlDoc2CmdletDoc/818295e99ead622052bd8bad692add4ba1bc7312/LocalNuGetPackageSource/RedGate.ThirdParty.Jolt.Core.0.4.0.1.nupkg -------------------------------------------------------------------------------- /NuGet.Config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # XmlDoc2CmdletDoc 2 | 3 | It's easy to write good help documentation for PowerShell *script* modules (those written in the PowerShell script language). You just write specially formatted comments alongside the source code for your cmdlets, and the PowerShell host automatically uses those comments to provide good inline help for your cmdlets' users. **XmlDoc2CmdletDoc** brings this same functionality to PowerShell *binary* modules (those written in C# or VB.NET). You no longer need to use *CmdletHelpEditor* or *PowerShell Cmdlet Help Editor* to manually edit a separate help file. Instead, this tool will automatically generate your PowerShell module's help file from XML Doc comments in your source code. 4 | 5 | For more details, [Michael Sorens](https://www.simple-talk.com/author/michael-sorens/) has written a [comprehensive guide to documenting your PowerShell binary cmdlets](https://www.simple-talk.com/dotnet/software-tools/documenting-your-powershell-binary-cmdlets/) using XmlDoc2CmdletDoc. 6 | 7 | To create a .dll-Help.xml file for your binary PowerShell module: 8 | 9 | 1. Ensure that your project is configured to generate an XML Documentation file alongside its output assembly. 10 | 2. Install the XmlDoc2CmdletDoc NuGet package into your project. 11 | 12 | Optionally, you can enable strict mode to cause a build error if any of your cmdlets are missing required documentation elements. To do so, add the following property to an appropriate `PropertyGroup` element in your project file: 13 | 14 | ```xml 15 | true 16 | ``` 17 | 18 | 19 | # Examples 20 | 21 | Here are some examples of how to document your cmdlets: 22 | 23 | 24 | ## Cmdlet synopsis and description 25 | 26 | The cmdlet's synopsis and description are defined using `` elements in the cmdlet class's XML doc comment. Tag the `` elements with a `type="synopsis"` or `type="description"` attribute, showing whether `` is part of the synopsis or description. 27 | 28 | You can use multiple `` elements for both the synopsis and the description, but a cmdlet synopsis is usually just one sentence. 29 | 30 | ```c# 31 | /// 32 | /// This is the cmdlet synopsis. 33 | /// This is part of the longer cmdlet description. 34 | /// This is also part of the longer cmdlet description. 35 | /// 36 | [Cmdlet("Test", "MyExample")] 37 | public class TestMyExampleCommand : Cmdlet 38 | { 39 | ... 40 | } 41 | ``` 42 | 43 | For guidance on writing the cmdlet synopsis, see http://msdn.microsoft.com/en-us/library/bb525429.aspx. 44 | For guidance on writing the cmdlet description, see http://msdn.microsoft.com/en-us/library/bb736332.aspx. 45 | 46 | 47 | ## Parameter description 48 | 49 | The description for a cmdlet parameter is defined using `` elements in the XML doc comment for the parameter's field or property. Tag the `` elements with a `type="description"` attribute. 50 | 51 | ```c# 52 | [Cmdlet("Test", "MyExample")] 53 | public class TestMyExampleCommand : Cmdlet 54 | { 55 | /// 56 | /// This is part of the parameter description. 57 | /// This is also part of the parameter description. 58 | /// 59 | [Parameter] 60 | public string MyParameter {get; set;} 61 | 62 | ... 63 | } 64 | 65 | ``` 66 | 67 | For guidance on writing the parameter description, see http://msdn.microsoft.com/en-us/library/bb736339.aspx. 68 | 69 | 70 | ## Type description 71 | 72 | You can document a parameter's input type or a cmdlet's output type, using `` elements in the type's XML doc comment. As before, tag the `` elements with a `type="description"` attribute. 73 | 74 | You can only document types defined in the PowerShell module like this. 75 | 76 | ```c# 77 | [Cmdlet("Test", "MyExample")] 78 | public class TestMyExampleCommand : Cmdlet 79 | { 80 | [Parameter] 81 | public MyType MyParameter {get; set;} 82 | 83 | ... 84 | } 85 | 86 | /// 87 | /// This is part of the type description. 88 | /// This is also part of the type description. 89 | /// 90 | public class MyType 91 | { 92 | ... 93 | } 94 | ``` 95 | 96 | 97 | ## Notes 98 | 99 | You can add notes to a cmdlet's help section using a `` element with a `type="alertSet"` attribute. Each `` sub-element corresponds to a single note. 100 | 101 | Inside each `` element, specify the note's title with the `` sub-element, and the note's body text with the `` sub-element. The `` element can directly contain the note's body text, or you can split the note's body text into multiple paragraphs, using `` elements. 102 | 103 | ```c# 104 | /// 105 | /// 106 | /// First note title 107 | /// 108 | /// This is the entire body text for the first note. 109 | /// 110 | /// 111 | /// 112 | /// Second note title 113 | /// 114 | /// The first paragraph of the body text for the second note. 115 | /// The second paragraph of the body text for the second note. 116 | /// 117 | /// 118 | /// 119 | [Cmdlet("Test", "MyExample")] 120 | public class TestMyExampleCommand : Cmdlet 121 | { 122 | ... 123 | } 124 | ``` 125 | 126 | For guidance on writing cmdlet notes, see http://msdn.microsoft.com/en-us/library/bb736330.aspx. 127 | 128 | 129 | ## Examples 130 | 131 | Cmdlet examples are defined using `` elements in the XML doc comment for the cmdlet class. 132 | 133 | The example's code body is taken from the `` element. Any `` elements before the `` element become the example's introduction. Any `` elements after the `` element become the example's remarks. The introduction and remarks are both optional. 134 | 135 | To add multiple cmdlet examples, use multiple `` elements. 136 | 137 | ```c# 138 | /// 139 | /// This is part of the example's introduction. 140 | /// This is also part of the example's introduction. 141 | /// Test-MyExample | Wrte-Host 142 | /// This is part of the example's remarks. 143 | /// This is also part of the example's remarks. 144 | /// 145 | [Cmdlet("Test", "MyExample")] 146 | public class TestMyExampleCommand : Cmdlet 147 | { 148 | ... 149 | } 150 | ``` 151 | 152 | For guidance on writing cmdlet examples, see http://msdn.microsoft.com/en-us/library/bb736335.aspx. 153 | 154 | 155 | ## Related links 156 | 157 | Related links are defined using `` elements in the XML doc comment for the cmdlet class. Tag the relevant `` elements with a `type="link"` attribute. The link text for each navigation link is taken from the body of the `` element. If you want to include a uri, specify a uri attribute in the `` element. 158 | 159 | ```c# 160 | /// 161 | /// This is the text of the first link. 162 | /// This is the text of the second link. 163 | /// The XmlDoc2CmdletDoc website. 164 | /// 165 | [Cmdlet("Test", "MyExample")] 166 | public class TestMyExampleCommand : Cmdlet 167 | { 168 | ... 169 | } 170 | ``` 171 | 172 | For guidance on writing related links, see http://msdn.microsoft.com/en-us/library/bb736334.aspx. 173 | 174 | # Developer notes 175 | 176 | XmlDoc2CmdletDoc has a handful of NuGet package dependencies that aren't yet available from the official public NuGet repository. Instead, they are included in a local file-based package source in the `LocalNuGetPackageSource` folder. 177 | 178 | 1. **RedGate.ThirdParty.JoltCore** - A fork of the [Jolt.NET productivity libraries](http://jolt.codeplex.com/), with some modifications. The [source is publicly available](https://github.com/red-gate/JoltNet-core) under the same [BSD licence](https://github.com/red-gate/JoltNet-core/blob/master/LICENSE) as the original library. 179 | 180 | 2. **RedGate.Build** - A PowerShell module that contains cmdlets used by this project's build scripts. The [source is publicly available](https://github.com/red-gate/RedGate.Build) under [version 2.0 of the Apache license](https://github.com/red-gate/RedGate.Build/blob/master/LICENSE.md). 181 | 182 | XmlDoc2CmdletDoc itself is available under the [3-clause BSD license](https://github.com/red-gate/XmlDoc2CmdletDoc/blob/master/LICENSE.md). 183 | 184 | ## Building XmlDoc2CmdletDoc 185 | 186 | Prerequisites: 187 | - Microsoft Visual Studio 2015 or Microsoft Build Tools 2015 188 | - PowerShell 4.0 or later. 189 | 190 | To build XmlDoc2CmdletDoc, simply invoke `.\build.ps1` from a PowerShell prompt. This will generate a NuGet package in the `dist` folder. If you'd like direct access to the `XmlDoc2CmdletDoc.exe`, it can be found in `XmlDoc2CmdletDoc\bin\Debug` or `XmlDoc2CmdletDoc\bin\Release`, depending on the configuration you build. 191 | 192 | # Contributors 193 | 194 | - [Chris Lambrou](https://github.com/chrislambrou) (Redgate) 195 | - [Michael Sorens](https://github.com/msorens) 196 | - [Mirosław Rypuła](https://github.com/rymir75) 197 | - [art-bel](https://github.com/art-bel) 198 | - [Bryan Dunn](https://github.com/VonOgre) 199 | - [Hamish Blake](https://github.com/hsimah) 200 | - [lordmilko](https://github.com/lordmilko) 201 | - [Bryan Dunn](https://github.com/VonOgre) 202 | -------------------------------------------------------------------------------- /SolutionInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.InteropServices; 3 | 4 | #if DEBUG 5 | [assembly: AssemblyConfiguration("Debug")] 6 | #else 7 | [assembly: AssemblyConfiguration("Release")] 8 | #endif 9 | 10 | [assembly: AssemblyCompany("Redgate")] 11 | [assembly: AssemblyProduct("XmlDoc2CmdletDoc")] 12 | [assembly: AssemblyCopyright("Copyright © 2014-2020, Red Gate Software Ltd and other contributors.")] 13 | 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | [assembly: ComVisible(false)] 18 | 19 | // The following attributes are automatically set by the build scripts. Please do not modify them manually. 20 | // Instead, change the value defined in version-number.txt. 21 | [assembly: AssemblyVersion("0.3.0")] 22 | [assembly: AssemblyFileVersion("0.3.0")] 23 | [assembly: AssemblyInformationalVersion("0.3.0")] 24 | -------------------------------------------------------------------------------- /XmlDoc2CmdletDoc.Core/Comments/CachingCommentReader.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Reflection; 4 | using System.Xml.Linq; 5 | 6 | namespace XmlDoc2CmdletDoc.Core.Comments 7 | { 8 | /// 9 | /// Implementation of that decorates a proxy instance by caching comment lookups. 10 | /// 11 | public class CachingCommentReader : ICommentReader 12 | { 13 | private readonly ICommentReader _proxy; 14 | private readonly IDictionary _cache; 15 | 16 | /// 17 | /// Creates a new instances that delegates to the specified . 18 | /// 19 | /// The decorated comment reader. 20 | public CachingCommentReader(ICommentReader proxy) 21 | { 22 | _proxy = proxy ?? throw new ArgumentNullException(nameof(proxy)); 23 | _cache = new Dictionary(); 24 | } 25 | 26 | #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member 27 | public XElement GetComments(Type type) 28 | { 29 | return _cache.TryGetValue(type, out XElement element) ? element : _cache[type] = _proxy.GetComments(type); 30 | } 31 | 32 | public XElement GetComments(FieldInfo fieldInfo) 33 | { 34 | return _cache.TryGetValue(fieldInfo, out XElement element) ? element : _cache[fieldInfo] = _proxy.GetComments(fieldInfo); 35 | } 36 | 37 | public XElement GetComments(PropertyInfo propertyInfo) 38 | { 39 | return _cache.TryGetValue(propertyInfo, out XElement element) ? element : _cache[propertyInfo] = _proxy.GetComments(propertyInfo); 40 | } 41 | #pragma warning restore CS1591 // Missing XML comment for publicly visible type or member 42 | } 43 | } -------------------------------------------------------------------------------- /XmlDoc2CmdletDoc.Core/Comments/ICommentReader.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | using System.Xml.Linq; 4 | 5 | namespace XmlDoc2CmdletDoc.Core.Comments 6 | { 7 | /// 8 | /// Abstracts the mechanism for retrieving the XML Doc comments for types, fields and properties. 9 | /// 10 | public interface ICommentReader 11 | { 12 | /// 13 | /// Retrieves the XML Doc comment for a type. 14 | /// 15 | /// The type. 16 | /// The type's XML Doc comment, or null if the type doesn't have a comment. 17 | XElement GetComments(Type type); 18 | 19 | /// 20 | /// Retrieves the XML Doc comment for a field. 21 | /// 22 | /// The field. 23 | /// The field's XML Doc comment, or null if the field doesn't have a comment. 24 | XElement GetComments(FieldInfo fieldInfo); 25 | 26 | /// 27 | /// Retrieves the XML Doc comment for a property. 28 | /// 29 | /// The property. 30 | /// The property's XML Doc comment, or null if the property doesn't have a comment. 31 | XElement GetComments(PropertyInfo propertyInfo); 32 | } 33 | } -------------------------------------------------------------------------------- /XmlDoc2CmdletDoc.Core/Comments/JoltCommentReader.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | using System.Xml.Linq; 4 | using Jolt; 5 | 6 | namespace XmlDoc2CmdletDoc.Core.Comments 7 | { 8 | /// 9 | /// Implementation of that's based on an instance 10 | /// of from the Jolt.Net library. 11 | /// 12 | public class JoltCommentReader : ICommentReader 13 | { 14 | private readonly XmlDocCommentReader _proxy; 15 | 16 | /// 17 | /// Creates a new instances that reads comments from the specified XML Doc comments file. 18 | /// 19 | /// The full path of the XML Doc comments file. 20 | public JoltCommentReader(string docCommentsFullPath) 21 | { 22 | if (docCommentsFullPath == null) throw new ArgumentNullException(nameof(docCommentsFullPath)); 23 | _proxy = new XmlDocCommentReader(docCommentsFullPath); 24 | } 25 | 26 | /// 27 | public XElement GetComments(Type type) => _proxy.GetComments(type); 28 | 29 | /// 30 | public XElement GetComments(FieldInfo fieldInfo) => _proxy.GetComments(fieldInfo); 31 | 32 | /// 33 | public XElement GetComments(PropertyInfo propertyInfo) => _proxy.GetComments(propertyInfo); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /XmlDoc2CmdletDoc.Core/Comments/LoggingCommentReader.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | using System.Xml.Linq; 4 | 5 | namespace XmlDoc2CmdletDoc.Core.Comments 6 | { 7 | /// 8 | /// implementation that decorates an existing instance, reporting a warning whenever a comment lookup occurs and an XML Doc comment 9 | /// cannot be found. 10 | /// 11 | public class LoggingCommentReader : ICommentReader 12 | { 13 | private readonly ICommentReader _proxy; 14 | private readonly ReportWarning _reportWarning; 15 | 16 | /// 17 | /// Creates a new instance that decorates the specified . 18 | /// 19 | /// The decorated proxy. 20 | /// Used to report failed comment lookups. 21 | public LoggingCommentReader(ICommentReader proxy, ReportWarning reportWarning) 22 | { 23 | _proxy = proxy ?? throw new ArgumentNullException(nameof(proxy)); 24 | _reportWarning = reportWarning ?? throw new ArgumentNullException(nameof(reportWarning)); 25 | } 26 | 27 | #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member 28 | public XElement GetComments(Type type) => CheckComment(_proxy.GetComments(type), type); 29 | public XElement GetComments(FieldInfo fieldInfo) => CheckComment(_proxy.GetComments(fieldInfo), fieldInfo); 30 | public XElement GetComments(PropertyInfo propertyInfo) => CheckComment(_proxy.GetComments(propertyInfo), propertyInfo); 31 | #pragma warning restore CS1591 // Missing XML comment for publicly visible type or member 32 | 33 | private XElement CheckComment(XElement commentElement, MemberInfo memberInfo) 34 | { 35 | if (commentElement == null) 36 | { 37 | _reportWarning(memberInfo, "No XML doc comment found."); 38 | } 39 | return commentElement; 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /XmlDoc2CmdletDoc.Core/Comments/RewritingCommentReader.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Management.Automation; 5 | using System.Reflection; 6 | using System.Xml.Linq; 7 | 8 | namespace XmlDoc2CmdletDoc.Core.Comments 9 | { 10 | /// 11 | /// Implementation of that decorates an existing reader, typically , 12 | /// and performs simple modifications to the XML Doc comments. Modifications include expanding <see cref="xxxx"> elements. 13 | /// 14 | public class RewritingCommentReader : ICommentReader 15 | { 16 | private readonly ICommentReader _proxy; 17 | 18 | /// 19 | /// Creates a new instance that decorates the specified . 20 | /// 21 | /// The proxy source of comments. 22 | public RewritingCommentReader(ICommentReader proxy) 23 | { 24 | _proxy = proxy ?? throw new ArgumentNullException(nameof(proxy)); 25 | } 26 | 27 | #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member 28 | public XElement GetComments(Type type) => RewriteComment(_proxy.GetComments(type)); 29 | public XElement GetComments(FieldInfo fieldInfo) => RewriteComment(_proxy.GetComments(fieldInfo)); 30 | public XElement GetComments(PropertyInfo propertyInfo) => RewriteComment(_proxy.GetComments(propertyInfo)); 31 | #pragma warning restore CS1591 // Missing XML comment for publicly visible type or member 32 | 33 | private static XElement RewriteComment(XElement element) 34 | { 35 | if (element != null) 36 | { 37 | foreach (var childElement in element.Elements().ToList()) 38 | { 39 | WalkElements(childElement, CollapseSeeElement); 40 | } 41 | } 42 | return element; 43 | } 44 | 45 | private static void WalkElements(XElement element, Func action) 46 | { 47 | var stack = new Stack(); 48 | stack.Push(element); 49 | while (stack.Count > 0) 50 | { 51 | var currentElement = stack.Pop(); 52 | if (action(currentElement)) 53 | { 54 | foreach (var childElement in currentElement.Elements()) 55 | { 56 | stack.Push(childElement); 57 | } 58 | } 59 | } 60 | } 61 | 62 | private static bool CollapseSeeElement(XElement element) 63 | { 64 | if (element.Name.LocalName == "see") 65 | { 66 | var attr = element.Attribute("cref"); 67 | if (attr != null) 68 | { 69 | var text = element.Value; 70 | if (string.IsNullOrWhiteSpace(text)) 71 | { 72 | text = GetTextForCrefValue(attr.Value); 73 | } 74 | if (!string.IsNullOrWhiteSpace(text)) 75 | { 76 | element.ReplaceWith(text); 77 | return false; 78 | } 79 | } 80 | } 81 | return true; 82 | } 83 | 84 | /// 85 | /// Returns a text representation of a reflection item referenced in a <see cref="xxx"/> element. 86 | /// 87 | /// The cref attribute value. 88 | /// A text representation of the referenced item. 89 | private static string GetTextForCrefValue(string cref) 90 | { 91 | // First try to find the corresponding type. 92 | if (cref.StartsWith("T:")) 93 | { 94 | var type = GetType(cref.Substring(2)); 95 | if (type != null) 96 | { 97 | // If the referenced type is actually a cmdlet type, return its cmdlet name rather than its type name. 98 | var cmdletAttribute = GetCustomAttribute(type); 99 | if (cmdletAttribute != null) 100 | { 101 | return $"{cmdletAttribute.VerbName}-{cmdletAttribute.NounName}"; 102 | } 103 | 104 | // Otherwise, return the short name. 105 | return type.Name; 106 | } 107 | } 108 | 109 | // Otherwise, just try to convert the cref value to a short name. 110 | var lastPeriodIndex = cref.LastIndexOf('.'); 111 | return lastPeriodIndex != -1 112 | ? cref.Substring(lastPeriodIndex + 1) 113 | : (cref.Length >= 2 && cref[1] == ':' 114 | ? cref.Substring(2) 115 | : cref); 116 | } 117 | 118 | /// 119 | /// Obtains a having the specified . The type is found by 120 | /// searching the assemblies loaded in the current app domain. 121 | /// 122 | /// The name of the type. 123 | /// The corresponding type, or null if the type could not be found. 124 | private static Type GetType(string typeName) 125 | { 126 | return AppDomain.CurrentDomain.GetAssemblies() 127 | .Select(assembly => assembly.GetType(typeName)) 128 | .FirstOrDefault(type => type != null); 129 | } 130 | 131 | /// 132 | /// Retrieves a single custom attribute from a type. 133 | /// 134 | /// The type of the required custom attribute. 135 | /// The target type. 136 | /// A single custom attribute of type for the target , 137 | /// or null if none could be found. 138 | private static T GetCustomAttribute(Type type) 139 | where T : Attribute 140 | { 141 | return type.GetCustomAttributes(typeof(T)) 142 | .Cast() 143 | .FirstOrDefault(); 144 | } 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /XmlDoc2CmdletDoc.Core/Domain/Command.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Management.Automation; 5 | using System.Reflection; 6 | 7 | namespace XmlDoc2CmdletDoc.Core.Domain 8 | { 9 | /// 10 | /// Represents a single cmdlet. 11 | /// 12 | public class Command 13 | { 14 | private readonly CmdletAttribute _attribute; 15 | 16 | /// 17 | /// Creates a new instance based on the specified cmdlet type. 18 | /// 19 | /// The type of the cmdlet. Must be a sub-class of 20 | /// and have a . 21 | public Command(Type cmdletType) 22 | { 23 | CmdletType = cmdletType ?? throw new ArgumentNullException(nameof(cmdletType)); 24 | _attribute = CmdletType.GetCustomAttribute() ?? throw new ArgumentException("Missing CmdletAttribute", nameof(cmdletType)); 25 | } 26 | 27 | /// 28 | /// The type of the cmdlet for this command. 29 | /// 30 | public readonly Type CmdletType; 31 | 32 | /// 33 | /// The cmdlet verb. 34 | /// 35 | public string Verb => _attribute.VerbName; 36 | 37 | /// 38 | /// The cmdlet noun. 39 | /// 40 | public string Noun => _attribute.NounName; 41 | 42 | /// 43 | /// The cmdlet name, of the form verb-noun. 44 | /// 45 | public string Name => Verb + "-" + Noun; 46 | 47 | /// 48 | /// The output types declared by the command. 49 | /// 50 | public IEnumerable OutputTypes => CmdletType.GetCustomAttributes() 51 | .SelectMany(attr => attr.Type) 52 | .Select(pstype => pstype.Type) 53 | .Distinct() 54 | .OrderBy(type => type.FullName); 55 | 56 | /// 57 | /// The parameters belonging to the command. 58 | /// 59 | public IEnumerable Parameters 60 | { 61 | get 62 | { 63 | IEnumerable parameters = CmdletType.GetMembers(BindingFlags.Instance | BindingFlags.Public) 64 | .Where(member => member.GetCustomAttributes().Any()) 65 | .Select(member => new ReflectionParameter(CmdletType, member)); 66 | if (typeof(IDynamicParameters).IsAssignableFrom(CmdletType)) 67 | { 68 | foreach (var nestedType in CmdletType.GetNestedTypes(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) 69 | { 70 | parameters = parameters.Concat(nestedType.GetMembers(BindingFlags.Instance | BindingFlags.Public) 71 | .Where(member => member.GetCustomAttributes().Any()) 72 | .Select(member => new ReflectionParameter(nestedType, member))); 73 | } 74 | 75 | var cmdlet = (IDynamicParameters) Activator.CreateInstance(CmdletType); 76 | 77 | if (cmdlet.GetDynamicParameters() is RuntimeDefinedParameterDictionary runtimeParamDictionary) 78 | { 79 | parameters = parameters.Concat(runtimeParamDictionary.Where(member => member.Value.Attributes.OfType().Any()) 80 | .Select(member => new RuntimeParameter(CmdletType, member.Value))); 81 | } 82 | } 83 | return parameters.ToList(); 84 | } 85 | } 86 | 87 | /// 88 | /// The command's parameters that belong to the specified parameter set. 89 | /// 90 | /// The name of the parameter set. 91 | /// 92 | /// The command's parameters that belong to the specified parameter set. 93 | /// 94 | public IEnumerable GetParameters(string parameterSetName) => 95 | parameterSetName == ParameterAttribute.AllParameterSets 96 | ? Parameters 97 | : Parameters.Where(p => p.ParameterSetNames.Contains(parameterSetName) || 98 | p.ParameterSetNames.Contains(ParameterAttribute.AllParameterSets)); 99 | 100 | /// 101 | /// The names of the parameter sets that the parameters belongs to. 102 | /// 103 | public IEnumerable ParameterSetNames => 104 | Parameters.SelectMany(p => p.ParameterSetNames) 105 | .Union(new[] {ParameterAttribute.AllParameterSets}) // Parameterless cmdlets need this seeded 106 | .Distinct(); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /XmlDoc2CmdletDoc.Core/Domain/Parameter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Management.Automation; 5 | using System.Reflection; 6 | 7 | namespace XmlDoc2CmdletDoc.Core.Domain 8 | { 9 | /// 10 | /// Represents a single parameter of a cmdlet. 11 | /// 12 | public abstract class Parameter 13 | { 14 | /// 15 | /// The type of the cmdlet this parameter is defined on. 16 | /// 17 | protected readonly Type _cmdletType; 18 | private readonly IEnumerable _attributes; 19 | 20 | /// 21 | /// Creates a new instance. 22 | /// 23 | /// The type of the cmdlet the parameter belongs to. 24 | /// The parameter attributes of the cmdlet. 25 | public Parameter(Type cmdletType, IEnumerable attributes) 26 | { 27 | _cmdletType = cmdletType ?? throw new ArgumentNullException(nameof(cmdletType)); 28 | _attributes = attributes; 29 | } 30 | 31 | /// 32 | /// The name of the parameter. 33 | /// 34 | public abstract string Name { get; } 35 | 36 | /// 37 | /// The type of the parameter. 38 | /// 39 | public abstract Type ParameterType { get; } 40 | 41 | /// 42 | /// The type of this parameter's member - method, constructor, property, and so on. 43 | /// 44 | public abstract MemberTypes MemberType { get; } 45 | 46 | /// 47 | /// Indicates whether or not the parameter supports globbing. 48 | /// 49 | public abstract bool SupportsGlobbing { get; } 50 | 51 | /// 52 | /// The names of the parameter sets that the parameter belongs to. 53 | /// 54 | public IEnumerable ParameterSetNames => _attributes.Select(attr => attr.ParameterSetName); 55 | 56 | private IEnumerable GetAttributes(string parameterSetName) => 57 | parameterSetName == ParameterAttribute.AllParameterSets 58 | ? _attributes 59 | : _attributes.Where(attr => attr.ParameterSetName == parameterSetName || 60 | attr.ParameterSetName == ParameterAttribute.AllParameterSets); 61 | 62 | /// 63 | /// Indicates whether or not the parameter is mandatory. 64 | /// 65 | public bool IsRequired(string parameterSetName) => GetAttributes(parameterSetName).Any(attr => attr.Mandatory); 66 | 67 | /// 68 | /// Indicates whether or not the parameter takes its value from the pipeline input. 69 | /// 70 | public bool IsPipeline(string parameterSetName) => 71 | GetAttributes(parameterSetName).Any(attr => attr.ValueFromPipeline || attr.ValueFromPipelineByPropertyName); 72 | 73 | /// 74 | /// Indicates whether or not the parameter takes its value from the pipeline input. 75 | /// 76 | public string GetIsPipelineAttribute(string parameterSetName) 77 | { 78 | var attributes = GetAttributes(parameterSetName).ToList(); 79 | bool byValue = attributes.Any(attr => attr.ValueFromPipeline); 80 | bool byParameterName = attributes.Any(attr => attr.ValueFromPipelineByPropertyName); 81 | return byValue 82 | ? byParameterName 83 | ? "true (ByValue, ByPropertyName)" 84 | : "true (ByValue)" 85 | : byParameterName 86 | ? "true (ByPropertyName)" 87 | : "false"; 88 | } 89 | 90 | /// 91 | /// The position of the parameter, or null if no position is defined. 92 | /// 93 | public string GetPosition(string parameterSetName) 94 | { 95 | var attribute = GetAttributes(parameterSetName).FirstOrDefault(); 96 | if (attribute == null) return null; 97 | return attribute.Position == int.MinValue ? "named" : Convert.ToString(attribute.Position); 98 | } 99 | 100 | /// 101 | /// The default value of the parameter. This may be obtained by instantiating the cmdlet and accessing the parameter 102 | /// property or field to determine its initial value. 103 | /// 104 | public abstract object GetDefaultValue(ReportWarning reportWarning); 105 | 106 | /// 107 | /// The list of enumerated value names. Returns an empty sequence if there are no enumerated values 108 | /// (normally because the parameter type is not an Enum type). 109 | /// 110 | public IEnumerable EnumValues 111 | { 112 | get 113 | { 114 | if (MemberType == MemberTypes.Property) 115 | { 116 | Type enumType = null; 117 | 118 | if (ParameterType.IsEnum) 119 | enumType = ParameterType; 120 | else 121 | { 122 | foreach (var @interface in ParameterType.GetInterfaces()) 123 | { 124 | if (@interface.IsGenericType && @interface.GetGenericTypeDefinition() == typeof(IEnumerable<>)) 125 | { 126 | var genericArgument = @interface.GetGenericArguments()[0]; 127 | 128 | if (genericArgument.IsEnum) 129 | enumType = genericArgument; 130 | 131 | break; 132 | } 133 | } 134 | } 135 | 136 | if (enumType != null) 137 | { 138 | return enumType 139 | .GetFields(BindingFlags.Public | BindingFlags.Static) 140 | .Select(field => field.Name); 141 | } 142 | } 143 | 144 | return Enumerable.Empty(); 145 | } 146 | } 147 | 148 | /// 149 | /// The list of parameter aliases. 150 | /// 151 | public IEnumerable Aliases 152 | { 153 | get 154 | { 155 | var aliasAttribute = (AliasAttribute)GetCustomAttributes() 156 | .FirstOrDefault(); 157 | return aliasAttribute?.AliasNames ?? new List(); 158 | } 159 | } 160 | 161 | /// 162 | /// Retrieves custom attributes defined on the parameter. 163 | /// 164 | /// The type of attribute to retrieve. 165 | public abstract object[] GetCustomAttributes() where T : Attribute; 166 | } 167 | } -------------------------------------------------------------------------------- /XmlDoc2CmdletDoc.Core/Domain/ReflectionParameter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Management.Automation; 4 | using System.Reflection; 5 | 6 | namespace XmlDoc2CmdletDoc.Core.Domain 7 | { 8 | /// 9 | /// Represents a single parameter of a cmdlet that is identified via reflection. 10 | /// 11 | public class ReflectionParameter : Parameter 12 | { 13 | /// 14 | /// The or that defines the property. 15 | /// 16 | public readonly MemberInfo MemberInfo; 17 | 18 | /// 19 | /// Creates a new instance. 20 | /// 21 | /// The type of the cmdlet the parameter belongs to. 22 | /// The parameter member of the cmdlet. May represent either a field or property. 23 | public ReflectionParameter(Type cmdletType, MemberInfo memberInfo) : base(cmdletType, memberInfo?.GetCustomAttributes()) 24 | { 25 | MemberInfo = memberInfo ?? throw new ArgumentNullException(nameof(memberInfo)); 26 | } 27 | 28 | /// 29 | /// The name of the parameter. 30 | /// 31 | public override string Name => MemberInfo.Name; 32 | 33 | /// 34 | /// The type of the parameter. 35 | /// 36 | public override Type ParameterType 37 | { 38 | get 39 | { 40 | Type GetType(Type type) => Nullable.GetUnderlyingType(type) ?? type; 41 | 42 | switch (MemberInfo.MemberType) 43 | { 44 | case MemberTypes.Property: 45 | return GetType(((PropertyInfo)MemberInfo).PropertyType); 46 | case MemberTypes.Field: 47 | return GetType(((FieldInfo)MemberInfo).FieldType); 48 | default: 49 | throw new NotSupportedException("Unsupported type: " + MemberInfo); 50 | } 51 | } 52 | } 53 | 54 | /// 55 | /// The type of this parameter's member - method, constructor, property, and so on. 56 | /// 57 | public override MemberTypes MemberType => MemberInfo.MemberType; 58 | 59 | /// 60 | public override bool SupportsGlobbing => MemberInfo.GetCustomAttributes(true).Any(); 61 | 62 | /// 63 | /// The default value of the parameter. This is obtained by instantiating the cmdlet and accessing the parameter 64 | /// property or field to determine its initial value. 65 | /// 66 | public override object GetDefaultValue(ReportWarning reportWarning) 67 | { 68 | var cmdlet = Activator.CreateInstance(_cmdletType); 69 | switch (MemberInfo.MemberType) 70 | { 71 | case MemberTypes.Property: 72 | var propertyInfo = ((PropertyInfo)MemberInfo); 73 | if (!propertyInfo.CanRead) 74 | { 75 | reportWarning(MemberInfo, "Parameter does not have a getter. Unable to determine its default value"); 76 | return null; 77 | } 78 | return propertyInfo.GetValue(cmdlet); 79 | case MemberTypes.Field: 80 | return ((FieldInfo)MemberInfo).GetValue(cmdlet); 81 | default: 82 | throw new NotSupportedException("Unsupported type: " + MemberInfo); 83 | } 84 | } 85 | 86 | /// 87 | /// Retrieves custom attributes defined on the parameter. 88 | /// 89 | /// The type of attribute to retrieve. 90 | public override object[] GetCustomAttributes() => MemberInfo.GetCustomAttributes(typeof(T), true); 91 | } 92 | } -------------------------------------------------------------------------------- /XmlDoc2CmdletDoc.Core/Domain/RuntimeParameter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Management.Automation; 4 | using System.Reflection; 5 | 6 | namespace XmlDoc2CmdletDoc.Core.Domain 7 | { 8 | /// 9 | /// Represents a single parameter of a cmdlet that is defined at runtime. 10 | /// 11 | public class RuntimeParameter : Parameter 12 | { 13 | /// 14 | /// The that defines the property. 15 | /// 16 | public readonly RuntimeDefinedParameter RuntimeDefinedParameter; 17 | 18 | /// 19 | /// Creates a new instance. 20 | /// 21 | /// The type of the cmdlet the parameter belongs to. 22 | /// The dynamic runtime parameter member of the cmdlet. 23 | public RuntimeParameter(Type cmdletType, RuntimeDefinedParameter runtimeDefinedParameter) : base(cmdletType, runtimeDefinedParameter?.Attributes.OfType()) 24 | { 25 | RuntimeDefinedParameter = runtimeDefinedParameter ?? throw new ArgumentNullException(nameof(runtimeDefinedParameter)); 26 | } 27 | 28 | /// 29 | /// The name of the parameter. 30 | /// 31 | public override string Name => RuntimeDefinedParameter.Name; 32 | 33 | /// 34 | /// The type of the parameter. 35 | /// 36 | public override Type ParameterType => RuntimeDefinedParameter.ParameterType; 37 | 38 | /// 39 | /// The type of this parameter's member - method, constructor, property, and so on. 40 | /// 41 | public override MemberTypes MemberType => MemberTypes.Property; //RuntimeDefinedParameters are always defined as a Property 42 | 43 | /// 44 | public override bool SupportsGlobbing => RuntimeDefinedParameter.Attributes.OfType().Any(); 45 | 46 | /// 47 | /// The default value of the parameter. Runtime parameters do not support specifying default values. 48 | /// 49 | public override object GetDefaultValue(ReportWarning reportWarning) 50 | { 51 | //RuntimeDefinedParameter objects cannot have a default value. 52 | return null; 53 | } 54 | 55 | /// 56 | /// Retrieves custom attributes defined on the parameter. 57 | /// 58 | /// The type of attribute to retrieve. 59 | public override object[] GetCustomAttributes() => 60 | RuntimeDefinedParameter.Attributes.OfType().Cast().ToArray(); 61 | } 62 | } -------------------------------------------------------------------------------- /XmlDoc2CmdletDoc.Core/Engine.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Management.Automation; 6 | using System.Reflection; 7 | using System.Text; 8 | using System.Xml.Linq; 9 | using XmlDoc2CmdletDoc.Core.Comments; 10 | using XmlDoc2CmdletDoc.Core.Domain; 11 | using XmlDoc2CmdletDoc.Core.Extensions; 12 | 13 | namespace XmlDoc2CmdletDoc.Core 14 | { 15 | /// 16 | /// Delegate used when reporting a warning against a reflected member. 17 | /// 18 | /// The reflected meber to which the warning pertains. 19 | /// The warning message. 20 | public delegate void ReportWarning(MemberInfo target, string warningText); 21 | 22 | /// 23 | /// Does all the work of generating the XML help file for an assembly. See . 24 | /// This class is stateless, so you can call multitple times on multiple threads. 25 | /// 26 | /// 27 | /// A lot of the detailed help generation is also delegated to . 28 | /// This class is generally responsible for generating the overall structure of the XML help file, whilst 29 | /// is resonsible for generating specific items of documentation, 30 | /// such as command synopses, and command and parameter descriptions. 31 | /// 32 | public class Engine 33 | { 34 | private static readonly XNamespace MshNs = XNamespace.Get("http://msh"); 35 | private static readonly XNamespace MamlNs = XNamespace.Get("http://schemas.microsoft.com/maml/2004/10"); 36 | private static readonly XNamespace CommandNs = XNamespace.Get("http://schemas.microsoft.com/maml/dev/command/2004/10"); 37 | private static readonly XNamespace DevNs = XNamespace.Get("http://schemas.microsoft.com/maml/dev/2004/10"); 38 | 39 | /// 40 | /// Public entry point that triggers the creation of the cmdlet XML help file for a single assembly. 41 | /// 42 | /// Defines the locations of the input assembly, the input XML doc comments file for the 43 | /// assembly, and where the cmdlet XML help file should be written to. 44 | /// A code indicating the result of the help generation. 45 | public EngineExitCode GenerateHelp(Options options) 46 | { 47 | try 48 | { 49 | var warnings = new List>(); 50 | ReportWarning reportWarning = (target, warningText) => warnings.Add(Tuple.Create(target, warningText)); 51 | 52 | var assembly = LoadAssembly(options); 53 | var commentReader = LoadComments(options, reportWarning); 54 | var cmdletTypes = GetCommands(assembly); 55 | 56 | 57 | var document = new XDocument(new XDeclaration("1.0", "utf-8", null), 58 | GenerateHelpItemsElement(commentReader, cmdletTypes, reportWarning, options)); 59 | 60 | HandleWarnings(options, warnings, assembly); 61 | 62 | using (var stream = new FileStream(options.OutputHelpFilePath, FileMode.Create, FileAccess.Write, FileShare.None)) 63 | using (var writer = new StreamWriter(stream, Encoding.UTF8)) 64 | { 65 | document.Save(writer); 66 | } 67 | 68 | return EngineExitCode.Success; 69 | } 70 | catch (Exception exception) 71 | { 72 | Console.Error.WriteLine(exception); 73 | if (exception is ReflectionTypeLoadException typeLoadException) 74 | { 75 | foreach (var loaderException in typeLoadException.LoaderExceptions) 76 | { 77 | Console.Error.WriteLine("Loader exception: {0}", loaderException); 78 | } 79 | } 80 | var engineException = exception as EngineException; 81 | return engineException?.ExitCode ?? EngineExitCode.UnhandledException; 82 | } 83 | } 84 | 85 | /// 86 | /// Handles the list of warnings generated once the cmdlet help XML document has been generated. 87 | /// 88 | /// The options. 89 | /// The warnings generated during the creation of the cmdlet help XML document. Each tuple 90 | /// consists of the type to which the warning pertains, and the text of the warning. 91 | /// The assembly of the PowerShell module being documented. 92 | private static void HandleWarnings(Options options, IEnumerable> warnings, Assembly targetAssembly) 93 | { 94 | var groups = warnings.Where(tuple => 95 | { 96 | // Exclude warnings about types outside of the assembly being documented. 97 | var type = tuple.Item1 as Type ?? tuple.Item1.DeclaringType; 98 | return type != null && type.Assembly == targetAssembly; 99 | }) 100 | .GroupBy(tuple => GetFullyQualifiedName(tuple.Item1), tuple => tuple.Item2) 101 | .OrderBy(group => group.Key) 102 | .ToList(); 103 | if (groups.Any()) 104 | { 105 | var writer = options.TreatWarningsAsErrors ? Console.Error : Console.Out; 106 | writer.WriteLine("Warnings:"); 107 | foreach (var group in groups) 108 | { 109 | writer.WriteLine(" {0}:", group.Key); 110 | foreach (var warningText in group.Distinct()) 111 | { 112 | writer.WriteLine(" {0}", warningText); 113 | } 114 | } 115 | if (options.TreatWarningsAsErrors) 116 | { 117 | throw new EngineException(EngineExitCode.WarningsAsErrors, 118 | "Failing due to the occurence of one or more warnings"); 119 | } 120 | } 121 | } 122 | 123 | /// 124 | /// Returns the fully-qualified name of a type, property or field. 125 | /// 126 | /// The member. 127 | /// The fully qualified name of the mamber. 128 | private static string GetFullyQualifiedName(MemberInfo memberInfo) 129 | { 130 | var type = memberInfo as Type; 131 | return type != null 132 | ? type.FullName 133 | : $"{GetFullyQualifiedName(memberInfo.DeclaringType)}.{memberInfo.Name}"; 134 | } 135 | 136 | /// 137 | /// Loads the assembly indicated in the specified . 138 | /// 139 | /// The options. 140 | /// The assembly indicated in the . 141 | private Assembly LoadAssembly(Options options) 142 | { 143 | var assemblyPath = options.AssemblyPath; 144 | if (!File.Exists(assemblyPath)) 145 | { 146 | throw new EngineException(EngineExitCode.AssemblyNotFound, 147 | "Assembly file not found: " + assemblyPath); 148 | } 149 | try 150 | { 151 | var assemblyDir = Path.GetDirectoryName(assemblyPath) ?? ""; 152 | AppDomain.CurrentDomain.AssemblyResolve += // TODO: Really ought to track this handler and cleanly remove it. 153 | (sender, args) => 154 | { 155 | var name = args.Name; 156 | var i = name.IndexOf(','); 157 | if (i != -1) 158 | { 159 | name = name.Substring(0, i); 160 | } 161 | name += ".dll"; 162 | var path = Path.Combine(assemblyDir, name); 163 | return Assembly.LoadFrom(path); 164 | }; 165 | 166 | return Assembly.LoadFile(assemblyPath); 167 | } 168 | catch (Exception exception) 169 | { 170 | throw new EngineException(EngineExitCode.AssemblyLoadError, 171 | "Failed to load assembly from file: " + assemblyPath, 172 | exception); 173 | } 174 | } 175 | 176 | /// 177 | /// Obtains an XML Doc comment reader for the assembly in the specified . 178 | /// 179 | /// The options. 180 | /// Function used to log warnings. 181 | /// A comment reader for the assembly in the . 182 | private ICommentReader LoadComments(Options options, ReportWarning reportWarning) 183 | { 184 | var docCommentsPath = options.DocCommentsPath; 185 | if (!File.Exists(docCommentsPath)) 186 | { 187 | throw new EngineException(EngineExitCode.AssemblyCommentsNotFound, 188 | "Assembly comments file not found: " + docCommentsPath); 189 | } 190 | try 191 | { 192 | return new CachingCommentReader( 193 | new RewritingCommentReader( 194 | new LoggingCommentReader( 195 | new JoltCommentReader(docCommentsPath), reportWarning))); 196 | } 197 | catch (Exception exception) 198 | { 199 | throw new EngineException(EngineExitCode.DocCommentsLoadError, 200 | "Failed to load XML Doc comments from file: " + docCommentsPath, 201 | exception); 202 | } 203 | } 204 | 205 | /// 206 | /// Retrieves a sequence of instances, one for each cmdlet defined in the specified . 207 | /// 208 | /// The assembly. 209 | /// A sequence of commands, one for each cmdlet defined in the . 210 | private static IEnumerable GetCommands(Assembly assembly) 211 | { 212 | return assembly.GetTypes() 213 | .Where(type => type.IsPublic && 214 | typeof(Cmdlet).IsAssignableFrom(type) && 215 | type.GetCustomAttribute() != null) 216 | .Select(type => new Command(type)) 217 | .OrderBy(command => command.Noun) 218 | .ThenBy(command => command.Verb); 219 | } 220 | 221 | /// 222 | /// Generates the root-level <helpItems> element. 223 | /// 224 | /// 225 | /// All of the commands in the module being documented. 226 | /// Function used to log warnings. 227 | /// The XmlDoc2CmdletDoc options. 228 | /// The root-level helpItems element. 229 | private XElement GenerateHelpItemsElement(ICommentReader commentReader, IEnumerable commands, ReportWarning reportWarning, Options options) 230 | { 231 | var helpItemsElement = new XElement(MshNs + "helpItems", new XAttribute("schema", "maml")); 232 | foreach (var command in commands) 233 | { 234 | helpItemsElement.Add(GenerateComment("Cmdlet: " + command.Name)); 235 | helpItemsElement.Add(GenerateCommandElement(commentReader, command, reportWarning, options)); 236 | } 237 | return helpItemsElement; 238 | } 239 | 240 | /// 241 | /// Generates a <command:command> element for the specified command. 242 | /// 243 | /// 244 | /// The command. 245 | /// Function used to log warnings. 246 | /// The XmlDoc2CmdletDoc options. 247 | /// A <command:command> element that represents the . 248 | private XElement GenerateCommandElement(ICommentReader commentReader, Command command, ReportWarning reportWarning, Options options) 249 | { 250 | return new XElement(CommandNs + "command", 251 | new XAttribute(XNamespace.Xmlns + "maml", MamlNs), 252 | new XAttribute(XNamespace.Xmlns + "command", CommandNs), 253 | new XAttribute(XNamespace.Xmlns + "dev", DevNs), 254 | GenerateDetailsElement(commentReader, command, reportWarning), 255 | GenerateDescriptionElement(commentReader, command, reportWarning), 256 | GenerateSyntaxElement(commentReader, command, reportWarning, options.IsExcludedParameterSetName), 257 | GenerateParametersElement(commentReader, command, reportWarning), 258 | GenerateInputTypesElement(commentReader, command, reportWarning), 259 | GenerateReturnValuesElement(commentReader, command, reportWarning), 260 | GenerateAlertSetElement(commentReader, command, reportWarning), 261 | GenerateExamplesElement(commentReader, command, reportWarning), 262 | GenerateRelatedLinksElement(commentReader, command, reportWarning)); 263 | } 264 | 265 | /// 266 | /// Generates the <command:details> element for a command. 267 | /// 268 | /// 269 | /// The command. 270 | /// Function used to log warnings. 271 | /// A <command:details> element for the . 272 | private XElement GenerateDetailsElement(ICommentReader commentReader, Command command, ReportWarning reportWarning) 273 | { 274 | return new XElement(CommandNs + "details", 275 | new XElement(CommandNs + "name", command.Name), 276 | new XElement(CommandNs + "verb", command.Verb), 277 | new XElement(CommandNs + "noun", command.Noun), 278 | commentReader.GetCommandSynopsisElement(command, reportWarning)); 279 | } 280 | 281 | /// 282 | /// Generates the <maml:description> element for a command. 283 | /// 284 | /// 285 | /// The command. 286 | /// Function used to log warnings. 287 | /// A <maml:description> element for the . 288 | private XElement GenerateDescriptionElement(ICommentReader commentReader, Command command, ReportWarning reportWarning) 289 | { 290 | return commentReader.GetCommandDescriptionElement(command, reportWarning); 291 | } 292 | 293 | /// 294 | /// Generates the <command:syntax> element for a command. 295 | /// 296 | /// Provides access to the XML Doc comments. 297 | /// The command. 298 | /// Function used to log warnings. 299 | /// Determines whether or not to exclude a parameter set from the cmdlet XML Help file, based on its name. 300 | /// A <command:syntax> element for the . 301 | private XElement GenerateSyntaxElement(ICommentReader commentReader, Command command, ReportWarning reportWarning, Predicate isExcludedParameterSetName) 302 | { 303 | var syntaxElement = new XElement(CommandNs + "syntax"); 304 | IEnumerable parameterSetNames = command.ParameterSetNames.ToList(); 305 | if (parameterSetNames.Count() > 1) 306 | { 307 | parameterSetNames = parameterSetNames.Where(name => name != ParameterAttribute.AllParameterSets); 308 | } 309 | foreach (var parameterSetName in parameterSetNames.Where(name => !isExcludedParameterSetName(name))) 310 | { 311 | syntaxElement.Add(GenerateComment("Parameter set: " + parameterSetName)); 312 | syntaxElement.Add(GenerateSyntaxItemElement(commentReader, command, parameterSetName, reportWarning)); 313 | } 314 | return syntaxElement; 315 | } 316 | 317 | /// 318 | /// Generates the <command:syntaxItem> element for a specific parameter set of a command. 319 | /// 320 | /// Provides access to the XML Doc comments. 321 | /// The command. 322 | /// The parameter set name. 323 | /// Function used to log warnings. 324 | /// A <command:syntaxItem> element for the specific of the . 325 | private XElement GenerateSyntaxItemElement(ICommentReader commentReader, Command command, string parameterSetName, ReportWarning reportWarning) 326 | { 327 | var syntaxItemElement = new XElement(CommandNs + "syntaxItem", 328 | new XElement(MamlNs + "name", command.Name)); 329 | foreach (var parameter in command.GetParameters(parameterSetName).Where(p => !p.GetCustomAttributes().Any()) 330 | .OrderBy(p => p.GetPosition(parameterSetName)) 331 | .ThenBy(p => p.IsRequired(parameterSetName) ? "0" : "1") 332 | .ThenBy(p => p.Name)) 333 | { 334 | syntaxItemElement.Add(GenerateComment("Parameter: " + parameter.Name)); 335 | syntaxItemElement.Add(GenerateParameterElement(commentReader, parameter, parameterSetName, reportWarning)); 336 | } 337 | return syntaxItemElement; 338 | } 339 | 340 | /// 341 | /// Generates the <command:parameters> element for a command. 342 | /// 343 | /// Provides access to the XML Doc comments. 344 | /// The command. 345 | /// Function used to log warnings. 346 | /// A <command:parameters> element for the . 347 | private XElement GenerateParametersElement(ICommentReader commentReader, Command command, ReportWarning reportWarning) 348 | { 349 | var parametersElement = new XElement(CommandNs + "parameters"); 350 | foreach (var parameter in command.Parameters) 351 | { 352 | parametersElement.Add(GenerateComment("Parameter: " + parameter.Name)); 353 | parametersElement.Add(GenerateParameterElement(commentReader, parameter, ParameterAttribute.AllParameterSets, reportWarning)); 354 | GenerateAliasElements(commentReader, reportWarning, parameter, parametersElement); 355 | } 356 | return parametersElement; 357 | } 358 | 359 | // Because the proper aliases generated in GenerateParameterElement are not manifested by Get-Help, 360 | // this simply duplicates parameters that have aliases, substituting in the alias name. 361 | // Thus, one could do Get-Help xyz -param actualName or Get-Help xyz -param aliasName 362 | private void GenerateAliasElements(ICommentReader commentReader, ReportWarning reportWarning, Parameter parameter, XElement parametersElement) 363 | { 364 | foreach (var alias in parameter.Aliases) 365 | { 366 | var parameterElement = GenerateParameterElement(commentReader, parameter, ParameterAttribute.AllParameterSets, reportWarning); 367 | parametersElement.Add(parameterElement); 368 | var nameElement = (XElement)(parameterElement.Nodes().First(n => ((XElement)n).Name == MamlNs + "name")); 369 | nameElement.Value = alias; 370 | var descriptionElement = (XElement)(parameterElement.Nodes().FirstOrDefault(n => ((XElement)n).Name == MamlNs + "description")); 371 | if (descriptionElement == null) 372 | { 373 | descriptionElement = new XElement(MamlNs + "description"); 374 | parameterElement.Add(descriptionElement); 375 | } 376 | descriptionElement.Add(new XElement(MamlNs + "para", $"This is an alias of the {parameter.Name} parameter.")); 377 | } 378 | } 379 | 380 | /// 381 | /// Generates a <command:parameter> element for a single parameter. 382 | /// 383 | /// Provides access to the XML Doc comments. 384 | /// The parameter. 385 | /// The specific parameter set name, or . 386 | /// Function used to log warnings. 387 | /// A <command:parameter> element for the . 388 | private XElement GenerateParameterElement(ICommentReader commentReader, Parameter parameter, string parameterSetName, ReportWarning reportWarning) 389 | { 390 | var position = parameter.GetPosition(parameterSetName); 391 | 392 | var element = new XElement(CommandNs + "parameter", 393 | new XAttribute("required", parameter.IsRequired(parameterSetName)), 394 | new XAttribute("globbing", parameter.SupportsGlobbing), 395 | new XAttribute("pipelineInput", parameter.GetIsPipelineAttribute(parameterSetName)), 396 | position != null ? new XAttribute("position", position) : null, 397 | new XElement(MamlNs + "name", parameter.Name), 398 | GenerateDescriptionElement(commentReader, parameter, reportWarning), 399 | commentReader.GetParameterValueElement(parameter, reportWarning), 400 | GenerateTypeElement(commentReader, parameter.ParameterType, true, reportWarning), 401 | commentReader.GetParameterDefaultValueElement(parameter, reportWarning), 402 | GetParameterEnumeratedValuesElement(parameter)); 403 | var aliasNames = parameter.Aliases.ToList(); 404 | if (aliasNames.Count > 0) 405 | { 406 | element.Add(new XAttribute("aliases", string.Join(",", aliasNames))); 407 | } 408 | return element; 409 | } 410 | 411 | /// 412 | /// Fetch the description from the ICommentReader. 413 | /// If the parameter is an Enum, add to the description a list of its legal values. 414 | /// 415 | private static XElement GenerateDescriptionElement(ICommentReader commentReader, Parameter parameter, ReportWarning reportWarning) 416 | { 417 | var descriptionElement = commentReader.GetParameterDescriptionElement(parameter, reportWarning); 418 | if (parameter.EnumValues.Any()) 419 | { 420 | if (descriptionElement == null) 421 | { 422 | descriptionElement = new XElement(MamlNs + "description"); 423 | } 424 | descriptionElement.Add( 425 | new XElement(MamlNs + "para", 426 | "Possible values: " + string.Join(", ", parameter.EnumValues))); 427 | } 428 | return descriptionElement; 429 | } 430 | 431 | /// 432 | /// Generates a <command:parameterValueGroup> element for a parameter 433 | /// in order to display enum choices in the cmdlet's syntax section. 434 | /// 435 | private XElement GetParameterEnumeratedValuesElement(Parameter parameter) 436 | { 437 | var enumValues = parameter.EnumValues.ToList(); 438 | if (enumValues.Any()) 439 | { 440 | var parameterValueGroupElement = new XElement(CommandNs + "parameterValueGroup"); 441 | foreach (var enumValue in enumValues) 442 | { 443 | parameterValueGroupElement.Add(GenerateParameterEnumeratedValueElement(enumValue)); 444 | } 445 | return parameterValueGroupElement; 446 | } 447 | return null; 448 | } 449 | 450 | /// 451 | /// Generates a <command:parameterValue> element for a single enum value. 452 | /// 453 | private XElement GenerateParameterEnumeratedValueElement(string enumValue) 454 | { 455 | // These hard-coded attributes were copied from what PowerShell's own core cmdlets use 456 | return new XElement(CommandNs + "parameterValue", 457 | new XAttribute("required", false), 458 | new XAttribute("variableLength", false), 459 | enumValue); 460 | } 461 | 462 | /// 463 | /// Generates the <command:inputTypes> element for a command. 464 | /// 465 | /// Provides access to the XML Doc comments. 466 | /// The command. 467 | /// Function used to log warnings. 468 | /// A <command:inputTypes> element for the . 469 | private XElement GenerateInputTypesElement(ICommentReader commentReader, Command command, ReportWarning reportWarning) 470 | { 471 | var inputTypesElement = new XElement(CommandNs + "inputTypes"); 472 | var pipelineParameters = command.GetParameters(ParameterAttribute.AllParameterSets) 473 | .Where(p => p.IsPipeline(ParameterAttribute.AllParameterSets)); 474 | foreach (var parameter in pipelineParameters) 475 | { 476 | inputTypesElement.Add(GenerateInputTypeElement(commentReader, parameter, reportWarning)); 477 | } 478 | return inputTypesElement; 479 | } 480 | 481 | /// 482 | /// Generates the <command:inputType> element for a pipeline parameter. 483 | /// 484 | /// Provides access to the XML Doc comments. 485 | /// The parameter. 486 | /// Function used to log warnings. 487 | /// A <command:inputType> element for the 's type. 488 | private XElement GenerateInputTypeElement(ICommentReader commentReader, Parameter parameter, ReportWarning reportWarning) 489 | { 490 | var inputTypeDescription = commentReader.GetInputTypeDescriptionElement(parameter, reportWarning); 491 | return new XElement(CommandNs + "inputType", 492 | GenerateTypeElement(commentReader, parameter.ParameterType, inputTypeDescription == null, reportWarning), 493 | inputTypeDescription); 494 | } 495 | 496 | /// 497 | /// Generates the <command:returnValues> element for a command. 498 | /// 499 | /// Provides access to the XML Doc comments. 500 | /// The command. 501 | /// Function used to log warnings. 502 | /// A <command:returnValues> element for the . 503 | private XElement GenerateReturnValuesElement(ICommentReader commentReader, Command command, ReportWarning reportWarning) 504 | { 505 | var returnValueElement = new XElement(CommandNs + "returnValues"); 506 | foreach (var type in command.OutputTypes) 507 | { 508 | returnValueElement.Add(GenerateComment("OutputType: " + (type == typeof(void) ? "None" : type.Name))); 509 | var returnValueDescription = commentReader.GetOutputTypeDescriptionElement(command, type, reportWarning); 510 | returnValueElement.Add(new XElement(CommandNs + "returnValue", 511 | GenerateTypeElement(commentReader, type, returnValueDescription == null, reportWarning), 512 | returnValueDescription)); 513 | } 514 | return returnValueElement; 515 | } 516 | 517 | /// 518 | /// Generates the <maml:alertSet> element for a command. 519 | /// 520 | /// Provides access to the XML Doc comments. 521 | /// The command. 522 | /// Function used to log warnings. 523 | /// A <maml:alertSet> element for the . 524 | private XElement GenerateAlertSetElement(ICommentReader commentReader, Command command, ReportWarning reportWarning) 525 | { 526 | return commentReader.GetCommandAlertSetElement(command, reportWarning); 527 | } 528 | 529 | /// 530 | /// Generates the <command:examples> element for a command. 531 | /// 532 | /// Provides access to the XML Doc comments. 533 | /// The command. 534 | /// Function used to log warnings. 535 | /// A <command:examples> element for the . 536 | private XElement GenerateExamplesElement(ICommentReader commentReader, Command command, ReportWarning reportWarning) 537 | { 538 | return commentReader.GetCommandExamplesElement(command, reportWarning); 539 | } 540 | 541 | /// 542 | /// Generates the <maml:relatedLinks> element for a command. 543 | /// 544 | /// Provides access to the XML Doc comments. 545 | /// The command. 546 | /// Function used to log warnings. 547 | /// A <maml:relatedLinks> element for the . 548 | private XElement GenerateRelatedLinksElement(ICommentReader commentReader, Command command, ReportWarning reportWarning) 549 | { 550 | return commentReader.GetCommandRelatedLinksElement(command, reportWarning); 551 | } 552 | 553 | /// 554 | /// Generates a <dev:type> element for a type. 555 | /// 556 | /// Provides access to the XML Doc comments. 557 | /// The type for which a corresopnding <dev:type> element is required. 558 | /// Indicates whether or not a <maml:description> element should be 559 | /// included for the type. A description can be obtained from the type's XML Doc comment, but it is useful to suppress it if 560 | /// a more context-specific description is available where the <dev:type> element is actually used. 561 | /// Function used to log warnings. 562 | /// A <dev:type> element for the specified . 563 | private XElement GenerateTypeElement(ICommentReader commentReader, Type type, bool includeMamlDescription, ReportWarning reportWarning) 564 | { 565 | return new XElement(DevNs + "type", 566 | new XElement(MamlNs + "name", type == typeof(void) ? "None" : type.FullName), 567 | new XElement(MamlNs + "uri"), 568 | includeMamlDescription ? commentReader.GetTypeDescriptionElement(type, reportWarning) : null); 569 | } 570 | 571 | /// 572 | /// Creates a comment. 573 | /// 574 | /// The text of the comment. 575 | /// An instance based on the specified . 576 | private XComment GenerateComment(string text) { return new XComment($" {text} "); } 577 | } 578 | } 579 | -------------------------------------------------------------------------------- /XmlDoc2CmdletDoc.Core/EngineException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace XmlDoc2CmdletDoc.Core 4 | { 5 | /// 6 | /// Custom exception type raised by the class. 7 | /// 8 | public class EngineException : ApplicationException 9 | { 10 | /// 11 | /// An exit code associated with the error. 12 | /// 13 | public readonly EngineExitCode ExitCode; 14 | 15 | /// 16 | /// Creates a new instance with the specified properties. 17 | /// 18 | /// The exit code associated with the error. 19 | /// An error message associated wih the error. 20 | /// An optional inner exception associated with the error. 21 | public EngineException(EngineExitCode exitCode, string message, Exception innerException = null) 22 | : base(message, innerException) 23 | { 24 | ExitCode = exitCode; 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /XmlDoc2CmdletDoc.Core/EngineExitCode.cs: -------------------------------------------------------------------------------- 1 | namespace XmlDoc2CmdletDoc.Core 2 | { 3 | /// 4 | /// Exit codes for the . 5 | /// 6 | public enum EngineExitCode 7 | { 8 | /// 9 | /// Indicates success. 10 | /// 11 | Success = 0, 12 | 13 | /// 14 | /// The target assembly could not be found. 15 | /// 16 | AssemblyNotFound = 1, 17 | 18 | /// 19 | /// The target assembly could not be loaded. This could indicate that the 20 | /// target assembly is not architecture independent. 21 | /// 22 | AssemblyLoadError = 2, 23 | 24 | /// 25 | /// The XML Doc comments file for the target assembly could not be found. 26 | /// 27 | AssemblyCommentsNotFound = 3, 28 | 29 | /// 30 | /// An error occurred whilst tring to load the target assembly's XML Doc comments file. 31 | /// 32 | DocCommentsLoadError = 4, 33 | 34 | /// 35 | /// An unspecified exception occurred. 36 | /// 37 | UnhandledException = 5, 38 | 39 | /// 40 | /// One or more warnings occurred, and warnings are treated as errors when in strict mode. 41 | /// 42 | WarningsAsErrors = 6, 43 | } 44 | } -------------------------------------------------------------------------------- /XmlDoc2CmdletDoc.Core/Extensions/CommentReaderExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Reflection; 6 | using System.Text.RegularExpressions; 7 | using System.Xml; 8 | using System.Xml.Linq; 9 | using System.Xml.XPath; 10 | using XmlDoc2CmdletDoc.Core.Comments; 11 | using XmlDoc2CmdletDoc.Core.Domain; 12 | 13 | namespace XmlDoc2CmdletDoc.Core.Extensions 14 | { 15 | /// 16 | /// Extension methods for . 17 | /// 18 | public static class CommentReaderExtensions 19 | { 20 | private static readonly XNamespace MshNs = XNamespace.Get("http://msh"); 21 | private static readonly XNamespace MamlNs = XNamespace.Get("http://schemas.microsoft.com/maml/2004/10"); 22 | private static readonly XNamespace CommandNs = XNamespace.Get("http://schemas.microsoft.com/maml/dev/command/2004/10"); 23 | private static readonly XNamespace DevNs = XNamespace.Get("http://schemas.microsoft.com/maml/dev/2004/10"); 24 | 25 | private static readonly IXmlNamespaceResolver Resolver; 26 | 27 | static CommentReaderExtensions() 28 | { 29 | var manager = new XmlNamespaceManager(new NameTable()); 30 | manager.AddNamespace("", MshNs.NamespaceName); 31 | manager.AddNamespace("maml", MamlNs.NamespaceName); 32 | manager.AddNamespace("command", CommandNs.NamespaceName); 33 | manager.AddNamespace("dev", DevNs.NamespaceName); 34 | Resolver = manager; 35 | } 36 | 37 | /// 38 | /// Obtains a <maml:description> element for a cmdlet's synopsis. 39 | /// 40 | /// The comment reader. 41 | /// The command. 42 | /// Used to record warnings. 43 | /// A description element for the cmdlet's synopsis. 44 | public static XElement GetCommandSynopsisElement(this ICommentReader commentReader, Command command, ReportWarning reportWarning) 45 | { 46 | var cmdletType = command.CmdletType; 47 | var commentsElement = commentReader.GetComments(cmdletType); 48 | return GetMamlDescriptionElementFromXmlDocComment(commentsElement, "synopsis", warningText => reportWarning(cmdletType, warningText)); 49 | } 50 | 51 | /// 52 | /// Obtains a <maml:description> element for a cmdlet's full description. 53 | /// 54 | /// The comment reader. 55 | /// The command. 56 | /// Used to record warnings. 57 | /// A description element for the cmdlet's full description. 58 | public static XElement GetCommandDescriptionElement(this ICommentReader commentReader, Command command, ReportWarning reportWarning) 59 | { 60 | var cmdletType = command.CmdletType; 61 | var commentsElement = commentReader.GetComments(cmdletType); 62 | return GetMamlDescriptionElementFromXmlDocComment(commentsElement, "description", warningText => reportWarning(cmdletType, warningText)); 63 | } 64 | 65 | /// 66 | /// Obtains a <command:examples> element for a cmdlet's examples. 67 | /// 68 | /// The comment reader. 69 | /// The command. 70 | /// Used to log any warnings. 71 | /// An examples element for the cmdlet. 72 | public static XElement GetCommandExamplesElement(this ICommentReader commentReader, Command command, ReportWarning reportWarning) 73 | { 74 | var cmdletType = command.CmdletType; 75 | var comments = commentReader.GetComments(cmdletType); 76 | if (comments == null) 77 | { 78 | reportWarning(cmdletType, "No XML doc comment found."); 79 | return null; 80 | } 81 | 82 | var xmlDocExamples = comments.XPathSelectElements("//example").ToList(); 83 | if (!xmlDocExamples.Any()) 84 | { 85 | reportWarning(cmdletType, "No examples found."); 86 | return null; 87 | } 88 | 89 | var examples = new XElement(CommandNs + "examples"); 90 | int exampleNumber = 1; 91 | foreach (var xmlDocExample in xmlDocExamples) 92 | { 93 | var example = GetCommandExampleElement(xmlDocExample, exampleNumber, warningText => reportWarning(cmdletType, warningText)); 94 | if (example != null) 95 | { 96 | examples.Add(example); 97 | exampleNumber++; 98 | } 99 | } 100 | return exampleNumber == 1 ? null : examples; 101 | } 102 | 103 | /// 104 | /// Obtains a <command:example> element based on an <example> XML doc comment element. 105 | /// 106 | /// The XML doc comment example element. 107 | /// The number of the example. 108 | /// Used to log any warnings. 109 | /// An example element. 110 | private static XElement GetCommandExampleElement(XElement exampleElement, int exampleNumber, Action reportWarning) 111 | { 112 | var items = exampleElement.XPathSelectElements("para | code").ToList(); 113 | var intros = items.TakeWhile(x => x.Name == "para").ToList(); 114 | var code = items.SkipWhile(x => x.Name == "para").TakeWhile(x => x.Name == "code").FirstOrDefault(); 115 | var paras = items.SkipWhile(x => x.Name == "para").SkipWhile(x => x.Name == "code").ToList(); 116 | 117 | var example = new XElement(CommandNs + "example", 118 | new XElement(MamlNs + "title", $"---------- EXAMPLE {exampleNumber} ----------")); 119 | 120 | bool isEmpty = true; 121 | if (intros.Any()) 122 | { 123 | var introduction = new XElement(MamlNs + "introduction"); 124 | intros.ForEach(intro => introduction.Add(new XElement(MamlNs + "para", Tidy(intro.Value)))); 125 | example.Add(introduction); 126 | isEmpty = false; 127 | } 128 | if (code != null) 129 | { 130 | example.Add(new XElement(DevNs + "code", TidyCode(code.Value))); 131 | isEmpty = false; 132 | } 133 | if (paras.Any()) 134 | { 135 | var remarks = new XElement(DevNs + "remarks"); 136 | paras.ForEach(para => remarks.Add(new XElement(MamlNs + "para", Tidy(para.Value)))); 137 | example.Add(remarks); 138 | isEmpty = false; 139 | } 140 | 141 | if (isEmpty) 142 | { 143 | reportWarning($"No para or code elements found for example {exampleNumber}."); 144 | } 145 | 146 | return example; 147 | } 148 | 149 | /// 150 | /// Obtains a <command:relatedLinks> element for a cmdlet's related links. 151 | /// 152 | /// The comment reader. 153 | /// The command. 154 | /// Used to log any warnings. 155 | /// An relatedLinks element for the cmdlet. 156 | public static XElement GetCommandRelatedLinksElement(this ICommentReader commentReader, Command command, ReportWarning reportWarning) 157 | { 158 | var cmdletType = command.CmdletType; 159 | var comments = commentReader.GetComments(cmdletType); 160 | if (comments == null) 161 | { 162 | return null; 163 | } 164 | 165 | var paras = comments.XPathSelectElements("//para[@type='link']").ToList(); 166 | if (!paras.Any()) return null; 167 | 168 | var relatedLinks = new XElement(MamlNs + "relatedLinks"); 169 | foreach (var para in paras) 170 | { 171 | var navigationLink = new XElement(MamlNs + "navigationLink", 172 | new XElement(MamlNs + "linkText", para.Value)); 173 | var uri = para.Attribute("uri"); 174 | if (uri != null) 175 | { 176 | navigationLink.Add(new XElement(MamlNs + "uri", uri.Value)); 177 | } 178 | relatedLinks.Add(navigationLink); 179 | } 180 | return relatedLinks; 181 | } 182 | 183 | /// 184 | /// Obtains a <maml:alertSet> element for a cmdlet's notes. 185 | /// 186 | /// The comment reader. 187 | /// The command. 188 | /// Used to log any warnings. 189 | /// A <maml:alertSet> element for the cmdlet's notes. 190 | public static XElement GetCommandAlertSetElement(this ICommentReader commentReader, Command command, ReportWarning reportWarning) 191 | { 192 | var cmdletType = command.CmdletType; 193 | var comments = commentReader.GetComments(cmdletType); 194 | if (comments == null) 195 | { 196 | return null; 197 | } 198 | 199 | // First see if there's an alertSet element in the comments 200 | var alertSet = comments.XPathSelectElement("//maml:alertSet", Resolver); 201 | if (alertSet != null) 202 | { 203 | return alertSet; 204 | } 205 | 206 | // Next, search for a list element of type alertSet. 207 | var list = comments.XPathSelectElement("//list[@type='alertSet']"); 208 | if (list == null) 209 | { 210 | return null; 211 | } 212 | alertSet = new XElement(MamlNs + "alertSet"); 213 | foreach (var item in list.XPathSelectElements("item")) 214 | { 215 | var term = item.XPathSelectElement("term"); 216 | var description = item.XPathSelectElement("description"); 217 | if (term != null && description != null) 218 | { 219 | var alertTitle = new XElement(MamlNs + "title", Tidy(term.Value)); 220 | 221 | var alert = new XElement(MamlNs + "alert"); 222 | var paras = description.XPathSelectElements("para").ToList(); 223 | if (paras.Any()) 224 | { 225 | paras.ForEach(para => alert.Add(new XElement(MamlNs + "para", Tidy(para.Value)))); 226 | } 227 | else 228 | { 229 | alert.Add(new XElement(MamlNs + "para", Tidy(description.Value))); 230 | } 231 | 232 | alertSet.Add(alertTitle, alert); 233 | } 234 | } 235 | return alertSet; 236 | } 237 | 238 | /// 239 | /// Obtains a <maml:description> element for a parameter. 240 | /// 241 | /// The comment reader. 242 | /// The parameter. 243 | /// Used to record warnings. 244 | /// A description element for the parameter. 245 | public static XElement GetParameterDescriptionElement(this ICommentReader commentReader, Parameter parameter, ReportWarning reportWarning) 246 | { 247 | var reflectionParameter = parameter as ReflectionParameter; 248 | 249 | if (reflectionParameter != null) 250 | { 251 | var memberInfo = reflectionParameter.MemberInfo; 252 | var commentsElement = commentReader.GetComments(memberInfo); 253 | return GetMamlDescriptionElementFromXmlDocComment(commentsElement, "description", warningText => reportWarning(memberInfo, warningText)); 254 | } 255 | 256 | //Other parameter types do not support XML comments. 257 | return null; 258 | } 259 | 260 | /// 261 | /// 262 | /// 263 | /// The comment reader. 264 | /// The parameter. 265 | /// Used to record warnings. 266 | /// A description element for the parameter. 267 | public static XElement GetParameterValueElement(this ICommentReader commentReader, Parameter parameter, ReportWarning reportWarning) 268 | { 269 | return new XElement(CommandNs + "parameterValue", 270 | new XAttribute("required", true), 271 | GetSimpleTypeName(parameter.ParameterType)); 272 | } 273 | 274 | /// 275 | /// Obtains a <dev:defaultValue> element for a parameter. 276 | /// 277 | /// The comment reader. 278 | /// The parameter. 279 | /// Used to record warnings. 280 | /// A default value element for the parameter's default value, or null if a default value could not be obtained. 281 | public static XElement GetParameterDefaultValueElement(this ICommentReader commentReader, Parameter parameter, ReportWarning reportWarning) 282 | { 283 | var defaultValue = parameter.GetDefaultValue(reportWarning); // TODO: Get the default value from the doc comments? 284 | if (defaultValue != null) 285 | { 286 | if (defaultValue is IEnumerable enumerable && !(defaultValue is string)) 287 | { 288 | var content = string.Join(", ", enumerable.Cast().Select(element => element.ToString())); 289 | if (content != "") 290 | { 291 | return new XElement(DevNs + "defaultValue", content); 292 | } 293 | } 294 | else 295 | { 296 | return new XElement(DevNs + "defaultValue", defaultValue.ToString()); 297 | } 298 | } 299 | return null; 300 | } 301 | 302 | /// 303 | /// Obtains a <maml:description> for an <command:inputType> coresponding to a specified parameter that accepts pipeline input. 304 | /// 305 | /// The comment reader. 306 | /// The parameter. 307 | /// Used to log any warnings. 308 | /// A <maml:description> for an <command:inputType> for the parameter, or null if no explicit description is available 309 | /// for the input type. 310 | public static XElement GetInputTypeDescriptionElement(this ICommentReader commentReader, Parameter parameter, ReportWarning reportWarning) 311 | { 312 | var reflectionParameter = parameter as ReflectionParameter; 313 | 314 | if (reflectionParameter != null) 315 | { 316 | var parameterMemberInfo = reflectionParameter.MemberInfo; 317 | var commentsElement = commentReader.GetComments(parameterMemberInfo); 318 | 319 | // First try to read the explicit inputType description. 320 | var inputTypeDescription = GetMamlDescriptionElementFromXmlDocComment(commentsElement, "inputType", _ => { }); 321 | if (inputTypeDescription != null) 322 | { 323 | return inputTypeDescription; 324 | } 325 | 326 | // Then fall back to using the parameter description. 327 | var parameterDescription = commentReader.GetParameterDescriptionElement(parameter, reportWarning); 328 | if (parameterDescription == null) 329 | { 330 | reportWarning(parameterMemberInfo, "No inputType comment found and no fallback description comment found."); 331 | } 332 | return parameterDescription; 333 | } 334 | 335 | //Other parameter types do not support XML comments. 336 | return null; 337 | } 338 | 339 | /// 340 | /// Obtains a <maml:description> for a command's output type. 341 | /// 342 | /// The comment reader. 343 | /// The command. 344 | /// The output type of the command. 345 | /// Used to log any warnings. 346 | /// A <maml:description> for the command's output type, 347 | /// or null if no explicit description is available for the output type. 348 | public static XElement GetOutputTypeDescriptionElement(this ICommentReader commentReader, 349 | Command command, 350 | Type outputType, 351 | ReportWarning reportWarning) 352 | { 353 | // TODO: Get the description from the element 354 | return commentReader.GetTypeDescriptionElement(outputType, reportWarning); 355 | } 356 | 357 | /// 358 | /// Obtains a <maml:description> element for a type. 359 | /// 360 | /// The comment reader. 361 | /// The type for which a description is required. 362 | /// Used to log any warnings. 363 | /// A description for the type, or null if no description is available. 364 | public static XElement GetTypeDescriptionElement(this ICommentReader commentReader, Type type, ReportWarning reportWarning) 365 | { 366 | if (type.IsArray) 367 | type = type.GetElementType(); 368 | 369 | var commentsElement = commentReader.GetComments(type); 370 | return GetMamlDescriptionElementFromXmlDocComment(commentsElement, "description", warningText => reportWarning(type, warningText)); 371 | } 372 | 373 | /// 374 | /// Helper method to retrieve the XML doc commments from a that represents either a property or a field. 375 | /// 376 | /// The comment reader. 377 | /// The member whose comments are to be retrieved. 378 | /// The XML doc commments for the , ornull if they are not available. 379 | private static XElement GetComments(this ICommentReader commentReader, MemberInfo memberInfo) 380 | { 381 | switch (memberInfo.MemberType) 382 | { 383 | case MemberTypes.Field: 384 | return commentReader.GetComments((FieldInfo)memberInfo); 385 | 386 | case MemberTypes.Property: 387 | return commentReader.GetComments((PropertyInfo)memberInfo); 388 | 389 | default: 390 | throw new NotSupportedException("Member type not supported: " + memberInfo.MemberType); 391 | } 392 | } 393 | 394 | /// 395 | /// Obtains a <maml:description> from an XML doc comment. 396 | /// 397 | /// The XML doc comments element as retrieved from an . May be null. 398 | /// 399 | /// An identifier used to select specific content from the XML doc comment. 400 | /// The first <maml:description> element with a type="<>" 401 | /// attribute will be used to provide the description (where <> is the value of the 402 | /// parameter). 403 | /// Alternatively, the XML Doc comments may contain multiple <para> elements. Those 404 | /// with the type="<>" attribute will be used to provide content for the description. 405 | /// 406 | /// Used to log any warnings. 407 | /// A description element derived from the XML doc comment, or null if a description could not be obtained. 408 | private static XElement GetMamlDescriptionElementFromXmlDocComment(XElement commentsElement, string typeAttribute, Action reportWarning) 409 | { 410 | if (commentsElement != null) 411 | { 412 | // Examine the XML doc comment first for an embedded element. 413 | var mamlDescriptionElement = commentsElement.XPathSelectElement($".//maml:description[@type='{typeAttribute}']", Resolver); 414 | if (mamlDescriptionElement != null) 415 | { 416 | mamlDescriptionElement = new XElement(mamlDescriptionElement); // Deep clone the element, as we're about to modify it. 417 | mamlDescriptionElement.RemoveAttributes(); // Intended to remove the xmlns:maml namespace declaration and id attribute. Assumes there aren't any further attributes. 418 | return mamlDescriptionElement; 419 | } 420 | 421 | // Next try elements. 422 | var paraElements = commentsElement.XPathSelectElements($".//para[@type='{typeAttribute}']").ToList(); 423 | if (paraElements.Any()) 424 | { 425 | var descriptionElement = new XElement(MamlNs + "description"); 426 | paraElements.ForEach(para => descriptionElement.Add(new XElement(MamlNs + "para", Tidy(para.Value)))); 427 | return descriptionElement; 428 | } 429 | } 430 | 431 | // We've failed to provide a description from the XML doc commment. 432 | reportWarning($"No {typeAttribute} comment found."); 433 | return null; 434 | } 435 | 436 | /// 437 | /// Tidies up the text retrieved from an XML doc comment. Multiple whitespace characters, including CR/LF, 438 | /// are replaced with a single space, and leading and trailing whitespace is removed. 439 | /// 440 | /// The string to tidy. 441 | /// The tidied string. 442 | private static string Tidy(string value) 443 | { 444 | return new Regex(@"\s{2,}").Replace(value, " ").Trim(); 445 | } 446 | 447 | private static string TidyCode(string value) 448 | { 449 | // Split the value into separate lines, and eliminate leading and trailing empty lines. 450 | IEnumerable lines = value.Split(new[] { "\r\n", "\n" }, StringSplitOptions.None) 451 | .SkipWhile(string.IsNullOrWhiteSpace) 452 | .Reverse() 453 | .SkipWhile(string.IsNullOrWhiteSpace) 454 | .Reverse() 455 | .ToList(); 456 | 457 | // If all of the non-empty lines start with leading whitespace, remove it. (i.e. dedent the code). 458 | var pattern = new Regex(@"^\s*"); 459 | var nonEmptyLines = lines.Where(s => !string.IsNullOrWhiteSpace(s)).ToList(); 460 | if (nonEmptyLines.Any()) 461 | { 462 | var shortestPrefixLength = nonEmptyLines.Min(s => pattern.Match(s).Value.Length); 463 | if (shortestPrefixLength > 0) 464 | { 465 | lines = lines.Select(s => s.Length <= shortestPrefixLength ? "" : s.Substring(shortestPrefixLength)); 466 | } 467 | } 468 | 469 | return string.Join(Environment.NewLine, lines); 470 | } 471 | 472 | private static string GetSimpleTypeName(Type type) 473 | { 474 | if (type.IsArray) 475 | { 476 | return GetSimpleTypeName(type.GetElementType()) + "[]"; 477 | } 478 | 479 | if (PredefinedSimpleTypeNames.TryGetValue(type, out string result)) 480 | { 481 | return result; 482 | } 483 | return type.Name; 484 | } 485 | 486 | private static readonly IDictionary PredefinedSimpleTypeNames = 487 | new Dictionary 488 | { 489 | {typeof(object), "object"}, 490 | {typeof(string), "string"}, 491 | {typeof(bool), "bool"}, 492 | {typeof(byte), "byte"}, 493 | {typeof(char), "char"}, 494 | {typeof(short), "short"}, 495 | {typeof(ushort), "ushort"}, 496 | {typeof(int), "int"}, 497 | {typeof(uint), "uint"}, 498 | {typeof(long), "long"}, 499 | {typeof(ulong), "ulong"}, 500 | {typeof(float), "float"}, 501 | {typeof(double), "double"}, 502 | }; 503 | } 504 | } 505 | -------------------------------------------------------------------------------- /XmlDoc2CmdletDoc.Core/Extensions/EnumerableExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace XmlDoc2CmdletDoc.Core.Extensions 6 | { 7 | /// 8 | /// Extension methods for . 9 | /// 10 | public static class EnumerableExtensions 11 | { 12 | /// 13 | /// Returns elements having distinct values of a specified property. 14 | /// 15 | /// The types of the element in the sequence. 16 | /// The type of the property selected from each element. 17 | /// The sequence of elements. 18 | /// The function used to select the property from each item in the sequence. 19 | /// Those items in the sequence which have a unique property. If multiple items share the same property, only the 20 | /// first item is returned. 21 | public static IEnumerable Distinct(this IEnumerable enumerable, Func selector) 22 | { 23 | var hashSet = new HashSet(); 24 | return enumerable.Where(item => hashSet.Add(selector(item))); 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /XmlDoc2CmdletDoc.Core/Options.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | 5 | namespace XmlDoc2CmdletDoc.Core 6 | { 7 | /// 8 | /// Represents the settings neccesary for generating the cmdlet XML help file for a single assembly. 9 | /// 10 | public class Options 11 | { 12 | /// 13 | /// The absolute path of the cmdlet assembly. 14 | /// 15 | public readonly string AssemblyPath; 16 | 17 | /// 18 | /// The absolute path of the output .dll-Help.xml help file. 19 | /// 20 | public readonly string OutputHelpFilePath; 21 | 22 | /// 23 | /// The absolute path of the assembly's XML Doc comments file. 24 | /// 25 | public readonly string DocCommentsPath; 26 | 27 | /// 28 | /// Indicates whether or not the presence of warnings should be treated as a failure condition. 29 | /// 30 | public readonly bool TreatWarningsAsErrors; 31 | 32 | /// 33 | /// A predicate that determines whether a parameter set should be excluded from the 34 | /// output help file, based on its name. This is intended to be used for deprecated parameter sets, 35 | /// to make them less discoverable. 36 | /// 37 | public readonly Predicate IsExcludedParameterSetName; 38 | 39 | /// 40 | /// Creates a new instance with the specified settings. 41 | /// 42 | /// Indicates whether or not the presence of warnings should be treated as a failure condition. 43 | /// The path of the taget assembly whose XML Doc comments file is to be converted 44 | /// into a cmdlet XML Help file. 45 | /// A predicate that determines whether a parameter set should be excluded from the 46 | /// output help file, based on its name. 47 | /// This is intended to be used for deprecated parameter sets, to make them less discoverable. 48 | /// The output path of the cmdlet XML Help file. 49 | /// If null, an appropriate default is selected based on . 50 | /// The path of the XML Doc comments file for the target assembly. 51 | /// If null, an appropriate default is selected based on 52 | public Options( 53 | bool treatWarningsAsErrors, 54 | string assemblyPath, 55 | Predicate isExcludedParameterSetName = null, 56 | string outputHelpFilePath = null, 57 | string docCommentsPath = null) 58 | { 59 | if (assemblyPath == null) throw new ArgumentNullException(nameof(assemblyPath)); 60 | 61 | TreatWarningsAsErrors = treatWarningsAsErrors; 62 | 63 | AssemblyPath = Path.GetFullPath(assemblyPath); 64 | 65 | IsExcludedParameterSetName = isExcludedParameterSetName ?? (_ => false); 66 | 67 | OutputHelpFilePath = outputHelpFilePath == null 68 | ? Path.ChangeExtension(AssemblyPath, "dll-Help.xml") 69 | : Path.GetFullPath(outputHelpFilePath); 70 | 71 | DocCommentsPath = docCommentsPath == null 72 | ? Path.ChangeExtension(AssemblyPath, ".xml") 73 | : Path.GetFullPath(docCommentsPath); 74 | } 75 | 76 | /// 77 | /// Provides a string representation of the options, for logging and debug purposes. 78 | /// 79 | public override string ToString() => $"AssemblyPath: {AssemblyPath}, " + 80 | $"OutputHelpFilePath: {OutputHelpFilePath}, " + 81 | $"TreatWarningsAsErrors {TreatWarningsAsErrors}"; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /XmlDoc2CmdletDoc.Core/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.InteropServices; 3 | 4 | // General Information about an assembly is controlled through the following 5 | // set of attributes. Change these attribute values to modify the information 6 | // associated with an assembly. 7 | [assembly: AssemblyTitle("XmlDoc2CmdletDoc.Core")] 8 | [assembly: AssemblyDescription("")] 9 | 10 | // The following GUID is for the ID of the typelib if this project is exposed to COM 11 | [assembly: Guid("8f997709-e41a-4dc8-87a4-7fcc90279969")] 12 | -------------------------------------------------------------------------------- /XmlDoc2CmdletDoc.Core/XmlDoc2CmdletDoc.Core.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net45 4 | true 5 | false 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | Properties\SolutionInfo.cs 14 | 15 | 16 | -------------------------------------------------------------------------------- /XmlDoc2CmdletDoc.Core/XmlDoc2CmdletDoc.Core.v2.ncrunchproject: -------------------------------------------------------------------------------- 1 | 2 | 1000 3 | false 4 | false 5 | false 6 | true 7 | false 8 | false 9 | false 10 | false 11 | false 12 | true 13 | true 14 | false 15 | true 16 | true 17 | true 18 | 60000 19 | 20 | 21 | 22 | AutoDetect 23 | STA 24 | x86 25 | -------------------------------------------------------------------------------- /XmlDoc2CmdletDoc.TestModule/DefaultValue/TestDefaultValueCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Management.Automation; 4 | 5 | namespace XmlDoc2CmdletDoc.TestModule.DefaultValue 6 | { 7 | /// 8 | /// This is part of the Test-DefaultValue synopsis. 9 | /// This is part of the Test-DefaultValue description. 10 | /// 11 | [Cmdlet(VerbsDiagnostic.Test, "DefaultValue")] 12 | public class TestDefaultValueCommand : Cmdlet 13 | { 14 | /// 15 | /// This has the default value: "default-string-value". 16 | /// 17 | [Parameter] 18 | public string StringParameter = "default-string-value"; 19 | 20 | /// 21 | /// This has the default values: 1, 2, 3. 22 | /// 23 | [Parameter] 24 | public int[] ArrayParameter = {1, 2, 3}; 25 | 26 | /// 27 | /// This has the default values: 1, 2, 3. 28 | /// 29 | [Parameter] 30 | public IList ListParameter = new List {1, 2, 3}; 31 | 32 | /// 33 | /// This has the default values: 1, 2, 3. 34 | /// 35 | [Parameter] 36 | public IEnumerable EnumerableParameter = Enumerable.Range(1, 3); 37 | 38 | /// 39 | /// This has no default value. 40 | /// 41 | [Parameter] 42 | public IEnumerable EmptyEnumerableParameter = Enumerable.Empty(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /XmlDoc2CmdletDoc.TestModule/DynamicParameters/TestNestedTypeDynamicParametersCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Management.Automation; 2 | 3 | namespace XmlDoc2CmdletDoc.TestModule.DynamicParameters 4 | { 5 | /// 6 | /// This is part of the Test-NestedTypeDynamicParameters synopsis. 7 | /// This is part of the Test-NestedTypeDynamicParameters description. 8 | /// 9 | [Cmdlet(VerbsDiagnostic.Test, "NestedTypeDynamicParameters")] 10 | public class TestNestedTypeDynamicParametersCommand : Cmdlet, IDynamicParameters 11 | { 12 | /// 13 | /// This is part of the StaticParam description. 14 | /// 15 | [Parameter] 16 | public string StaticParam { get; set; } 17 | 18 | public object GetDynamicParameters() { return new NestedDynamicParameters(); } 19 | 20 | private class NestedDynamicParameters 21 | { 22 | /// 23 | /// This is part of the DynamicParam description. 24 | /// 25 | [Parameter] 26 | public string DynamicParam { get; set; } 27 | 28 | /// 29 | /// Despite the description attribute, this is not a cmdlet 30 | /// parameter and this text should not appear in the help documentation. 31 | /// 32 | public string IrrelevantProperty { get; set; } 33 | } 34 | 35 | private class UnrelatedClass 36 | { 37 | public string IrrelevantProperty { get; set; } 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /XmlDoc2CmdletDoc.TestModule/DynamicParameters/TestRuntimeDynamicParametersCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.ObjectModel; 3 | using System.Management.Automation; 4 | 5 | namespace XmlDoc2CmdletDoc.TestModule.DynamicParameters 6 | { 7 | /// 8 | /// This is part of the Test-RuntimeDynamicParameters synopsis. 9 | /// This is part of the Test-RuntimeDynamicParameters description. 10 | /// 11 | [Cmdlet(VerbsDiagnostic.Test, "RuntimeDynamicParameters")] 12 | public class TestRuntimeDynamicParametersCommand : Cmdlet, IDynamicParameters 13 | { 14 | /// 15 | /// This is part of the StaticParam description. 16 | /// 17 | [Parameter] 18 | public string StaticParam { get; set; } 19 | 20 | public object GetDynamicParameters() 21 | { 22 | var runtimeParamDictionary = new RuntimeDefinedParameterDictionary(); 23 | 24 | var attributes = new Collection(); 25 | attributes.Add(new ParameterAttribute()); 26 | 27 | //This parameter has a ParameterAttribute and is considered a dynamic parameter. 28 | var dynamicParam = new RuntimeDefinedParameter("DynamicParam", typeof(string), attributes); 29 | 30 | //This parameter is missing a ParameterAttribute. While it is technically still a dynamic parameter, 31 | //it is not considered part of any parameter set and cannot be used. 32 | var irrelevantParam = new RuntimeDefinedParameter("IrrelevantParam", typeof(string), new Collection()); 33 | 34 | runtimeParamDictionary.Add("DynamicParam", dynamicParam); 35 | runtimeParamDictionary.Add("IrrelevantParam", irrelevantParam); 36 | 37 | return runtimeParamDictionary; 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /XmlDoc2CmdletDoc.TestModule/InputTypes/InputTypeClass.cs: -------------------------------------------------------------------------------- 1 | namespace XmlDoc2CmdletDoc.TestModule.InputTypes 2 | { 3 | /// 4 | /// InputTypeClass1 description. 5 | /// 6 | public class InputTypeClass1 7 | { 8 | } 9 | 10 | /// 11 | /// InputTypeClass2 description. 12 | /// 13 | public class InputTypeClass2 14 | { 15 | } 16 | 17 | /// 18 | /// InputTypeClass3 description. 19 | /// 20 | public class InputTypeClass3 21 | { 22 | } 23 | } -------------------------------------------------------------------------------- /XmlDoc2CmdletDoc.TestModule/InputTypes/TestInputTypesCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Management.Automation; 2 | 3 | namespace XmlDoc2CmdletDoc.TestModule.InputTypes 4 | { 5 | [Cmdlet(VerbsDiagnostic.Test, "InputTypes")] 6 | public class TestInputTypesCommand : Cmdlet 7 | { 8 | /// 9 | /// This is the explicit inputType description for ParameterOne. 10 | /// 11 | [Parameter(ValueFromPipeline = true)] 12 | public InputTypeClass1 ParameterOne { get; set; } 13 | 14 | /// 15 | /// There's no explicit inputType description, so the description of the itself will be used instead. 16 | /// 17 | [Parameter(ValueFromPipeline = true)] 18 | public InputTypeClass2 ParameterTwo { get; set; } 19 | 20 | /// 21 | /// This is the fallback description for ParameterThree. 22 | /// 23 | [Parameter(ValueFromPipeline = true)] 24 | public InputTypeClass3 ParameterThree { get; set; } 25 | } 26 | } -------------------------------------------------------------------------------- /XmlDoc2CmdletDoc.TestModule/Maml/MamlClass.cs: -------------------------------------------------------------------------------- 1 | namespace XmlDoc2CmdletDoc.TestModule.Maml 2 | { 3 | /// 4 | /// Dummy class. 5 | /// 6 | /// This is the MamlClass description. 7 | /// 8 | /// 9 | public class MamlClass 10 | { 11 | 12 | } 13 | } -------------------------------------------------------------------------------- /XmlDoc2CmdletDoc.TestModule/Maml/TestMamlElementsCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Management.Automation; 2 | 3 | namespace XmlDoc2CmdletDoc.TestModule.Maml 4 | { 5 | /// 6 | /// Example dummy comdlet. This text shouldn't appear in the cmdlet help. 7 | /// 8 | /// This is the Test-MamlElements synopsis. 9 | /// 10 | /// 11 | /// This is the Test-MamlElements description. 12 | /// 13 | /// 14 | /// First Note 15 | /// 16 | /// This is the description for the first note. 17 | /// 18 | /// Second Note 19 | /// 20 | /// This is part of the description for the second note. 21 | /// This is also part of the description for the second note. 22 | /// 23 | /// 24 | /// 25 | [Cmdlet(VerbsDiagnostic.Test, "MamlElements")] 26 | [OutputType(typeof(MamlClass))] 27 | [OutputType(typeof(string))] 28 | public class TestMamlElementsCommand : Cmdlet 29 | { 30 | /// 31 | /// This text shouldn't appear in the cmldet help. 32 | /// 33 | /// This is the CommonParameter description. 34 | /// 35 | /// 36 | [Parameter] 37 | public MamlClass CommonParameter { get; set; } 38 | 39 | /// 40 | /// This text shouldn't appear in the cmldet help. 41 | /// 42 | /// This is the ArrayParameter description. 43 | /// 44 | /// 45 | [Parameter] 46 | public MamlClass[] ArrayParameter { get; set; } 47 | 48 | [Parameter(ParameterSetName = "One", ValueFromPipeline = true)] 49 | public string ParameterOne { get; set; } 50 | 51 | [Parameter(ParameterSetName = "Two", ValueFromPipelineByPropertyName = true)] 52 | public MamlClass ParameterTwo { get; set; } 53 | } 54 | } -------------------------------------------------------------------------------- /XmlDoc2CmdletDoc.TestModule/Manual/ManualClass.cs: -------------------------------------------------------------------------------- 1 | namespace XmlDoc2CmdletDoc.TestModule.Manual 2 | { 3 | /// 4 | /// This is part of the ManualClass description. 5 | /// 6 | /// This is also part of the ManualClass description. 7 | public class ManualClass 8 | { 9 | 10 | } 11 | } -------------------------------------------------------------------------------- /XmlDoc2CmdletDoc.TestModule/Manual/TestManualElementsCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Management.Automation; 4 | 5 | namespace XmlDoc2CmdletDoc.TestModule.Manual 6 | { 7 | /// 8 | /// Example dummy comdlet. This text shouldn't appear in the cmdlet help. 9 | /// This is part of the Test-ManualElements synopsis. 10 | /// This is part of the Test-ManualElements description. 11 | /// 12 | /// This is also part of the Test-ManualElements synopsis. 13 | /// This is also part of the Test-ManualElements description. 14 | /// 15 | /// 16 | /// First Note 17 | /// 18 | /// This is the description for the first note. 19 | /// 20 | /// 21 | /// 22 | /// Second Note 23 | /// 24 | /// This is part of the description for the second note. 25 | /// This is also part of the description for the second note. 26 | /// 27 | /// 28 | /// 29 | /// 30 | /// This is part of the first example's introduction. 31 | /// This is also part of the first example's introduction. 32 | /// New-Thingy | Write-Host 33 | /// This is part of the first example's remarks. 34 | /// This is also part of the first example's remarks. 35 | /// 36 | /// 37 | /// This is the second example's introduction. 38 | /// 39 | /// $thingy = New-Thingy 40 | /// If ($thingy -eq $that) { 41 | /// Write-Host 'Same' 42 | /// } else { 43 | /// $thingy | Write-Host 44 | /// } 45 | /// 46 | /// This is the second example's remarks. 47 | /// 48 | /// 49 | /// New-Thingy | Write-Host 50 | /// This is the third example's remarks. 51 | /// 52 | /// 53 | /// This is the fourth example's introduction. 54 | /// New-Thingy | Write-Host 55 | /// 56 | [Cmdlet(VerbsDiagnostic.Test, "ManualElements")] 57 | [OutputType(typeof(ManualClass))] 58 | [OutputType(typeof(string))] 59 | [OutputType(typeof(void))] 60 | public class TestManualElementsCommand : Cmdlet 61 | { 62 | public enum Importance 63 | { 64 | Regular, 65 | Important, 66 | Critical 67 | }; 68 | 69 | /// 70 | /// This is part of the MandatoryParameter description. 71 | /// 72 | /// This is also part of the MandatoryParameter description. 73 | [Parameter(Mandatory = true)] 74 | public ManualClass MandatoryParameter { get; set; } 75 | 76 | /// 77 | /// This is part of the ArrayParameter description. 78 | /// 79 | /// This is also part of the ArrayParameter description. 80 | [Parameter(Mandatory = true)] 81 | public ManualClass[] ArrayParameter { get; set; } 82 | 83 | [Parameter(Mandatory = false)] 84 | public string OptionalParameter { get; set; } 85 | 86 | [Parameter(Position = 1)] 87 | public string PositionedParameter { get; set; } 88 | 89 | /// 90 | /// ValueFromPipeline string parameter is... 91 | /// 92 | [Parameter(ValueFromPipeline = true)] 93 | public string ValueFromPipelineParameter { get; set; } 94 | 95 | /// 96 | /// OtherValueFromPipeline string parameter is... 97 | /// 98 | [Parameter(ValueFromPipelineByPropertyName = true)] 99 | public string OtherValueFromPipelineParameter { get; set; } 100 | 101 | [Parameter(ValueFromPipelineByPropertyName = true)] 102 | public ManualClass ValueFromPipelineByPropertyNameParameter { get; set; } 103 | 104 | [Parameter(Mandatory = false)] 105 | [Alias("Name1A")] 106 | public string SingleAliasParameter { get; set; } 107 | 108 | /// 109 | /// Description here. 110 | /// 111 | [Parameter(Mandatory = false)] 112 | [Alias("Name1B")] 113 | public string WithDescriptionAliasParameter { get; set; } 114 | 115 | [Parameter(Mandatory = false)] 116 | [Alias("Name1C", "Name2C", "Name3C")] 117 | public string MultipleAliasParameter { get; set; } 118 | 119 | [Parameter(Mandatory = false)] 120 | public Importance EnumParameter { get; set; } 121 | 122 | [Parameter(Mandatory = false)] 123 | public Importance[] EnumArrayParameter { get; set; } 124 | 125 | [Parameter(Mandatory = false)] 126 | public List EnumListParameter { get; set; } 127 | 128 | [Parameter] 129 | public int? NullableParameter { get; set; } 130 | 131 | [Parameter] 132 | [Obsolete] 133 | public string ObsoleteParameter { get; set; } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /XmlDoc2CmdletDoc.TestModule/Parameterless/TestParameterlessCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Management.Automation; 2 | 3 | namespace XmlDoc2CmdletDoc.TestModule.Parameterless 4 | { 5 | /// 6 | /// This is part of the Test-PositionedParameters synopsis. 7 | /// This is part of the Test-PositionedParameters description. 8 | /// 9 | [Cmdlet(VerbsDiagnostic.Test, "Parameterless")] 10 | public class TestParameterlessCommand : Cmdlet 11 | { 12 | // Addresses https://github.com/red-gate/XmlDoc2CmdletDoc/issues/28 13 | // This cmdlet has no parameters. We expect this to result in the following syntax element: 14 | // 15 | // 16 | // 17 | // Test-Parameterless 18 | // 19 | // 20 | } 21 | } -------------------------------------------------------------------------------- /XmlDoc2CmdletDoc.TestModule/PositionedParameters/TestPositionedParametersCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Management.Automation; 2 | 3 | namespace XmlDoc2CmdletDoc.TestModule.PositionedParameters 4 | { 5 | /// 6 | /// This is part of the Test-PositionedParameters synopsis. 7 | /// This is part of the Test-PositionedParameters description. 8 | /// 9 | [Cmdlet(VerbsDiagnostic.Test, "PositionedParameters")] 10 | public class TestPositionedParametersCommand : Cmdlet 11 | { 12 | /// 13 | /// This is part of the ParameterA description. 14 | /// 15 | [Parameter(Position = 3)] 16 | public string ParameterA { get; set; } 17 | 18 | /// 19 | /// This is part of the ParameterB description. 20 | /// 21 | [Parameter(Position = 2)] 22 | public string ParameterB { get; set; } 23 | 24 | /// 25 | /// This is part of the ParameterC description. 26 | /// 27 | [Parameter(Position = 0)] 28 | public string ParameterC { get; set; } 29 | 30 | /// 31 | /// This is part of the ParameterD description. 32 | /// 33 | [Parameter(Position = 1)] 34 | public string ParameterD { get; set; } 35 | 36 | /// 37 | /// This is part of the ParameterE description. 38 | /// 39 | [Parameter] 40 | public string ParameterE { get; set; } 41 | 42 | /// 43 | /// This is part of the ParameterF description. 44 | /// 45 | [Parameter(Mandatory = true)] 46 | public string ParameterF { get; set; } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /XmlDoc2CmdletDoc.TestModule/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("XmlDoc2CmdletDoc.TestModule")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("XmlDoc2CmdletDoc.TestModule")] 13 | [assembly: AssemblyCopyright("Copyright © 2014")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("b79b24be-1b06-4433-a296-719dcb400160")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /XmlDoc2CmdletDoc.TestModule/References/TestReferencesCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Management.Automation; 2 | 3 | namespace XmlDoc2CmdletDoc.TestModule.References 4 | { 5 | /// 6 | /// This description for references 7 | /// and the second parameter. 8 | /// 9 | [Cmdlet(VerbsDiagnostic.Test, "References")] 10 | public class TestReferencesCommand : Cmdlet 11 | { 12 | [Parameter(Mandatory = true)] 13 | public string ParameterOne { get; set; } 14 | 15 | [Parameter(Mandatory = true)] 16 | public string ParameterTwo { get; set; } 17 | } 18 | } -------------------------------------------------------------------------------- /XmlDoc2CmdletDoc.TestModule/Undocumented/TestUndocumentedCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Management.Automation; 2 | 3 | namespace XmlDoc2CmdletDoc.TestModule.Undocumented 4 | { 5 | /// 6 | /// This cmdlet has no cmdlet help documentation. 7 | /// 8 | [Cmdlet(VerbsDiagnostic.Test, "Undocumented")] 9 | [OutputType(typeof(UndocumentedClass))] 10 | [OutputType(typeof(string))] 11 | public class TestUndocumentedCommand : Cmdlet 12 | { 13 | /// 14 | /// This field has no cmdlet help documentation. 15 | /// 16 | [Parameter] 17 | public object UndocumentedField; 18 | 19 | /// 20 | /// This property has no cmdlet help documentation. 21 | /// 22 | [Parameter] 23 | public object UndocumentedProperty; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /XmlDoc2CmdletDoc.TestModule/Undocumented/UndocumentedClass.cs: -------------------------------------------------------------------------------- 1 | namespace XmlDoc2CmdletDoc.TestModule.Undocumented 2 | { 3 | /// 4 | /// This type has no cmdlet help documentation. 5 | /// 6 | /// 7 | /// An empty example with no <para> or <code> elements. 8 | /// 9 | public class UndocumentedClass 10 | { 11 | } 12 | } -------------------------------------------------------------------------------- /XmlDoc2CmdletDoc.TestModule/Wildcards/TestWildcardSupportedCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Management.Automation; 2 | 3 | namespace XmlDoc2CmdletDoc.TestModule.Wildcards 4 | { 5 | /// 6 | /// This is part of the Test-WildcardSupport synopsis. 7 | /// This is part of the Test-WildcardSupport description. 8 | /// 9 | [Cmdlet(VerbsDiagnostic.Test, "WildcardSupport")] 10 | public class TestWildcardSupportedCommand : Cmdlet 11 | { 12 | /// 13 | /// This supports wildcards. 14 | /// 15 | [Parameter] 16 | [SupportsWildcards] 17 | public string StringParameter { get; set; } 18 | 19 | /// 20 | /// This does not support wildcards. 21 | /// 22 | [Parameter] 23 | public string NonWildParameter { get; set; } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /XmlDoc2CmdletDoc.TestModule/WriteOnly/TestWriteOnlyParameterCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Management.Automation; 2 | 3 | namespace XmlDoc2CmdletDoc.TestModule.WriteOnly 4 | { 5 | /// 6 | /// Cmdlet that has a parameter with no getter. This exists to demonstrate that XmlDoc2CmdletDoc 7 | /// doesn't blow up when trying to determine the default value of a parameter that cannot be read. 8 | /// 9 | [Cmdlet(VerbsDiagnostic.Test, "WriteOnlyParameter")] 10 | public class TestWriteOnlyParameterCommand : Cmdlet 11 | { 12 | /// 13 | /// This parameter has no getter. 14 | /// 15 | [Parameter] 16 | public string WriteOnly { set { } } 17 | } 18 | } -------------------------------------------------------------------------------- /XmlDoc2CmdletDoc.TestModule/XmlDoc2CmdletDoc.TestModule.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net45 4 | true 5 | false 6 | 1591 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /XmlDoc2CmdletDoc.TestModule/XmlDoc2CmdletDoc.TestModule.v2.ncrunchproject: -------------------------------------------------------------------------------- 1 | 2 | 1000 3 | false 4 | false 5 | false 6 | true 7 | false 8 | false 9 | false 10 | false 11 | false 12 | true 13 | true 14 | false 15 | true 16 | true 17 | true 18 | 60000 19 | 20 | 21 | 22 | AutoDetect 23 | STA 24 | x86 25 | -------------------------------------------------------------------------------- /XmlDoc2CmdletDoc.Tests/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("XmlDoc2CmdletDoc.Tests")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("XmlDoc2CmdletDoc.Tests")] 13 | [assembly: AssemblyCopyright("Copyright © 2014")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("7a9ab8d3-e139-4647-832b-efa984f7aaa3")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /XmlDoc2CmdletDoc.Tests/XElementExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Xml.Linq; 3 | 4 | namespace XmlDoc2CmdletDoc.Tests 5 | { 6 | public static class XElementExtensions 7 | { 8 | /// 9 | /// Returns a simple, clean string representation of the 10 | /// 11 | /// 12 | /// 13 | public static string ToSimpleString(this XElement element) 14 | { 15 | // Strip out superfluous text and comments from the element. 16 | element = element.Simplify(); 17 | 18 | // Add the element to a container element with the common namespace prefixes defined, 19 | // so that it adops those prefixes. 20 | var container = new XElement("container", 21 | new XAttribute(XNamespace.Xmlns + "maml", "http://schemas.microsoft.com/maml/2004/10"), 22 | new XAttribute(XNamespace.Xmlns + "command", "http://schemas.microsoft.com/maml/dev/command/2004/10"), 23 | new XAttribute(XNamespace.Xmlns + "dev", "http://schemas.microsoft.com/maml/dev/2004/10"), 24 | element); 25 | element = container.Elements().First(); 26 | 27 | // And then format it nicely. 28 | return element.ToString(SaveOptions.OmitDuplicateNamespaces); 29 | } 30 | 31 | private static XElement Simplify(this XElement element) 32 | { 33 | var newElement = new XElement(element.Name); 34 | foreach (var attribute in element.Attributes()) 35 | { 36 | newElement.Add(attribute); 37 | } 38 | foreach (var node in element.Nodes()) 39 | { 40 | if (node is XElement) 41 | { 42 | newElement.Add(((XElement)node).Simplify()); 43 | } 44 | else if (node is XText) 45 | { 46 | newElement.Add(((XText)node).Value.Trim()); 47 | } 48 | } 49 | return newElement; 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /XmlDoc2CmdletDoc.Tests/XmlDoc2CmdletDoc.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net45 4 | false 5 | false 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /XmlDoc2CmdletDoc.Tests/XmlDoc2CmdletDoc.Tests.v2.ncrunchproject: -------------------------------------------------------------------------------- 1 | 2 | 1000 3 | true 4 | true 5 | false 6 | true 7 | false 8 | false 9 | false 10 | false 11 | false 12 | true 13 | true 14 | false 15 | true 16 | true 17 | true 18 | 60000 19 | 20 | 21 | 22 | AutoDetect 23 | STA 24 | x86 25 | CopyReferencedAssembliesToWorkspaceIsOn 26 | -------------------------------------------------------------------------------- /XmlDoc2CmdletDoc.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 14.00 3 | # Visual Studio 2013 4 | VisualStudioVersion = 14.0.30110.0 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XmlDoc2CmdletDoc.Core", "XmlDoc2CmdletDoc.Core\XmlDoc2CmdletDoc.Core.csproj", "{D59BC007-3898-4556-9D8F-6C951BB3E50E}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XmlDoc2CmdletDoc", "XmlDoc2CmdletDoc\XmlDoc2CmdletDoc.csproj", "{34C12E10-BF70-4771-89CA-234DBD44E03A}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XmlDoc2CmdletDoc.TestModule", "XmlDoc2CmdletDoc.TestModule\XmlDoc2CmdletDoc.TestModule.csproj", "{5FADA979-0FB5-4A14-A1BD-93AFB62D7B44}" 11 | EndProject 12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XmlDoc2CmdletDoc.Tests", "XmlDoc2CmdletDoc.Tests\XmlDoc2CmdletDoc.Tests.csproj", "{C105725D-8594-4D9E-9418-57B2FBB7563A}" 13 | EndProject 14 | Global 15 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 16 | Debug|Any CPU = Debug|Any CPU 17 | Release|Any CPU = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 20 | {D59BC007-3898-4556-9D8F-6C951BB3E50E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {D59BC007-3898-4556-9D8F-6C951BB3E50E}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {D59BC007-3898-4556-9D8F-6C951BB3E50E}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {D59BC007-3898-4556-9D8F-6C951BB3E50E}.Release|Any CPU.Build.0 = Release|Any CPU 24 | {34C12E10-BF70-4771-89CA-234DBD44E03A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 25 | {34C12E10-BF70-4771-89CA-234DBD44E03A}.Debug|Any CPU.Build.0 = Debug|Any CPU 26 | {34C12E10-BF70-4771-89CA-234DBD44E03A}.Release|Any CPU.ActiveCfg = Release|Any CPU 27 | {34C12E10-BF70-4771-89CA-234DBD44E03A}.Release|Any CPU.Build.0 = Release|Any CPU 28 | {5FADA979-0FB5-4A14-A1BD-93AFB62D7B44}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {5FADA979-0FB5-4A14-A1BD-93AFB62D7B44}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {5FADA979-0FB5-4A14-A1BD-93AFB62D7B44}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {5FADA979-0FB5-4A14-A1BD-93AFB62D7B44}.Release|Any CPU.Build.0 = Release|Any CPU 32 | {C105725D-8594-4D9E-9418-57B2FBB7563A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {C105725D-8594-4D9E-9418-57B2FBB7563A}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {C105725D-8594-4D9E-9418-57B2FBB7563A}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {C105725D-8594-4D9E-9418-57B2FBB7563A}.Release|Any CPU.Build.0 = Release|Any CPU 36 | EndGlobalSection 37 | GlobalSection(SolutionProperties) = preSolution 38 | HideSolutionNode = FALSE 39 | EndGlobalSection 40 | EndGlobal 41 | -------------------------------------------------------------------------------- /XmlDoc2CmdletDoc.sln.DotSettings: -------------------------------------------------------------------------------- 1 |  2 | True 3 | True 4 | True 5 | True 6 | True 7 | True 8 | True 9 | True 10 | True 11 | True 12 | True 13 | True 14 | True 15 | True 16 | True 17 | NEVER 18 | False 19 | NEVER 20 | False 21 | True 22 | True 23 | False 24 | False 25 | 160 26 | True 27 | True 28 | True 29 | True 30 | True 31 | True 32 | True 33 | False -------------------------------------------------------------------------------- /XmlDoc2CmdletDoc.v2.ncrunchsolution: -------------------------------------------------------------------------------- 1 | 2 | 1 3 | false 4 | false 5 | true 6 | UseDynamicAnalysis 7 | UseStaticAnalysis 8 | UseStaticAnalysis 9 | UseStaticAnalysis 10 | UseStaticAnalysis 11 | 12 | 13 | -------------------------------------------------------------------------------- /XmlDoc2CmdletDoc/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using XmlDoc2CmdletDoc.Core; 5 | 6 | namespace XmlDoc2CmdletDoc 7 | { 8 | public static class Program 9 | { 10 | public static void Main(string[] args) 11 | { 12 | var options = ParseArguments(args); 13 | Console.WriteLine(options); 14 | var engine = new Engine(); 15 | var exitCode = engine.GenerateHelp(options); 16 | Console.WriteLine("GenerateHelp completed with exit code '{0}'", exitCode); 17 | Environment.Exit((int)exitCode); 18 | } 19 | 20 | private static Options ParseArguments(IReadOnlyList args) 21 | { 22 | const string strictSwitch = "-strict"; 23 | const string excludeParameterSetSwitch = "-excludeParameterSets"; 24 | 25 | try 26 | { 27 | var treatWarningsAsErrors = false; 28 | var excludedParameterSets = new List(); 29 | string assemblyPath = null; 30 | 31 | for (var i = 0; i < args.Count; i++) 32 | { 33 | if (args[i] == strictSwitch) 34 | { 35 | treatWarningsAsErrors = true; 36 | } 37 | else if (args[i] == excludeParameterSetSwitch) 38 | { 39 | i++; 40 | if (i >= args.Count) throw new ArgumentException(); 41 | excludedParameterSets.AddRange(args[i].Split(new [] {','}, StringSplitOptions.RemoveEmptyEntries).Select(p => p.Trim())); 42 | } 43 | else if (assemblyPath == null) 44 | { 45 | assemblyPath = args[i]; 46 | } 47 | else 48 | { 49 | throw new ArgumentException(); 50 | } 51 | } 52 | 53 | if (assemblyPath == null) 54 | { 55 | throw new ArgumentException(); 56 | } 57 | 58 | return new Options(treatWarningsAsErrors, assemblyPath, excludedParameterSets.Contains); 59 | } 60 | catch (ArgumentException) 61 | { 62 | Console.Error.WriteLine($"Usage: XmlDoc2CmdletDoc.exe [{strictSwitch}] [{excludeParameterSetSwitch} parameterSetToExclude1,parameterSetToExclude2] assemblyPath"); 63 | Environment.Exit(-1); 64 | throw; 65 | } 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /XmlDoc2CmdletDoc/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.InteropServices; 3 | 4 | // General Information about an assembly is controlled through the following 5 | // set of attributes. Change these attribute values to modify the information 6 | // associated with an assembly. 7 | [assembly: AssemblyTitle("XmlDoc2CmdletDoc")] 8 | [assembly: AssemblyDescription("")] 9 | 10 | // The following GUID is for the ID of the typelib if this project is exposed to COM 11 | [assembly: Guid("a2ea1eb0-60cb-481d-888d-b738eddc98f3")] 12 | -------------------------------------------------------------------------------- /XmlDoc2CmdletDoc/XmlDoc2CmdletDoc.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net45 4 | Exe 5 | false 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | Properties\SolutionInfo.cs 14 | 15 | 16 | -------------------------------------------------------------------------------- /XmlDoc2CmdletDoc/XmlDoc2CmdletDoc.nuspec: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | XmlDoc2CmdletDoc 5 | $version$ 6 | Redgate XmlDoc2CmdletDoc tool 7 | Redgate 8 | Redgate 9 | https://github.com/red-gate/XmlDoc2CmdletDoc 10 | license\LICENSE.md 11 | false 12 | Generates help documentation for binary PowerShell modules from XML Doc comments. 13 | Tool used to generate .dll-Help.xml help files for binary PowerShell modules from XML Doc comments. Licensed under the terms of the new 3-clause BSD license. 14 | en-us 15 | Copyright 2014-2019 Red Gate Software Ltd and other contributors. 16 | PowerShell help xmldoc 17 | true 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /XmlDoc2CmdletDoc/XmlDoc2CmdletDoc.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | false 11 | 12 | 19 | 20 | 21 | XmlDoc2CmdletDoc32.exe 22 | XmlDoc2CmdletDoc.exe 23 | 24 | 28 | 30 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /XmlDoc2CmdletDoc/XmlDoc2CmdletDoc.v2.ncrunchproject: -------------------------------------------------------------------------------- 1 | 2 | 1000 3 | false 4 | false 5 | false 6 | true 7 | false 8 | false 9 | false 10 | false 11 | false 12 | true 13 | true 14 | false 15 | true 16 | true 17 | true 18 | 60000 19 | 20 | 21 | 22 | AutoDetect 23 | STA 24 | x86 25 | -------------------------------------------------------------------------------- /build.ps1: -------------------------------------------------------------------------------- 1 | # Initialises the build system (which declares three global build functions, build, clean and rebuild) then starts a build. 2 | # The real work of the build system is defined in .build\build.ps1 and .build\_init.ps1. 3 | & "$PsScriptRoot\.build\_init.ps1" 4 | Rebuild 5 | -------------------------------------------------------------------------------- /release-notes.md: -------------------------------------------------------------------------------- 1 | *Note: The build version number is derived from the first entry in this file.* 2 | 3 | # 0.3.0 4 | 5 | - Use Microsoft.PowerShell.5.ReferenceAssemblies instead of a legacy version of System.Management.Automation. 6 | - Added support for globbing/wildcards based on the presence of SupportsWildcardsAttribute. 7 | 8 | # 0.2.13 9 | 10 | - Correctly resolve type descriptions for array-typed parameters. 11 | 12 | # 0.2.12 13 | 14 | - Extended the support for documenting parameters of type `Enum` to include parameters of type `IEnumerable where T : Enum`. 15 | 16 | # 0.2.11 17 | 18 | - Added support for dynamic parameters. 19 | 20 | # 0.2.10 21 | 22 | - Parameters marked with the [Obsolete] attribute no longer appear in the cmdlet syntax summary, though it's still possible to provide help text for the parameter, which can be viewed using the `-Parameter` switch of the `Get-Help` cmdlet. 23 | 24 | - Added support for excluding parameter sets by name, via a new command-line option and corresponding msbuild property. 25 | 26 | # 0.2.9 27 | 28 | - Fixed issue #39. Corrected a regression for the default value of string parameters. 29 | 30 | # 0.2.8 31 | 32 | - Partially addressed issues #33 and #37. Slightly improved handling of default values and array parameters. More work is required in this area, though. 33 | 34 | # 0.2.7 35 | 36 | - Fixed issue #31. MSBuild task now accommodates binary modules that are specifically targeted at only x86 or x64 architectures. 37 | 38 | # 0.2.6 39 | 40 | - Fixed issue #28. Ensure that help syntax is correctly displayed for parameterless cmdlets. 41 | 42 | # 0.2.5 43 | 44 | - Added limited support for documenting dynamic parameters. If a cmdlet implements IDynamicParameters, and its GetDynamicParameters method returns an instance of a nested type within the cmdlet, then help documentation will be extracted from the nested type's XML Doc comments. 45 | 46 | # 0.2.4 47 | 48 | - Fixed issue #22. When encountering a Parameter with no getter, XmlDoc2CmdletDoc now records a warning that the default value for the Parameter cannot be obtained. Previously this raised a fatal exception. 49 | 50 | # 0.2.3 51 | 52 | - XmlDoc2CmdletDoc now executes prior to the AfterBuild target, rather than prior to the PostBuildEvent target, to give developers the option to copy files around in either target, rather than only in the latter. 53 | 54 | # 0.2.2 55 | 56 | - Fixed issue #19: Help for cmdlet parameters is now explicitly ordered by Position, then Required, then Name, rather than relying on the arbitrary order of Type.GetMembers. 57 | 58 | # 0.2.1 59 | 60 | - First public release. --------------------------------------------------------------------------------