Skip to content

Commit 9e6b225

Browse files
committed
* Ability to specify multiple Documentation attributes on the same field to generate multiple rows.
* Ability to specify multiple strings on a Documentation attribute to generate multiple columns.
1 parent d43c9b4 commit 9e6b225

File tree

4 files changed

+64
-56
lines changed

4 files changed

+64
-56
lines changed

Src/Attributes.cs

Lines changed: 25 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -65,24 +65,25 @@ public CommandGroupAttribute() { }
6565
}
6666

6767
/// <summary>
68-
/// Use this attribute to link a command-line option or command with the help text that describes (documents) it. Suitable
69-
/// for single-language applications only. See Remarks.</summary>
68+
/// Use this attribute to write the help text that describes (documents) a command-line option or command. Specifying
69+
/// multiple strings will create multiple columns in the table. See Remarks.</summary>
7070
/// <remarks>
7171
/// This attribute specifies the documentation in plain text. All characters are printed exactly as specified. You may
7272
/// wish to use <see cref="DocumentationRhoMLAttribute"/> to specify documentation with special markup for
7373
/// command-line-related concepts, as well as <see cref="DocumentationEggsMLAttribute"/> for an alternative markup
7474
/// language without command-line specific concepts.</remarks>
75-
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Class | AttributeTargets.Method, Inherited = false, AllowMultiple = false), RummageKeepUsersReflectionSafe]
76-
public class DocumentationAttribute(string documentation) : Attribute
75+
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Class | AttributeTargets.Method, Inherited = false, AllowMultiple = true), RummageKeepUsersReflectionSafe]
76+
public class DocumentationAttribute(params string[] documentation) : Attribute
7777
{
7878
/// <summary>
79-
/// Gets the console-colored documentation string. Note that this property may throw if the text couldn't be parsed
80-
/// where applicable.</summary>
81-
public virtual ConsoleColoredString Text => OriginalText;
79+
/// Gets the console-colored documentation string. Multiple strings may be returned to create multiple columns. Note
80+
/// that this property may throw if the text couldn't be parsed where applicable.</summary>
81+
public virtual ConsoleColoredString[] Texts => _converted ??= OriginalTexts.Select(s => (ConsoleColoredString) s).ToArray();
82+
private ConsoleColoredString[] _converted;
8283
/// <summary>Gets a string describing the documentation format to the programmer (not seen by the users).</summary>
8384
public virtual string OriginalFormat => "Plain text";
8485
/// <summary>Gets the original documentation string exactly as specified in the attribute.</summary>
85-
public string OriginalText { get; private set; } = documentation;
86+
public string[] OriginalTexts { get; private set; } = documentation;
8687
}
8788

8889
/// <summary>
@@ -94,27 +95,29 @@ public class DocumentationLiteralAttribute(string documentation) : Documentation
9495
}
9596

9697
/// <summary>
97-
/// Use this attribute to link a command-line option or command with the help text that describes (documents) it. Suitable
98-
/// for single-language applications only. The documentation is to be specified in <see cref="EggsML"/>, which is
99-
/// interpreted as described in <see cref="CommandLineParser.Colorize(EggsNode)"/>. See also <see
100-
/// cref="DocumentationRhoMLAttribute"/> and <see cref="DocumentationAttribute"/>.</summary>
101-
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Class | AttributeTargets.Method, Inherited = false, AllowMultiple = false), RummageKeepUsersReflectionSafe]
102-
public class DocumentationEggsMLAttribute(string documentation) : DocumentationAttribute(documentation)
98+
/// Use this attribute to write the help text that describes (documents) a command-line option or command. Specifying
99+
/// multiple strings will create multiple columns in the table. The documentation is to be specified in <see
100+
/// cref="EggsML"/>, which is interpreted as described in <see cref="CommandLineParser.Colorize(EggsNode)"/>.</summary>
101+
/// <seealso cref="DocumentationAttribute"/>
102+
/// <seealso cref="DocumentationRhoMLAttribute"/>
103+
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Class | AttributeTargets.Method, Inherited = false, AllowMultiple = true), RummageKeepUsersReflectionSafe]
104+
public class DocumentationEggsMLAttribute(params string[] documentation) : DocumentationAttribute(documentation)
103105
{
104106
/// <summary>Gets a string describing the documentation format to the programmer (not seen by the users).</summary>
105107
public override string OriginalFormat => "EggsML";
106108
/// <summary>
107109
/// Gets the console-colored documentation string. Note that this property may throw if the text couldn't be parsed
108110
/// where applicable.</summary>
109-
public override ConsoleColoredString Text => _parsed ??= CommandLineParser.Colorize(EggsML.Parse(OriginalText));
110-
private ConsoleColoredString _parsed;
111+
public override ConsoleColoredString[] Texts => _parsed ??= OriginalTexts.Select(text => CommandLineParser.Colorize(EggsML.Parse(text))).ToArray();
112+
private ConsoleColoredString[] _parsed;
111113
}
112114

