Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
a1987c8
Add example title support to comment-based help
MariusStorhaug Feb 26, 2026
450f024
Reset parts i didnt touch
MariusStorhaug Feb 26, 2026
5de3078
Merge branch 'master' into feature/example-titles-in-comment-help
MariusStorhaug Apr 17, 2026
47fbd96
Merge branch 'master' of https://github.com/MariusStorhaug/PowerShell…
MariusStorhaug May 1, 2026
38f9720
Address review: keep API non-breaking, localize title extraction, fix…
MariusStorhaug May 1, 2026
02dc3f0
Add example title extraction and reflection tests for ProxyCommand
MariusStorhaug May 1, 2026
628df60
Remove leftover debug scratch scripts
MariusStorhaug May 1, 2026
26eee9f
Move example-title help tests to a separate Pester file so discovery …
MariusStorhaug May 1, 2026
9caffb9
Fix ProxyCommand example-title tests to handle Get-Help PS prompt pre…
MariusStorhaug May 1, 2026
3130b90
Address Copilot review: remove UTF-8 BOM from test file; fix misleadi…
MariusStorhaug May 1, 2026
5bded45
Fix ExtractExampleTitle doc comment (colon, not colon-space); strengt…
MariusStorhaug May 1, 2026
7d593b9
Delete debug2
MariusStorhaug May 2, 2026
f34c532
Merge branch 'master' into feature/example-titles-in-comment-help
MariusStorhaug May 21, 2026
7d70bcf
Merge branch 'master' into feature/example-titles-in-comment-help
MariusStorhaug Jul 11, 2026
b7967ba
Merge remote-tracking branch 'upstream/master' into feature/example-t…
MariusStorhaug Jul 23, 2026
7d503ac
Fix ProxyCommand.GetHelpComments dropping single-example functions
MariusStorhaug Jul 23, 2026
28038a4
Merge branch 'master' into feature/example-titles-in-comment-help
MariusStorhaug Jul 23, 2026
2c1e3aa
Merge branch 'master' into feature/example-titles-in-comment-help
MariusStorhaug Aug 8, 2026
7575577
Merge branch 'master' into feature/example-titles-in-comment-help
MariusStorhaug Aug 20, 2026
9b8f622
Merge branch 'master' into feature/example-titles-in-comment-help
MariusStorhaug Aug 28, 2026
5f6f612
Fix arbitrary MAML example title round-tripping
MariusStorhaug Aug 29, 2026
0c68fdc
Strengthen example title test coverage
MariusStorhaug Aug 29, 2026
14329c3
Merge branch 'master' into feature/example-titles-in-comment-help
MariusStorhaug Sep 3, 2026
509b7d5
Keep example titles and bodies in sync by construction
Sep 3, 2026
74aba26
Simplify proxy help example title extraction
MariusStorhaug Sep 5, 2026
802c7a2
Handle variable-width example title borders
Sep 8, 2026
24fef03
Simplify example title extraction without losing dashes
Sep 8, 2026
5bbe60c
Merge remote-tracking branch 'origin/master' into feature/example-tit…
Sep 8, 2026
5ec877b
Cover mixed titled and untitled example layouts
Sep 9, 2026
294bef6
Cover all-titled and all-untitled example layouts
Sep 9, 2026
856816b
Extract example titles without a regular expression
Sep 9, 2026
d8c3d5d
Trim example title borders instead of matching them
Sep 9, 2026
3f9e8aa
Preserve authored dashes in proxy example titles
MariusStorhaug Sep 10, 2026
3e18011
Merge branch 'master' into feature/example-titles-in-comment-help
MariusStorhaug Sep 10, 2026
db7d905
Merge branch 'master' into feature/example-titles-in-comment-help
MariusStorhaug Sep 11, 2026
59e9049
Use generated help in example title tests
MariusStorhaug Sep 11, 2026
afff413
Simplify proxy example title extraction
MariusStorhaug Sep 11, 2026
18f056d
Merge branch 'master' into feature/example-titles-in-comment-help
MariusStorhaug Sep 11, 2026
45ed8f2
Merge branch 'master' into feature/example-titles-in-comment-help
MariusStorhaug Sep 12, 2026
ec70ae1
Merge branch 'master' into feature/example-titles-in-comment-help
MariusStorhaug Sep 19, 2026
9e6d2bf
Merge branch 'master' into feature/example-titles-in-comment-help
MariusStorhaug Sep 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 38 additions & 2 deletions src/System.Management.Automation/engine/ProxyCommand.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Text;

