Summary
Passing a module file (the -WithModulePage output) to Import-MarkdownCommandHelp and piping the
result to Export-MamlCommandHelp throws and aborts the entire batch, so no MAML is written at
all — including for the valid command help in the same pipeline.
The failure is silent in the sense that matters: you get a created output directory containing zero
.xml files, and the only signal is a terminating error naming a directory rather than a document.
Steps to reproduce
Microsoft.PowerShell.PlatyPS 1.0.3, PowerShell 7.6.5, Windows.
Import-Module Microsoft.PowerShell.PlatyPS -RequiredVersion 1.0.3
# a one-function module, imported
$mi = Import-Module .\DemoMod\DemoMod.psd1 -Force -PassThru
New-MarkdownCommandHelp -ModuleInfo $mi -OutputFolder .\docs -WithModulePage -Force
# => docs\DemoMod\DemoMod.md, docs\DemoMod\Get-DemoThing.md
Import-MarkdownCommandHelp -Path (Get-ChildItem .\docs\DemoMod -Filter *.md).FullName |
Export-MamlCommandHelp -OutputFolder .\maml -Force
Actual
imported objects: 2
Title='DemoMod' ExternalHelpFile=''
Title='Get-DemoThing' ExternalHelpFile='DemoMod-Help.xml'
EXPORT THREW: UnauthorizedAccessException :: Access to the path '...\maml\DemoMod' is denied.
xml files written: 0
Get-DemoThing's help is valid and was lost.
Expected
Either Import-MarkdownCommandHelp rejects a module file the way Update-MarkdownCommandHelp
already does, or Export-MamlCommandHelp skips the record with a non-terminating error and still
writes the command help it can.
Analysis
Two things combine.
Import-MarkdownCommandHelp does no document-type probing. Update-MarkdownCommandHelp does:
var identity = MarkdownProbe.Identify(path);
if (! identity.IsCommandHelp())
{
WriteError(new ErrorRecord(new ArgumentException($"'{path}' is not a CommandHelp file."), ...));
continue;
}
ImportMarkdownCommand.cs has no equivalent check and calls
MarkdownConverter.GetCommandHelpFromMarkdownFile(path) directly, so a module file returns a
CommandHelp with ExternalHelpFile set to the empty string rather than null.
That empty string then defeats both null-coalescing fallbacks in the export path:
GroupBy(c => c?.ExternalHelpFile ?? c?.ModuleName)
helpFileName = group.First().ExternalHelpFile ?? $"{moduleName}-Help.xml"
"" is not null, so neither falls through, and Path.Combine(moduleMamlBasePath, "") resolves to
the directory itself. Writing it throws UnauthorizedAccessException. Because OrderBy(g => g.Key)
sorts the empty key first and the exception is unhandled in EndProcessing, the batch stops before
reaching any real document.
Suggested fix
A string.IsNullOrEmpty check in place of the ?? in the export path would turn a total-loss abort
into a single skipped record. Probing document type in Import-MarkdownCommandHelp, matching
Update-MarkdownCommandHelp, would stop it earlier and give a clearer message.
Notes
The documented idiom does filter module files out, and every example I found uses it:
Measure-PlatyPSMarkdown -Path .\WidgetModule\*.md |
Where-Object Filetype -match 'CommandHelp' |
Import-MarkdownCommandHelp -Path {$_.FilePath} |
Export-MamlCommandHelp -OutputFolder .\maml
So this is avoidable, and we do avoid it. It is reported because the failure mode is
disproportionate — a caller who forgets the filter, or who points at a docs tree that happens to
contain a module page, loses the whole export rather than one file, and the error message names a
directory with no indication that a module page was the cause.
Summary
Passing a module file (the
-WithModulePageoutput) toImport-MarkdownCommandHelpand piping theresult to
Export-MamlCommandHelpthrows and aborts the entire batch, so no MAML is written atall — including for the valid command help in the same pipeline.
The failure is silent in the sense that matters: you get a created output directory containing zero
.xmlfiles, and the only signal is a terminating error naming a directory rather than a document.Steps to reproduce
Microsoft.PowerShell.PlatyPS1.0.3, PowerShell 7.6.5, Windows.Actual
Get-DemoThing's help is valid and was lost.Expected
Either
Import-MarkdownCommandHelprejects a module file the wayUpdate-MarkdownCommandHelpalready does, or
Export-MamlCommandHelpskips the record with a non-terminating error and stillwrites the command help it can.
Analysis
Two things combine.
Import-MarkdownCommandHelpdoes no document-type probing.Update-MarkdownCommandHelpdoes:ImportMarkdownCommand.cshas no equivalent check and callsMarkdownConverter.GetCommandHelpFromMarkdownFile(path)directly, so a module file returns aCommandHelpwithExternalHelpFileset to the empty string rather than null.That empty string then defeats both null-coalescing fallbacks in the export path:
""is not null, so neither falls through, andPath.Combine(moduleMamlBasePath, "")resolves tothe directory itself. Writing it throws
UnauthorizedAccessException. BecauseOrderBy(g => g.Key)sorts the empty key first and the exception is unhandled in
EndProcessing, the batch stops beforereaching any real document.
Suggested fix
A
string.IsNullOrEmptycheck in place of the??in the export path would turn a total-loss abortinto a single skipped record. Probing document type in
Import-MarkdownCommandHelp, matchingUpdate-MarkdownCommandHelp, would stop it earlier and give a clearer message.Notes
The documented idiom does filter module files out, and every example I found uses it:
So this is avoidable, and we do avoid it. It is reported because the failure mode is
disproportionate — a caller who forgets the filter, or who points at a docs tree that happens to
contain a module page, loses the whole export rather than one file, and the error message names a
directory with no indication that a module page was the cause.