113115
/// <summary>
114-
/// Use this attribute to link a command-line option or command with the help text that describes (documents) it. Suitable
115-
/// for single-language applications only. The documentation is to be specified in <see cref="RhoML"/>, which is
116-
/// interpreted as described in <see cref="CommandLineParser.Colorize(RhoElement)"/>. See also <see
117-
/// cref="DocumentationAttribute"/>.</summary>
116+
/// Use this attribute to write the help text that describes (documents) a command-line option or command. Specifying
117+
/// multiple strings will create multiple columns in the table. The documentation is to be specified in <see
118+
/// cref="RhoML"/>, which is interpreted as described in <see cref="CommandLineParser.Colorize(RhoElement)"/>.</summary>
119+
/// <seealso cref="DocumentationAttribute"/>
120+
/// <seealso cref="DocumentationEggsMLAttribute"/>
118121
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Class | AttributeTargets.Method, Inherited = false, AllowMultiple = false), RummageKeepUsersReflectionSafe]
119122
public class DocumentationRhoMLAttribute(string documentation) : DocumentationAttribute(documentation)
120123
{
@@ -123,8 +126,8 @@ public class DocumentationRhoMLAttribute(string documentation) : DocumentationAt
123126
/// <summary>
124127
/// Gets the console-colored documentation string. Note that this property may throw if the text couldn't be parsed
125128
/// where applicable.</summary>
126-
public override ConsoleColoredString Text => _parsed ??= CommandLineParser.Colorize(RhoML.Parse(OriginalText));
127-
private ConsoleColoredString _parsed;
129+
public override ConsoleColoredString[] Texts => _parsed ??= OriginalTexts.Select(text => CommandLineParser.Colorize(RhoML.Parse(text))).ToArray();
130+
private ConsoleColoredString[] _parsed;
128131
}
129132

130133
/// <summary>

Src/CommandLineParser.cs

Lines changed: 36 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -624,12 +624,18 @@ private static Func<int, ConsoleColoredString> getHelpGenerator(Type type, Func<
624624
}
625625
helpString.Add(ConsoleColoredString.NewLine);
626626

627-
// Word-wrap the documentation for the command (if any)
627+
// Output the documentation for the command (if any)
628628
var doc = getDocumentation(type, helpProcessor);
629-
foreach (var line in doc.WordWrap(wrapWidth))
629+
if (doc.Any(d => d.Length > 0))
630630
{
631-
helpString.Add(line);
632-
helpString.Add(ConsoleColoredString.NewLine);
631+
var docTable = new TextTable { MaxWidth = wrapWidth, ColumnSpacing = fmtOpt.ColumnSpacing, RowSpacing = fmtOpt.RowSpacing, StretchLastCell = true };
632+
foreach (var row in doc)
633+
{
634+
for (var x = 0; x < row.Length; x++)
635+
docTable.AddCell(row[x]);
636+
docTable.FinishRow();
637+
}
638+
helpString.Add(docTable.ToColoredString());
633639
}
634640

