mirror of
https://github.com/SabreTools/SabreTools.CommandLine.git
synced 2026-07-10 10:48:26 +00:00
Add man page generation from the command-line model (#2)
* Add man page generation from the command-line model Add CommandSet.OutputManPage (string and file overloads), a ManPageInfo type for document-level metadata, a reference ManPage feature invoked as a man command, and UserInput.FormatManPage which reuses FormatFlags so the page cannot drift from --help. Output uses only portable man(7) macros and validates warning-free under groff and mandoc -T lint. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Address review feedback on man page generation Rename the man page API to the single-word "Manpage" spelling throughout (Manpage feature, ManpageInfo, CommandSet.OutputManpage, UserInput.FormatManpage) for consistency with the "manpage" convention. - Drop the redundant null check in OutputManpage; info is non-nullable under the enabled nullable context. - Move the tests that exercise CommandSet.OutputManpage into CommandSetTests as a "Manpage Output" region, and relocate the Manpage feature test into a path-synced Features subfolder (SabreTools.CommandLine.Test.Features). Private test helpers now sit at the bottom of their classes. - Fix the Manpage feature doc example to use the actual "man" command. Builds clean across all target frameworks; the 235 tests pass and the generated page still validates warning-free under groff -man -ww and mandoc -T lint. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using SabreTools.CommandLine.Inputs;
|
||||
using Xunit;
|
||||
|
||||
@@ -839,6 +840,144 @@ namespace SabreTools.CommandLine.Test
|
||||
|
||||
#endregion
|
||||
|
||||
#region Manpage Output
|
||||
|
||||
[Fact]
|
||||
public void OutputManpage_Structure_ContainsRequiredSections()
|
||||
{
|
||||
var set = BuildSampleSet();
|
||||
var info = new ManpageInfo("sample") { Version = "sample 1.0", Description = "do sample things" };
|
||||
|
||||
string page = set.OutputManpage(info);
|
||||
|
||||
Assert.Contains(".TH \"SAMPLE\" \"1\"", page);
|
||||
Assert.Contains(".SH NAME", page);
|
||||
Assert.Contains("sample \\- do sample things", page);
|
||||
Assert.Contains(".SH SYNOPSIS", page);
|
||||
Assert.Contains(".SH DESCRIPTION", page);
|
||||
Assert.Contains(".SH OPTIONS", page);
|
||||
Assert.Contains(".TP", page);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OutputManpage_EscapesHyphensInFlags()
|
||||
{
|
||||
var set = BuildSampleSet();
|
||||
var info = new ManpageInfo("sample");
|
||||
|
||||
string page = set.OutputManpage(info);
|
||||
|
||||
Assert.Contains(".B \\-\\-convert", page);
|
||||
Assert.DoesNotContain(".B --convert", page);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OutputManpage_Verbose_TogglesDetailedText()
|
||||
{
|
||||
var set = BuildSampleSet();
|
||||
var info = new ManpageInfo("sample");
|
||||
|
||||
string terse = set.OutputManpage(info, includeVerbose: false);
|
||||
string verbose = set.OutputManpage(info, includeVerbose: true);
|
||||
|
||||
Assert.DoesNotContain("Built\\-in to most of the programs", terse);
|
||||
Assert.Contains("Built\\-in to most of the programs", verbose);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OutputManpage_NestsChildren()
|
||||
{
|
||||
var set = BuildSampleSet();
|
||||
var info = new ManpageInfo("sample");
|
||||
|
||||
string page = set.OutputManpage(info);
|
||||
|
||||
Assert.Contains(".RS", page);
|
||||
Assert.Contains(".RE", page);
|
||||
Assert.Contains(".B \\-\\-output", page);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OutputManpage_HonorsCustomOptionsHeading()
|
||||
{
|
||||
var set = BuildSampleSet();
|
||||
var info = new ManpageInfo("sample") { OptionsHeading = "COMMANDS" };
|
||||
|
||||
string page = set.OutputManpage(info);
|
||||
|
||||
Assert.Contains(".SH COMMANDS", page);
|
||||
Assert.DoesNotContain(".SH OPTIONS", page);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OutputManpage_LeavesBlankDateForStamping()
|
||||
{
|
||||
var set = BuildSampleSet();
|
||||
var info = new ManpageInfo("sample") { Version = "sample 1.0" };
|
||||
|
||||
string page = set.OutputManpage(info);
|
||||
|
||||
// Section, then an empty date field, then the version
|
||||
Assert.Contains(".TH \"SAMPLE\" \"1\" \"\" \"sample 1.0\"", page);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OutputManpage_LinesWithinWidth()
|
||||
{
|
||||
var set = BuildSampleSet();
|
||||
var info = new ManpageInfo("sample") { Description = "do sample things" };
|
||||
|
||||
string page = set.OutputManpage(info, includeVerbose: true);
|
||||
|
||||
foreach (string line in page.Split('\n'))
|
||||
{
|
||||
Assert.True(line.Length <= 78, $"Line exceeds 78 columns: {line}");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OutputManpage_FileOverload_WritesSameContent()
|
||||
{
|
||||
var set = BuildSampleSet();
|
||||
var info = new ManpageInfo("sample");
|
||||
|
||||
string expected = set.OutputManpage(info);
|
||||
string path = Path.Combine(Path.GetTempPath(), $"sabretools-manpage-{Guid.NewGuid():N}.1");
|
||||
try
|
||||
{
|
||||
set.OutputManpage(path, info);
|
||||
string actual = File.ReadAllText(path);
|
||||
Assert.Equal(expected, actual);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (File.Exists(path))
|
||||
File.Delete(path);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Build a representative command set with nested inputs and detail text
|
||||
/// </summary>
|
||||
private static CommandSet BuildSampleSet()
|
||||
{
|
||||
var set = new CommandSet("Sample header line");
|
||||
|
||||
var help = new MockFeature("Help", ["?", "h", "help"], "Show this help",
|
||||
"Built-in to most of the programs is a basic help text.");
|
||||
set.Add(help);
|
||||
|
||||
var convert = new MockFeature("Convert", "--convert", "Convert input files",
|
||||
"Converts the provided input files into the desired output format.");
|
||||
convert.Add(new StringInput("output", "--output", "Set the output path"));
|
||||
convert.Add(new FlagInput("force", "--force", "Overwrite existing files"));
|
||||
set.Add(convert);
|
||||
|
||||
return set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mock Feature implementation for testing
|
||||
/// </summary>
|
||||
|
||||
80
SabreTools.CommandLine.Test/Features/ManpageTests.cs
Normal file
80
SabreTools.CommandLine.Test/Features/ManpageTests.cs
Normal file
@@ -0,0 +1,80 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using SabreTools.CommandLine.Features;
|
||||
using SabreTools.CommandLine.Inputs;
|
||||
using Xunit;
|
||||
|
||||
namespace SabreTools.CommandLine.Test.Features
|
||||
{
|
||||
public class ManpageTests
|
||||
{
|
||||
[Fact]
|
||||
public void ManpageFeature_WritesPageToStandardOutput()
|
||||
{
|
||||
var set = BuildSampleSet();
|
||||
var info = new ManpageInfo("sample") { Version = "sample 1.0" };
|
||||
set.Add(new Manpage(info));
|
||||
|
||||
var original = Console.Out;
|
||||
var writer = new StringWriter();
|
||||
string output;
|
||||
try
|
||||
{
|
||||
Console.SetOut(writer);
|
||||
bool result = set.ProcessArgs(["man"]);
|
||||
Assert.True(result);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Console.SetOut(original);
|
||||
}
|
||||
|
||||
output = writer.ToString();
|
||||
Assert.Contains(".TH \"SAMPLE\" \"1\"", output);
|
||||
Assert.Contains(".SH NAME", output);
|
||||
Assert.Contains(".B \\-\\-convert", output);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Build a representative command set with nested inputs and detail text
|
||||
/// </summary>
|
||||
private static CommandSet BuildSampleSet()
|
||||
{
|
||||
var set = new CommandSet("Sample header line");
|
||||
|
||||
var help = new MockFeature("Help", ["?", "h", "help"], "Show this help",
|
||||
"Built-in to most of the programs is a basic help text.");
|
||||
set.Add(help);
|
||||
|
||||
var convert = new MockFeature("Convert", "--convert", "Convert input files",
|
||||
"Converts the provided input files into the desired output format.");
|
||||
convert.Add(new StringInput("output", "--output", "Set the output path"));
|
||||
convert.Add(new FlagInput("force", "--force", "Overwrite existing files"));
|
||||
set.Add(convert);
|
||||
|
||||
return set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mock Feature implementation for testing
|
||||
/// </summary>
|
||||
private class MockFeature : Feature
|
||||
{
|
||||
public MockFeature(string name, string flag, string description, string? detailed = null)
|
||||
: base(name, flag, description, detailed)
|
||||
{
|
||||
}
|
||||
|
||||
public MockFeature(string name, string[] flags, string description, string? detailed = null)
|
||||
: base(name, flags, description, detailed)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool Execute() => true;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool VerifyInputs() => true;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user