Expand Down Expand Up @@ -423,7 +424,13 @@ public static string GetHelpComments(PSObject help)
}

PSObject examples = GetProperty<PSObject>(help, "examples");
PSObject[] example = GetProperty<PSObject[]>(examples, "example");
// Get-Help returns a single example as PSObject and multiple as PSObject[].
// Normalize both to IEnumerable<PSObject> so the loop handles either case.
PSObject[] exampleArray = GetProperty<PSObject[]>(examples, "example");
IEnumerable<PSObject> example = exampleArray
?? (GetProperty<PSObject>(examples, "example") is PSObject singleEx
? new[] { singleEx }
: null);
if (example != null)
{
foreach (PSObject ex in example)
Expand Down Expand Up @@ -461,7 +468,20 @@ public static string GetHelpComments(PSObject help)

if (exsb.Length > 0)
{
sb.Append("\n\n.EXAMPLE\n\n");
// The title property value may be stored as a PSObject wrapping a string,
// so use ToString() on the raw Value rather than 'Value as string'.
ReadOnlySpan<char> exampleTitle = ExtractExampleTitle(ex.Properties["title"]?.Value?.ToString());
if (!exampleTitle.IsEmpty)
{
sb.Append("\n\n.EXAMPLE ");
sb.Append(exampleTitle);
sb.Append("\n\n");
}
else
{
sb.Append("\n\n.EXAMPLE\n\n");
}

sb.Append(exsb);
}
}
Expand Down Expand Up @@ -498,6 +518,22 @@ public static string GetHelpComments(PSObject help)
return sb.ToString();
}

/// <summary>
/// Extracts the title after the first ": " delimiter in a generated Get-Help example heading.
/// The final dash border and surrounding whitespace are trimmed, preserving authored dashes
/// separated from the border by a space. The localized example label is not interpreted,
/// and a heading without the delimiter has no title.
/// </summary>
private static ReadOnlySpan<char> ExtractExampleTitle(string decoratedTitle)
{
ReadOnlySpan<char> heading = decoratedTitle.AsSpan();
int separatorIndex = heading.IndexOf(": ", StringComparison.Ordinal);
Comment thread
MariusStorhaug marked this conversation as resolved.

return separatorIndex < 0 || separatorIndex + 2 >= heading.Length
? default
: heading.Slice(separatorIndex + 2).TrimEnd('-').Trim();
}

#endregion
}
}
28 changes: 25 additions & 3 deletions src/System.Management.Automation/engine/parser/ast.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10687,6 +10687,16 @@ public sealed class CommentHelpInfo
/// </summary>
public ReadOnlyCollection<string> Examples { get; internal set; }

/// <summary>
/// The optional titles of each .EXAMPLE section, parallel to <see cref="Examples"/>.
/// Each entry corresponds to the example body at the same index. An entry is an empty
/// string when the example was declared without a title (the historical form
/// <c>.EXAMPLE</c> on its own line). When the inline form
/// <c>.EXAMPLE &lt;Title&gt;</c> is used, the trimmed title text is stored at the
/// matching index.
/// </summary>
public ReadOnlyCollection<string> ExampleTitles { get; internal set; }

/// <summary>
/// The help content from all of the specified .INPUT sections.
/// </summary>
Expand Down Expand Up @@ -10782,9 +10792,21 @@ public string GetCommentBlock()

for (int index = 0; index < Examples.Count; index++)
{
var example = Examples[index];
sb.AppendLine(".EXAMPLE");
sb.AppendLine(example);
// ExampleTitles is always parallel to Examples, so the index is valid whenever
// Examples[index] is. An empty entry means the example was declared untitled.
string title = ExampleTitles[index];

if (!string.IsNullOrEmpty(title))
{
sb.Append(".EXAMPLE ");
sb.AppendLine(title);
}
else
{
sb.AppendLine(".EXAMPLE");
}

sb.AppendLine(Examples[index]);
}

for (int index = 0; index < Links.Count; index++)
Expand Down
41 changes: 29 additions & 12 deletions src/System.Management.Automation/help/HelpCommentsParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ private HelpCommentsParser(CommandInfo commandInfo, List<string> parameterDescri
private readonly Language.CommentHelpInfo _sections = new Language.CommentHelpInfo();
private readonly Dictionary<string, string> _parameters = new Dictionary<string, string>();
private readonly List<string> _examples = new List<string>();
private readonly List<string> _exampleTitles = new List<string>();
private readonly List<string> _inputs = new List<string>();
private readonly List<string> _outputs = new List<string>();
private readonly List<string> _links = new List<string>();
Expand Down Expand Up @@ -355,22 +356,28 @@ internal XmlDocument BuildXmlFromComments()
{
XmlElement examples = _doc.CreateElement("command:examples", commandURI);
int count = 1;
foreach (string example in _examples)
for (int exampleIndex = 0; exampleIndex < _examples.Count; exampleIndex++)
{
string exampleBody = _examples[exampleIndex];
string exampleTitle = _exampleTitles[exampleIndex];
XmlElement example_node = _doc.CreateElement("command:example", commandURI);

// The title is automatically generated
// The title is automatically generated, with an optional custom title appended
XmlElement title = _doc.CreateElement("maml:title", mamlURI);
string titleStr = string.Format(CultureInfo.InvariantCulture,
"\t\t\t\t-------------------------- {0} {1} --------------------------",
HelpDisplayStrings.ExampleUpperCase, count++);
string titleStr = string.IsNullOrEmpty(exampleTitle)
? string.Format(CultureInfo.InvariantCulture,
"\t\t\t\t-------------------------- {0} {1} --------------------------",
HelpDisplayStrings.ExampleUpperCase, count++)
: string.Format(CultureInfo.InvariantCulture,
"\t\t\t\t-------------------------- {0} {1}: {2} --------------------------",
HelpDisplayStrings.ExampleUpperCase, count++, exampleTitle);
XmlText title_text = _doc.CreateTextNode(titleStr);
example_node.AppendChild(title).AppendChild(title_text);

string prompt_str;
string code_str;
string remarks_str;
GetExampleSections(example, out prompt_str, out code_str, out remarks_str);
GetExampleSections(exampleBody, out prompt_str, out code_str, out remarks_str);

// Introduction (usually the prompt)
XmlElement introduction = _doc.CreateElement("maml:introduction", mamlURI);
Expand Down Expand Up @@ -719,9 +726,21 @@ private bool AnalyzeCommentBlock(List<string> commentLines)
{
directiveFound = true;

if (match.Groups[3].Success)
string sectionName = match.Groups[1].Value.ToUpperInvariant();

// .EXAMPLE is the only directive that is valid both with and without inline
// text, so it is handled once here instead of in both switches below. Adding
// the title and the body together keeps _exampleTitles and _examples the same
// length by construction. Groups[3].Value is the empty string when the
// optional group did not participate, which is the untitled form.
if (sectionName == "EXAMPLE")
{
_exampleTitles.Add(match.Groups[3].Value.Trim());
_examples.Add(GetSection(commentLines, ref i));
}
else if (match.Groups[3].Success)
{
switch (match.Groups[1].Value.ToUpperInvariant())
switch (sectionName)
{
case "PARAMETER":
{
Expand Down Expand Up @@ -753,7 +772,7 @@ private bool AnalyzeCommentBlock(List<string> commentLines)
}
else
{
switch (match.Groups[1].Value.ToUpperInvariant())
switch (sectionName)
{
case "SYNOPSIS":
_sections.Synopsis = GetSection(commentLines, ref i);
Expand All @@ -767,9 +786,6 @@ private bool AnalyzeCommentBlock(List<string> commentLines)
case "LINK":
_links.Add(GetSection(commentLines, ref i).Trim());
break;
case "EXAMPLE":
_examples.Add(GetSection(commentLines, ref i));
break;
case "INPUTS":
_inputs.Add(GetSection(commentLines, ref i));
break;
Expand Down Expand Up @@ -797,6 +813,7 @@ private bool AnalyzeCommentBlock(List<string> commentLines)
}

_sections.Examples = new ReadOnlyCollection<string>(_examples);
_sections.ExampleTitles = new ReadOnlyCollection<string>(_exampleTitles);
_sections.Inputs = new ReadOnlyCollection<string>(_inputs);
_sections.Outputs = new ReadOnlyCollection<string>(_outputs);
_sections.Links = new ReadOnlyCollection<string>(_links);
Expand Down
Loading