635641

@@ -650,7 +656,7 @@ private static Func<int, ConsoleColoredString> getHelpGenerator(Type type, Func<
650656
var section = field.GetCustomAttribute<SectionAttribute>();
651657
if (curTable == null || lastMandatory != mandatory || section != null)
652658
{
653-
curTable = new TextTable { MaxWidth = wrapWidth - fmtOpt.LeftMargin, ColumnSpacing = fmtOpt.ColumnSpacing, RowSpacing = fmtOpt.RowSpacing, LeftMargin = fmtOpt.LeftMargin };
659+
curTable = new TextTable { MaxWidth = wrapWidth - fmtOpt.LeftMargin, ColumnSpacing = fmtOpt.ColumnSpacing, RowSpacing = fmtOpt.RowSpacing, LeftMargin = fmtOpt.LeftMargin, StretchLastCell = true };
654660
paramsTables.Add((section?.Heading ?? $"{(mandatory ? "Required" : "Optional")} parameters:", curTable));
655661
curRow = 0;
656662
}
@@ -689,18 +695,22 @@ private static bool createParameterHelpRow(ref int row, TextTable table, FieldIn
689695
var anyCommandsWithSuboptions = false;
690696
var cmdName = "<".Color(CmdLineColor.FieldBrackets) + field.Name.Color(CmdLineColor.Field) + ">".Color(CmdLineColor.FieldBrackets);
691697

698+
int plonkDocumentation(int startCol, int startRow, ConsoleColoredString[][] docs, bool min1)
699+
{
700+
for (var r = 0; r < docs.Length; r++)
701+
for (var c = 0; c < docs[r].Length; c++)
702+
table.SetCell(startCol + c, startRow + r, docs[r][c]);
703+
return Math.Max(docs.Length, min1 ? 1 : 0);
704+
}
705+
692706
if (field.FieldType.IsEnum)
693707
{
694708
// ### ENUM fields, positional
695709
if (positional)
696710
{
697711
var topRow = row;
698712
var doc = getDocumentation(field, helpProcessor);
699-
if (doc.Length > 0 || field.FieldType.GetFields(BindingFlags.Static | BindingFlags.Public).All(el => el.IsDefined<UndocumentedAttribute>() || !el.GetCustomAttributes<CommandNameAttribute>().Any()))
700-
{
701-
table.SetCell(2, row, doc, colSpan: 4);
702-
row++;
703-
}
713+
row += plonkDocumentation(2, row, doc, min1: false);
704714
foreach (var el in field.FieldType.GetFields(BindingFlags.Static | BindingFlags.Public))
705715
{
706716
if (el.IsDefined<UndocumentedAttribute>())
@@ -710,32 +720,31 @@ private static bool createParameterHelpRow(ref int row, TextTable table, FieldIn
710720
continue;
711721
table.SetCell(2, row, attr.Names.Where(n => n.Length <= 2).Select(s => s.Color(CmdLineColor.EnumValue)).JoinColoredString(", "), noWrap: true);
712722
table.SetCell(3, row, attr.Names.Where(n => n.Length > 2).Select(s => s.Color(CmdLineColor.EnumValue)).JoinColoredString(Environment.NewLine), noWrap: true);
713-
table.SetCell(4, row, getDocumentation(el, helpProcessor), colSpan: 2);
714-
row++;
723+
row += plonkDocumentation(4, row, getDocumentation(el, helpProcessor), min1: true);
715724
}
725+
if (row == topRow)
726+
row++;
716727
table.SetCell(0, topRow, cmdName, noWrap: true, colSpan: 2, rowSpan: row - topRow);
717728
}
718729
// ### ENUM fields, “-x foo” scheme
719730
else if (field.IsDefined<OptionAttribute>())
720731
{
721732
var topRow = row;
722-
row++;
733+
row += plonkDocumentation(2, row, getDocumentation(field, helpProcessor), min1: false);
723734
foreach (var el in field.FieldType.GetFields(BindingFlags.Static | BindingFlags.Public).Where(e => !e.IsDefined<UndocumentedAttribute>()))
724735
{
725736
var attr = el.GetCustomAttributes<CommandNameAttribute>().FirstOrDefault();
726737
if (attr == null) // skip the default value
727738
continue;
728739
table.SetCell(3, row, attr.Names.Where(n => n.Length <= 2).Select(s => s.Color(CmdLineColor.EnumValue)).JoinColoredString(", "), noWrap: true);
729740
table.SetCell(4, row, attr.Names.Where(n => n.Length > 2).Select(s => s.Color(CmdLineColor.EnumValue)).JoinColoredString(Environment.NewLine), noWrap: true);
730-
table.SetCell(5, row, getDocumentation(el, helpProcessor));
731-
row++;
741+
row += plonkDocumentation(5, row, getDocumentation(el, helpProcessor), min1: true);
732742
}
733-
if (row == topRow + 1)
734-
throw new InvalidOperationException($"Enum type {field.FieldType.DeclaringType.FullName}.{field.FieldType} has no values (apart from default value for field {field.DeclaringType.FullName}.{field.Name}).");
743+
if (row == topRow)
744+
row++;
735745
table.SetCell(0, topRow, field.GetOrderedOptionAttributeNames().Where(o => !o.StartsWith("--")).OrderBy(cmd => cmd.Length).Select(cmd => cmd.Color(CmdLineColor.Option)).JoinColoredString(", "), noWrap: true, rowSpan: row - topRow);
736746
table.SetCell(1, topRow, field.GetOrderedOptionAttributeNames().Where(o => o.StartsWith("--")).OrderBy(cmd => cmd.Length).Select(cmd => cmd.Color(CmdLineColor.Option)).JoinColoredString(Environment.NewLine), noWrap: true, rowSpan: row - topRow);
737-
table.SetCell(2, topRow, getDocumentation(field, helpProcessor), colSpan: 4);
738-
table.SetCell(2, topRow + 1, cmdName, noWrap: true, rowSpan: row - topRow - 1);
747+
table.SetCell(2, topRow, cmdName, noWrap: true, rowSpan: row - topRow - 1);
739748
}
740749
// ### ENUM fields, “-x” scheme
741750
else
@@ -744,8 +753,7 @@ private static bool createParameterHelpRow(ref int row, TextTable table, FieldIn
744753
{
745754
table.SetCell(0, row, el.GetOrderedOptionAttributeNames().Where(o => !o.StartsWith("--")).OrderBy(cmd => cmd.Length).Select(cmd => cmd.Color(CmdLineColor.Option)).JoinColoredString(", "), noWrap: true);
746755
table.SetCell(1, row, el.GetOrderedOptionAttributeNames().Where(o => o.StartsWith("--")).OrderBy(cmd => cmd.Length).Select(cmd => cmd.Color(CmdLineColor.Option)).JoinColoredString(Environment.NewLine), noWrap: true);
747-
table.SetCell(2, row, getDocumentation(el, helpProcessor), colSpan: 4);
748-
row++;
756+
row += plonkDocumentation(2, row, getDocumentation(el, helpProcessor), min1: true);
749757
}
750758
}
751759
}
@@ -765,25 +773,22 @@ private static bool createParameterHelpRow(ref int row, TextTable table, FieldIn
765773
var names = ty.GetCustomAttributes<CommandNameAttribute>().First().Names;
766774
table.SetCell(2, row, names.Where(n => n.Length <= 2).Select(n => n.Color(CmdLineColor.Command) + asterisk).JoinColoredString(", "), noWrap: true);
767775
table.SetCell(3, row, names.Where(n => n.Length > 2).Select(n => n.Color(CmdLineColor.Command) + asterisk).JoinColoredString(Environment.NewLine), noWrap: true);
768-
table.SetCell(4, row, getDocumentation(ty, helpProcessor), colSpan: 2);
769-
row++;
776+
row += plonkDocumentation(4, row, getDocumentation(ty, helpProcessor), min1: true);
770777
}
771778
table.SetCell(0, origRow, cmdName, colSpan: 2, rowSpan: row - origRow, noWrap: true);
772779
}
773780
// ### All other positional parameters
774781
else if (positional)
775782
{
776783
table.SetCell(0, row, cmdName, noWrap: true, colSpan: 2);
777-
table.SetCell(2, row, getDocumentation(field, helpProcessor), colSpan: 4);
778-
row++;
784+
row += plonkDocumentation(2, row, getDocumentation(field, helpProcessor), min1: true);
779785
}
780786
// ### All other non-positional parameters
781787
else
782788
{
783789
table.SetCell(0, row, field.GetOrderedOptionAttributeNames().Where(o => !o.StartsWith("--")).OrderBy(cmd => cmd.Length).Select(cmd => cmd.Color(CmdLineColor.Option)).JoinColoredString(", "), noWrap: true);
784790
table.SetCell(1, row, field.GetOrderedOptionAttributeNames().Where(o => o.StartsWith("--")).OrderBy(cmd => cmd.Length).Select(cmd => cmd.Color(CmdLineColor.Option)).JoinColoredString(Environment.NewLine), noWrap: true);
785-
table.SetCell(2, row, getDocumentation(field, helpProcessor), colSpan: 4);
786-
row++;
791+
row += plonkDocumentation(2, row, getDocumentation(field, helpProcessor), min1: true);
787792
}
788793
return anyCommandsWithSuboptions;
789794
}
@@ -806,8 +811,8 @@ private static void getFieldsForHelp(Type type, out List<FieldInfo> optionalOpti
806811
}
807812
}
808813

809-
private static ConsoleColoredString getDocumentation(MemberInfo member, Func<ConsoleColoredString, ConsoleColoredString> helpProcessor) =>
810-
member.IsDefined<DocumentationAttribute>() ? helpProcessor(member.GetCustomAttributes<DocumentationAttribute>().Select(d => d.Text ?? "").First()) : "";
814+
private static ConsoleColoredString[][] getDocumentation(MemberInfo member, Func<ConsoleColoredString, ConsoleColoredString> helpProcessor) =>
815+
member.GetCustomAttributes<DocumentationAttribute>().Select(doc => doc.Texts.Select(helpProcessor).ToArray() ?? []).ToArray();
811816

812817
#region Post-build step check
813818

@@ -1049,12 +1054,12 @@ private static void checkDocumentation(IPostBuildReporter rep, MemberInfo member
10491054
return;
10501055

10511056
var attr = member.GetCustomAttributes<DocumentationAttribute>().FirstOrDefault();
1052-
ConsoleColoredString toCheck = null;
1057+
ConsoleColoredString[] toCheck = null;
10531058
if (attr != null)
10541059
{
10551060
try
10561061
{
1057-
toCheck = attr.Text; // this property can throw the first time it's accessed
1062+
toCheck = attr.Texts; // this property can throw the first time it's accessed
10581063
}
10591064
catch (Exception e)
10601065
{

Src/RT.CommandLine.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,12 @@
1717

1818
<ItemGroup>
1919
<PackageReference Include="Microsoft.CSharp" Version="4.7.0" />
20-
<PackageReference Include="RT.PostBuild" Version="2.0.1776" />
20+
<PackageReference Include="RT.PostBuild" Version="2.0.1782" />
2121
</ItemGroup>
2222
<ItemGroup Condition="'$(Configuration)' == 'Debug-locallibs'">
2323
<ProjectReference Include="..\..\RT.Util\RT.Util.Core\RT.Util.Core.csproj" />
2424
</ItemGroup>
2525
<ItemGroup Condition="'$(Configuration)' != 'Debug-locallibs'">
26-
<PackageReference Include="RT.Util.Core" Version="2.0.1777" />
26+
<PackageReference Include="RT.Util.Core" Version="2.0.1782" />
2727
</ItemGroup>
2828
</Project>

0 commit comments

Comments
 (0)