Skip to content

Commit d1b6b5b

Browse files
committed
Add an abstraction for help text generation
1 parent 382abcf commit d1b6b5b

File tree

3 files changed

+206
-83
lines changed

3 files changed

+206
-83
lines changed

src/CommandLineUtils/CommandLineApplication.cs

Lines changed: 66 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
using System.Linq;
1111
using System.Text;
1212
using System.Threading.Tasks;
13+
using McMaster.Extensions.CommandLineUtils.HelpText;
1314

1415
namespace McMaster.Extensions.CommandLineUtils
1516
{
@@ -20,6 +21,7 @@ namespace McMaster.Extensions.CommandLineUtils
2021
public partial class CommandLineApplication
2122
{
2223
private IConsole _console;
24+
private IHelpTextGenerator _helpTextGenerator;
2325

2426
/// <summary>
2527
/// Initializes a new instance of <see cref="CommandLineApplication"/>.
@@ -45,6 +47,17 @@ public CommandLineApplication(IConsole console)
4547
/// <param name="workingDirectory">The current working directory.</param>
4648
/// <param name="throwOnUnexpectedArg">Initial value for <see cref="ThrowOnUnexpectedArgument"/>.</param>
4749
public CommandLineApplication(IConsole console, string workingDirectory, bool throwOnUnexpectedArg)
50+
: this(DefaultHelpTextGenerator.Singleton, console, workingDirectory, throwOnUnexpectedArg)
51+
{ }
52+
53+
/// <summary>
54+
/// Initializes a new instance of <see cref="CommandLineApplication"/>.
55+
/// </summary>
56+
/// <param name="helpTextGenerator">The help text generator to use.</param>
57+
/// <param name="console">The console implementation to use.</param>
58+
/// <param name="workingDirectory">The current working directory.</param>
59+
/// <param name="throwOnUnexpectedArg">Initial value for <see cref="ThrowOnUnexpectedArgument"/>.</param>
60+
public CommandLineApplication(IHelpTextGenerator helpTextGenerator, IConsole console, string workingDirectory, bool throwOnUnexpectedArg)
4861
{
4962
if (console == null)
5063
{
@@ -62,13 +75,14 @@ public CommandLineApplication(IConsole console, string workingDirectory, bool th
6275
Arguments = new List<CommandArgument>();
6376
Commands = new List<CommandLineApplication>();
6477
RemainingArguments = new List<string>();
78+
HelpTextGenerator = helpTextGenerator;
6579
Invoke = () => 0;
6680
ValidationErrorHandler = DefaultValidationErrorHandler;
6781
SetConsole(console);
6882
}
6983

7084
private CommandLineApplication(CommandLineApplication parent, string name, bool throwOnUnexpectedArg)
71-
: this(parent._console, parent.WorkingDirectory, throwOnUnexpectedArg)
85+
: this(parent._helpTextGenerator, parent._console, parent.WorkingDirectory, throwOnUnexpectedArg)
7286
{
7387
Name = name;
7488
Parent = parent;
@@ -81,6 +95,15 @@ private CommandLineApplication(CommandLineApplication parent, string name, bool
8195
/// </summary>
8296
public CommandLineApplication Parent { get; set; }
8397

98+
/// <summary>
99+
/// The help text generator to use.
100+
/// </summary>
101+
public IHelpTextGenerator HelpTextGenerator
102+
{
103+
get => _helpTextGenerator;
104+
set => _helpTextGenerator = value ?? throw new ArgumentNullException(nameof(value));
105+
}
106+
84107
/// <summary>
85108
/// The short name of the command. When this is a subcommand, it is the name of the word used to invoke the subcommand.
86109
/// </summary>
@@ -449,30 +472,29 @@ public void ShowHint()
449472
/// <summary>
450473
/// Show full help.
451474
/// </summary>
452-
/// <param name="commandName">The subcommand for which to show help. Leave null to show for the current command.</param>
453-
public void ShowHelp(string commandName = null)
475+
public void ShowHelp()
454476
{
455477
for (var cmd = this; cmd != null; cmd = cmd.Parent)
456478
{
457479
cmd.IsShowingInformation = true;
458480
}
459481

460-
Out.WriteLine(GetHelpText(commandName));
482+
_helpTextGenerator.Generate(this, Out);
461483
}
462484

463485
/// <summary>
464-
/// Produces help text describing command usage.
486+
/// This method has been marked as obsolete and will be removed in a future version.
487+
/// The recommended replacement is <see cref="ShowHelp()" />.
465488
/// </summary>
466-
/// <param name="commandName"></param>
467-
/// <returns></returns>
468-
public virtual string GetHelpText(string commandName = null)
489+
/// <param name="commandName">The subcommand for which to show help. Leave null to show for the current command.</param>
490+
[Obsolete("This method has been marked as obsolete and will be removed in a future version." +
491+
"The recommended replacement is ShowHelp()")]
492+
public void ShowHelp(string commandName = null)
469493
{
470-
var headerBuilder = new StringBuilder("Usage:");
471-
for (var cmd = this; cmd != null; cmd = cmd.Parent)
494+
if (commandName == null)
472495
{
473-
headerBuilder.Insert(6, string.Format(" {0}", cmd.Name));
496+
ShowHelp();
474497
}
475-
476498
CommandLineApplication target;
477499

478500
if (commandName == null || string.Equals(Name, commandName, StringComparison.OrdinalIgnoreCase))
@@ -483,94 +505,55 @@ public virtual string GetHelpText(string commandName = null)
483505
{
484506
target = Commands.SingleOrDefault(cmd => string.Equals(cmd.Name, commandName, StringComparison.OrdinalIgnoreCase));
485507

486-
if (target != null)
487-
{
488-
headerBuilder.AppendFormat(" {0}", commandName);
489-
}
490-
else
508+
if (target == null)
491509
{
492510
// The command name is invalid so don't try to show help for something that doesn't exist
493511
target = this;
494512
}
495-
496513
}
497514

498-
var optionsBuilder = new StringBuilder();
499-
var commandsBuilder = new StringBuilder();
500-
var argumentsBuilder = new StringBuilder();
515+
target.ShowHelp();
516+
}
501517

502-
var arguments = target.Arguments.Where(a => a.ShowInHelpText).ToList();
503-
if (arguments.Any())
504-
{
505-
headerBuilder.Append(" [arguments]");
518+
/// <summary>
519+
/// Produces help text describing command usage.
520+
/// </summary>
521+
/// <returns>The help text.</returns>
522+
public virtual string GetHelpText()
523+
{
524+
var sb = new StringBuilder();
525+
_helpTextGenerator.Generate(this, new StringWriter(sb));
526+
return sb.ToString();
527+
}
506528

507-
argumentsBuilder.AppendLine();
508-
argumentsBuilder.AppendLine("Arguments:");
509-
var maxArgLen = arguments.Max(a => a.Name.Length);
510-
var outputFormat = string.Format(" {{0, -{0}}}{{1}}", maxArgLen + 2);
511-
foreach (var arg in arguments)
512-
{
513-
argumentsBuilder.AppendFormat(outputFormat, arg.Name, arg.Description);
514-
argumentsBuilder.AppendLine();
515-
}
516-
}
529+
/// <summary>
530+
/// This method has been marked as obsolete and will be removed in a future version.
531+
/// The recommended replacement is <see cref="GetHelpText()" />
532+
/// </summary>
533+
/// <param name="commandName"></param>
534+
/// <returns></returns>
535+
[Obsolete("This method has been marked as obsolete and will be removed in a future version." +
536+
"The recommended replacement is GetHelpText()")]
537+
public virtual string GetHelpText(string commandName = null)
538+
{
539+
CommandLineApplication target;
517540

518-
var options = target.GetOptions().Where(o => o.ShowInHelpText).ToList();
519-
if (options.Any())
541+
if (commandName == null || string.Equals(Name, commandName, StringComparison.OrdinalIgnoreCase))
520542
{
521-
headerBuilder.Append(" [options]");
522-
523-
optionsBuilder.AppendLine();
524-
optionsBuilder.AppendLine("Options:");
525-
var maxOptLen = options.Max(o => o.Template?.Length ?? 0);
526-
var outputFormat = string.Format(" {{0, -{0}}}{{1}}", maxOptLen + 2);
527-
foreach (var opt in options)
528-
{
529-
optionsBuilder.AppendFormat(outputFormat, opt.Template, opt.Description);
530-
optionsBuilder.AppendLine();
531-
}
543+
target = this;
532544
}
533-
534-
var commands = target.Commands.Where(c => c.ShowInHelpText).ToList();
535-
if (commands.Any())
545+
else
536546
{
537-
headerBuilder.Append(" [command]");
538-
539-
commandsBuilder.AppendLine();
540-
commandsBuilder.AppendLine("Commands:");
541-
var maxCmdLen = commands.Max(c => c.Name?.Length ?? 0);
542-
var outputFormat = string.Format(" {{0, -{0}}}{{1}}", maxCmdLen + 2);
543-
foreach (var cmd in commands.OrderBy(c => c.Name))
544-
{
545-
commandsBuilder.AppendFormat(outputFormat, cmd.Name, cmd.Description);
546-
commandsBuilder.AppendLine();
547-
}
547+
target = Commands.SingleOrDefault(cmd => string.Equals(cmd.Name, commandName, StringComparison.OrdinalIgnoreCase));
548548

549-
if (OptionHelp != null)
549+
if (target == null)
550550
{
551-
commandsBuilder.AppendLine();
552-
commandsBuilder.AppendFormat($"Use \"{target.Name} [command] --{OptionHelp.LongName}\" for more information about a command.");
553-
commandsBuilder.AppendLine();
551+
// The command name is invalid so don't try to show help for something that doesn't exist
552+
target = this;
554553
}
555554
}
556555

557-
if (target.AllowArgumentSeparator)
558-
{
559-
headerBuilder.Append(" [[--] <arg>...]");
560-
}
561-
562-
headerBuilder.AppendLine();
563-
564-
var nameAndVersion = new StringBuilder();
565-
nameAndVersion.AppendLine(GetFullNameAndVersion());
566-
nameAndVersion.AppendLine();
567-
568-
return nameAndVersion.ToString()
569-
+ headerBuilder.ToString()
570-
+ argumentsBuilder.ToString()
571-
+ optionsBuilder.ToString()
572-
+ commandsBuilder.ToString()
573-
+ target.ExtendedHelpText;
556+
return target.GetHelpText();
574557
}
575558

576559
/// <summary>
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
// Copyright (c) Nate McMaster.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System.Collections.Generic;
5+
using System.IO;
6+
using System.Linq;
7+
using System.Text;
8+
9+
namespace McMaster.Extensions.CommandLineUtils.HelpText
10+
{
11+
/// <summary>
12+
/// A default implementation of help text generation.
13+
/// </summary>
14+
public class DefaultHelpTextGenerator : IHelpTextGenerator
15+
{
16+
/// <summary>
17+
/// A singleton instance of <see cref="DefaultHelpTextGenerator" />.
18+
/// </summary>
19+
public static DefaultHelpTextGenerator Singleton { get; } = new DefaultHelpTextGenerator();
20+
21+
private DefaultHelpTextGenerator() { }
22+
23+
/// <inheritdoc />
24+
public void Generate(CommandLineApplication application, TextWriter output)
25+
{
26+
var nameAndVersion = application.GetFullNameAndVersion();
27+
if (!string.IsNullOrEmpty(nameAndVersion))
28+
{
29+
output.WriteLine(nameAndVersion);
30+
output.WriteLine();
31+
}
32+
33+
output.Write("Usage:");
34+
var stack = new Stack<string>();
35+
for (var cmd = application; cmd != null; cmd = cmd.Parent)
36+
{
37+
stack.Push(cmd.Name);
38+
}
39+
40+
while (stack.Count > 0)
41+
{
42+
output.Write(' ');
43+
output.Write(stack.Pop());
44+
}
45+
46+
var arguments = application.Arguments.Where(a => a.ShowInHelpText).ToList();
47+
var options = application.GetOptions().Where(o => o.ShowInHelpText).ToList();
48+
var commands = application.Commands.Where(c => c.ShowInHelpText).ToList();
49+
50+
if (arguments.Any())
51+
{
52+
output.Write(" [arguments]");
53+
}
54+
55+
if (options.Any())
56+
{
57+
output.Write(" [options]");
58+
}
59+
60+
if (commands.Any())
61+
{
62+
output.Write(" [command]");
63+
}
64+
65+
if (application.AllowArgumentSeparator)
66+
{
67+
output.Write(" [[--] <arg>...]");
68+
}
69+
70+
output.WriteLine();
71+
72+
if (arguments.Any())
73+
{
74+
output.WriteLine();
75+
output.WriteLine("Arguments:");
76+
var maxArgLen = arguments.Max(a => a.Name.Length);
77+
var outputFormat = string.Format(" {{0, -{0}}}{{1}}", maxArgLen + 2);
78+
foreach (var arg in arguments)
79+
{
80+
output.Write(outputFormat, arg.Name, arg.Description);
81+
output.WriteLine();
82+
}
83+
}
84+
85+
if (options.Any())
86+
{
87+
output.WriteLine();
88+
output.WriteLine("Options:");
89+
var maxOptLen = options.Max(o => o.Template?.Length ?? 0);
90+
var outputFormat = string.Format(" {{0, -{0}}}{{1}}", maxOptLen + 2);
91+
foreach (var opt in options)
92+
{
93+
output.Write(outputFormat, opt.Template, opt.Description);
94+
output.WriteLine();
95+
}
96+
}
97+
98+
if (commands.Any())
99+
{
100+
output.WriteLine();
101+
output.WriteLine("Commands:");
102+
var maxCmdLen = commands.Max(c => c.Name?.Length ?? 0);
103+
var outputFormat = string.Format(" {{0, -{0}}}{{1}}", maxCmdLen + 2);
104+
foreach (var cmd in commands.OrderBy(c => c.Name))
105+
{
106+
output.Write(outputFormat, cmd.Name, cmd.Description);
107+
output.WriteLine();
108+
}
109+
110+
if (application.OptionHelp != null)
111+
{
112+
output.WriteLine();
113+
output.WriteLine($"Use \"{application.Name} [command] --{application.OptionHelp.LongName}\" for more information about a command.");
114+
}
115+
}
116+
117+
output.Write(application.ExtendedHelpText);
118+
}
119+
}
120+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Copyright (c) Nate McMaster.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System.IO;
5+
6+
namespace McMaster.Extensions.CommandLineUtils.HelpText
7+
{
8+
/// <summary>
9+
/// Generates help text for a command line application.
10+
/// </summary>
11+
public interface IHelpTextGenerator
12+
{
13+
/// <summary>
14+
/// Generate help text for the application.
15+
/// </summary>
16+
/// <param name="application"></param>
17+
/// <param name="output"></param>
18+
void Generate(CommandLineApplication application, TextWriter output);
19+
}
20+
}

0 commit comments

Comments
 (0)