Skip to content

Export-MamlCommandHelp: a module file in the batch aborts the whole export and writes nothing #862

Description

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions