diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratorUtils.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratorUtils.cs
index 77657ad05e..9c711c354f 100644
--- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratorUtils.cs
+++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GeneratorUtils.cs
@@ -1,6 +1,7 @@
using System.Globalization;
using System.Text;
using System.Text.RegularExpressions;
+using ModularPipelines.OptionsGenerator.Models;
namespace ModularPipelines.OptionsGenerator.Generators;
@@ -164,4 +165,130 @@ public static string EscapeIdentifier(string identifier)
return CSharpKeywords.Contains(identifier) ? $"@{identifier}" : identifier;
}
+
+ ///
+ /// Generates the CLI attribute string for an option definition.
+ /// Used by both OptionsClassGenerator and GlobalOptionsBaseGenerator.
+ ///
+ /// The CLI option definition.
+ /// The attribute string (e.g., "CliFlag(\"--verbose\")" or "CliOption(\"--output\")").
+ public static string GenerateCliAttributeString(CliOptionDefinition option)
+ {
+ if (option.IsFlag)
+ {
+ // Use CliFlag for boolean flags
+ var parts = new List { $"\"{option.SwitchName}\"" };
+
+ if (!string.IsNullOrEmpty(option.ShortForm))
+ {
+ parts.Add($"ShortForm = \"{option.ShortForm}\"");
+ }
+
+ return $"CliFlag({string.Join(", ", parts)})";
+ }
+
+ // Use CliOption for value options
+ var optionParts = new List { $"\"{option.SwitchName}\"" };
+
+ if (!string.IsNullOrEmpty(option.ShortForm))
+ {
+ optionParts.Add($"ShortForm = \"{option.ShortForm}\"");
+ }
+
+ if (option.ValueSeparator == "=")
+ {
+ optionParts.Add("Format = OptionFormat.EqualsSeparated");
+ }
+ else if (option.ValueSeparator == ":")
+ {
+ optionParts.Add("Format = OptionFormat.ColonSeparated");
+ }
+ else if (option.ValueSeparator != " " && !string.IsNullOrEmpty(option.ValueSeparator))
+ {
+ optionParts.Add($"CustomSeparator = \"{option.ValueSeparator}\"");
+ }
+
+ if (option.AcceptsMultipleValues)
+ {
+ optionParts.Add("AllowMultiple = true");
+ }
+
+ return $"CliOption({string.Join(", ", optionParts)})";
+ }
+
+ ///
+ /// Generates validation attribute lines for an option with validation constraints.
+ ///
+ /// The StringBuilder to append to.
+ /// The validation constraints.
+ /// The indentation string (defaults to 4 spaces).
+ public static void GenerateValidationAttributes(StringBuilder sb, CliValidationConstraints constraints, string indent = " ")
+ {
+ ArgumentNullException.ThrowIfNull(sb);
+ ArgumentNullException.ThrowIfNull(constraints);
+
+ if (constraints.MinValue.HasValue || constraints.MaxValue.HasValue)
+ {
+ var min = constraints.MinValue ?? int.MinValue;
+ var max = constraints.MaxValue ?? int.MaxValue;
+ sb.AppendLine($"{indent}[Range({min}, {max})]");
+ }
+
+ if (!string.IsNullOrEmpty(constraints.Pattern))
+ {
+ sb.AppendLine($"{indent}[RegularExpression(@\"{constraints.Pattern}\")]");
+ }
+ }
+
+ ///
+ /// Generates XML documentation block for a description.
+ ///
+ /// The StringBuilder to append to.
+ /// The description text.
+ /// The indentation string (defaults to 4 spaces).
+ public static void GenerateXmlDocumentation(StringBuilder sb, string? description, string indent = " ")
+ {
+ ArgumentNullException.ThrowIfNull(sb);
+
+ if (!string.IsNullOrEmpty(description))
+ {
+ sb.AppendLine($"{indent}/// ");
+ sb.AppendLine($"{indent}/// {EscapeXmlComment(description)}");
+ sb.AppendLine($"{indent}/// ");
+ }
+ }
+
+ ///
+ /// Generates a C# method name from CLI command parts.
+ /// Converts command parts to PascalCase method name.
+ /// E.g., ["container", "create"] -> "ContainerCreate", ["build-server"] -> "BuildServer"
+ ///
+ /// The command parts array.
+ /// The method name in PascalCase.
+ public static string GenerateMethodNameFromCommandParts(string[] commandParts)
+ {
+ return string.Join("", commandParts
+ .SelectMany(p => p.Split('-', StringSplitOptions.RemoveEmptyEntries))
+ .Select(p => char.ToUpperInvariant(p[0]) + (p.Length > 1 ? p[1..].ToLowerInvariant() : "")));
+ }
+
+ ///
+ /// Generates a C# method name from the last part of a CLI command.
+ /// Used for sub-domain commands where the method name is derived from the last segment.
+ /// E.g., ["network", "create"] uses "create" -> "Create"
+ ///
+ /// The CLI command definition.
+ /// The method name in PascalCase, or "Execute" if no command parts.
+ public static string GenerateMethodNameFromLastCommandPart(CliCommandDefinition command)
+ {
+ if (command.CommandParts.Length == 0)
+ {
+ return "Execute";
+ }
+
+ var lastPart = command.CommandParts[^1];
+ var parts = lastPart.Split('-', StringSplitOptions.RemoveEmptyEntries);
+ return string.Join("", parts.Select(p =>
+ char.ToUpperInvariant(p[0]) + (p.Length > 1 ? p[1..].ToLowerInvariant() : "")));
+ }
}
diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GlobalOptionsBaseGenerator.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GlobalOptionsBaseGenerator.cs
index 089b19088d..b6471e1b6b 100644
--- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GlobalOptionsBaseGenerator.cs
+++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/GlobalOptionsBaseGenerator.cs
@@ -88,85 +88,19 @@ private static string GenerateBaseOptionsClass(CliToolDefinition tool)
private static void GenerateProperty(StringBuilder sb, CliOptionDefinition option)
{
// XML documentation
- if (!string.IsNullOrEmpty(option.Description))
- {
- sb.AppendLine(" /// ");
- sb.AppendLine($" /// {EscapeXmlComment(option.Description)}");
- sb.AppendLine(" /// ");
- }
+ GeneratorUtils.GenerateXmlDocumentation(sb, option.Description);
// Validation attributes
if (option.ValidationConstraints is not null)
{
- GenerateValidationAttributes(sb, option.ValidationConstraints);
+ GeneratorUtils.GenerateValidationAttributes(sb, option.ValidationConstraints);
}
// Command attribute
- var attribute = GetAttributeString(option);
+ var attribute = GeneratorUtils.GenerateCliAttributeString(option);
sb.AppendLine($" [{attribute}]");
// Property
sb.AppendLine($" public virtual {option.CSharpType} {option.PropertyName} {{ get; set; }}");
}
-
- private static void GenerateValidationAttributes(StringBuilder sb, CliValidationConstraints constraints)
- {
- if (constraints.MinValue.HasValue || constraints.MaxValue.HasValue)
- {
- var min = constraints.MinValue ?? int.MinValue;
- var max = constraints.MaxValue ?? int.MaxValue;
- sb.AppendLine($" [Range({min}, {max})]");
- }
-
- if (!string.IsNullOrEmpty(constraints.Pattern))
- {
- sb.AppendLine($" [RegularExpression(@\"{constraints.Pattern}\")]");
- }
- }
-
- private static string GetAttributeString(CliOptionDefinition option)
- {
- if (option.IsFlag)
- {
- // Use CliFlag for boolean flags
- var parts = new List { $"\"{option.SwitchName}\"" };
-
- if (!string.IsNullOrEmpty(option.ShortForm))
- {
- parts.Add($"ShortForm = \"{option.ShortForm}\"");
- }
-
- return $"CliFlag({string.Join(", ", parts)})";
- }
-
- // Use CliOption for value options
- var optionParts = new List { $"\"{option.SwitchName}\"" };
-
- if (!string.IsNullOrEmpty(option.ShortForm))
- {
- optionParts.Add($"ShortForm = \"{option.ShortForm}\"");
- }
-
- if (option.ValueSeparator == "=")
- {
- optionParts.Add("Format = OptionFormat.EqualsSeparated");
- }
- else if (option.ValueSeparator == ":")
- {
- optionParts.Add("Format = OptionFormat.ColonSeparated");
- }
- else if (option.ValueSeparator != " " && !string.IsNullOrEmpty(option.ValueSeparator))
- {
- optionParts.Add($"CustomSeparator = \"{option.ValueSeparator}\"");
- }
-
- if (option.AcceptsMultipleValues)
- {
- optionParts.Add("AllowMultiple = true");
- }
-
- return $"CliOption({string.Join(", ", optionParts)})";
- }
-
- private static string EscapeXmlComment(string text) => GeneratorUtils.EscapeXmlComment(text);
}
diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/OptionsClassGenerator.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/OptionsClassGenerator.cs
index f355cf22de..bf41c9dd3d 100644
--- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/OptionsClassGenerator.cs
+++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/OptionsClassGenerator.cs
@@ -65,12 +65,7 @@ private static string GenerateOptionsClass(CliCommandDefinition command, CliTool
sb.AppendLine();
// XML documentation
- if (!string.IsNullOrEmpty(command.Description))
- {
- sb.AppendLine("/// ");
- sb.AppendLine($"/// {EscapeXmlComment(command.Description)}");
- sb.AppendLine("/// ");
- }
+ GeneratorUtils.GenerateXmlDocumentation(sb, command.Description, "");
// Class attributes
sb.AppendLine("[ExcludeFromCodeCoverage]");
@@ -133,7 +128,7 @@ private static void GenerateClassDeclaration(StringBuilder sb, CliCommandDefinit
foreach (var opt in requiredOptions)
{
- var attr = GetAttributeString(opt);
+ var attr = GeneratorUtils.GenerateCliAttributeString(opt);
parameters.Add($" [property: {attr}] {opt.CSharpType.TrimEnd('?')} {opt.PropertyName}");
existingNames.Add(opt.PropertyName);
}
@@ -162,94 +157,25 @@ private static void GenerateClassDeclaration(StringBuilder sb, CliCommandDefinit
private static void GenerateProperty(StringBuilder sb, CliOptionDefinition option)
{
// XML documentation
- if (!string.IsNullOrEmpty(option.Description))
- {
- sb.AppendLine(" /// ");
- sb.AppendLine($" /// {EscapeXmlComment(option.Description)}");
- sb.AppendLine(" /// ");
- }
+ GeneratorUtils.GenerateXmlDocumentation(sb, option.Description);
// Validation attributes
if (option.ValidationConstraints is not null)
{
- GenerateValidationAttributes(sb, option.ValidationConstraints);
+ GeneratorUtils.GenerateValidationAttributes(sb, option.ValidationConstraints);
}
// Command attribute
- var attribute = GetAttributeString(option);
+ var attribute = GeneratorUtils.GenerateCliAttributeString(option);
sb.AppendLine($" [{attribute}]");
// Property
sb.AppendLine($" public {option.CSharpType} {option.PropertyName} {{ get; set; }}");
}
- private static void GenerateValidationAttributes(StringBuilder sb, CliValidationConstraints constraints)
- {
- if (constraints.MinValue.HasValue || constraints.MaxValue.HasValue)
- {
- var min = constraints.MinValue ?? int.MinValue;
- var max = constraints.MaxValue ?? int.MaxValue;
- sb.AppendLine($" [Range({min}, {max})]");
- }
-
- if (!string.IsNullOrEmpty(constraints.Pattern))
- {
- sb.AppendLine($" [RegularExpression(@\"{constraints.Pattern}\")]");
- }
- }
-
- private static string GetAttributeString(CliOptionDefinition option)
- {
- if (option.IsFlag)
- {
- // Use CliFlag for boolean flags
- var parts = new List { $"\"{option.SwitchName}\"" };
-
- if (!string.IsNullOrEmpty(option.ShortForm))
- {
- parts.Add($"ShortForm = \"{option.ShortForm}\"");
- }
-
- return $"CliFlag({string.Join(", ", parts)})";
- }
-
- // Use CliOption for value options
- var optionParts = new List { $"\"{option.SwitchName}\"" };
-
- if (!string.IsNullOrEmpty(option.ShortForm))
- {
- optionParts.Add($"ShortForm = \"{option.ShortForm}\"");
- }
-
- if (option.ValueSeparator == "=")
- {
- optionParts.Add("Format = OptionFormat.EqualsSeparated");
- }
- else if (option.ValueSeparator == ":")
- {
- optionParts.Add("Format = OptionFormat.ColonSeparated");
- }
- else if (option.ValueSeparator != " " && !string.IsNullOrEmpty(option.ValueSeparator))
- {
- optionParts.Add($"CustomSeparator = \"{option.ValueSeparator}\"");
- }
-
- if (option.AcceptsMultipleValues)
- {
- optionParts.Add("AllowMultiple = true");
- }
-
- return $"CliOption({string.Join(", ", optionParts)})";
- }
-
private static void GeneratePositionalArgument(StringBuilder sb, CliPositionalArgument positional)
{
- if (!string.IsNullOrEmpty(positional.Description))
- {
- sb.AppendLine(" /// ");
- sb.AppendLine($" /// {EscapeXmlComment(positional.Description)}");
- sb.AppendLine(" /// ");
- }
+ GeneratorUtils.GenerateXmlDocumentation(sb, positional.Description);
var attrString = GetPositionalAttributeString(positional);
sb.AppendLine($" [{attrString}]");
@@ -282,6 +208,4 @@ private static string GetPositionalAttributeString(CliPositionalArgument positio
return $"CliArgument({string.Join(", ", parts)})";
}
-
- private static string EscapeXmlComment(string text) => GeneratorUtils.EscapeXmlComment(text);
}
diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/ServiceImplementationGenerator.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/ServiceImplementationGenerator.cs
index d28721eb75..c39c3467d5 100644
--- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/ServiceImplementationGenerator.cs
+++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/ServiceImplementationGenerator.cs
@@ -138,7 +138,7 @@ private static string GenerateMainServiceClass(CliToolDefinition tool)
StringComparer.OrdinalIgnoreCase);
var nonCollidingRootCommands = rootCommands
- .Where(c => !subDomainNames.Contains(GenerateMethodName(c)))
+ .Where(c => !subDomainNames.Contains(GeneratorUtils.GenerateMethodNameFromCommandParts(c.CommandParts)))
.ToList();
if (nonCollidingRootCommands.Count > 0)
@@ -162,7 +162,7 @@ private static string GenerateMainServiceClass(CliToolDefinition tool)
private static void GenerateMethod(StringBuilder sb, CliCommandDefinition command, CliToolDefinition tool)
{
- var methodName = GenerateMethodName(command);
+ var methodName = GeneratorUtils.GenerateMethodNameFromCommandParts(command.CommandParts);
var hasRequiredParams = command.RequiredOptions.Count > 0 ||
command.PositionalArguments.Any(p => p.IsRequired);
@@ -190,14 +190,4 @@ private static void GenerateMethod(StringBuilder sb, CliCommandDefinition comman
sb.AppendLine(" }");
}
-
- private static string GenerateMethodName(CliCommandDefinition command)
- {
- // Convert command parts to method name
- // e.g., ["container", "create"] -> "ContainerCreate"
- // Handle hyphens within parts (e.g., "build-server" -> "BuildServer")
- return string.Join("", command.CommandParts
- .SelectMany(p => p.Split('-', StringSplitOptions.RemoveEmptyEntries))
- .Select(p => char.ToUpperInvariant(p[0]) + p[1..].ToLowerInvariant()));
- }
}
diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/ServiceInterfaceGenerator.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/ServiceInterfaceGenerator.cs
index f3f6a9a0ec..7fbddd5b57 100644
--- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/ServiceInterfaceGenerator.cs
+++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/ServiceInterfaceGenerator.cs
@@ -82,7 +82,7 @@ private static string GenerateInterface(CliToolDefinition tool)
.ToList();
var nonCollidingRootCommands = rootCommands
- .Where(c => !subDomainNames.Contains(GenerateMethodName(c)))
+ .Where(c => !subDomainNames.Contains(GeneratorUtils.GenerateMethodNameFromCommandParts(c.CommandParts)))
.ToList();
if (nonCollidingRootCommands.Count > 0)
@@ -107,14 +107,12 @@ private static string GenerateInterface(CliToolDefinition tool)
private static void GenerateMethodSignature(StringBuilder sb, CliCommandDefinition command)
{
// Generate method name from command parts
- var methodName = GenerateMethodName(command);
+ var methodName = GeneratorUtils.GenerateMethodNameFromCommandParts(command.CommandParts);
// Single method - users set LogSettings on options if they need custom logging
if (!string.IsNullOrEmpty(command.Description))
{
- sb.AppendLine(" /// ");
- sb.AppendLine($" /// {EscapeXmlComment(command.Description)}");
- sb.AppendLine(" /// ");
+ GeneratorUtils.GenerateXmlDocumentation(sb, command.Description);
sb.AppendLine(" /// The command options.");
sb.AppendLine(" /// Cancellation token.");
sb.AppendLine(" /// The command result.");
@@ -122,16 +120,4 @@ private static void GenerateMethodSignature(StringBuilder sb, CliCommandDefiniti
sb.AppendLine($" Task {methodName}({command.ClassName} options, CancellationToken cancellationToken = default);");
}
-
- private static string GenerateMethodName(CliCommandDefinition command)
- {
- // Convert command parts to method name
- // e.g., ["container", "create"] -> "ContainerCreate"
- // Handle hyphens within parts (e.g., "build-server" -> "BuildServer")
- return string.Join("", command.CommandParts
- .SelectMany(p => p.Split('-', StringSplitOptions.RemoveEmptyEntries))
- .Select(p => char.ToUpperInvariant(p[0]) + p[1..].ToLowerInvariant()));
- }
-
- private static string EscapeXmlComment(string text) => GeneratorUtils.EscapeXmlComment(text);
}
diff --git a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/SubDomainClassGenerator.cs b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/SubDomainClassGenerator.cs
index 6ec4912ef3..8aad71da0e 100644
--- a/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/SubDomainClassGenerator.cs
+++ b/tools/ModularPipelines.OptionsGenerator/src/ModularPipelines.OptionsGenerator/Generators/SubDomainClassGenerator.cs
@@ -52,7 +52,7 @@ private static void GenerateFilesFromTree(
var collidingCommands = new Dictionary(StringComparer.OrdinalIgnoreCase);
foreach (var command in node.Commands)
{
- var methodName = GetMethodNameFromCommand(command);
+ var methodName = GeneratorUtils.GenerateMethodNameFromLastCommandPart(command);
var matchingChild = node.Children.Values
.FirstOrDefault(c => c.PascalSegment.Equals(methodName, StringComparison.OrdinalIgnoreCase));
@@ -194,16 +194,10 @@ private static string GenerateNodeClass(
private static void GenerateExecuteMethod(StringBuilder sb, CliCommandDefinition command)
{
// Single method - users set LogSettings on options if they need custom logging
- sb.AppendLine(" /// ");
- if (!string.IsNullOrEmpty(command.Description))
- {
- sb.AppendLine($" /// {EscapeXmlComment(command.Description)}");
- }
- else
- {
- sb.AppendLine(" /// Executes the parent command directly.");
- }
- sb.AppendLine(" /// ");
+ var description = !string.IsNullOrEmpty(command.Description)
+ ? command.Description
+ : "Executes the parent command directly.";
+ GeneratorUtils.GenerateXmlDocumentation(sb, description);
sb.AppendLine(" /// The command options.");
sb.AppendLine(" /// Cancellation token.");
sb.AppendLine(" /// The command result.");
@@ -217,14 +211,12 @@ private static void GenerateExecuteMethod(StringBuilder sb, CliCommandDefinition
private static void GenerateMethod(StringBuilder sb, CliCommandDefinition command, CommandTreeNode node)
{
- var methodName = GetMethodName(command, node);
+ var methodName = GeneratorUtils.GenerateMethodNameFromLastCommandPart(command);
// Single method - users set LogSettings on options if they need custom logging
if (!string.IsNullOrEmpty(command.Description))
{
- sb.AppendLine(" /// ");
- sb.AppendLine($" /// {EscapeXmlComment(command.Description)}");
- sb.AppendLine(" /// ");
+ GeneratorUtils.GenerateXmlDocumentation(sb, command.Description);
sb.AppendLine(" /// The command options.");
sb.AppendLine(" /// Cancellation token.");
sb.AppendLine(" /// The command result.");
@@ -237,24 +229,4 @@ private static void GenerateMethod(StringBuilder sb, CliCommandDefinition comman
sb.AppendLine(" return await _command.ExecuteCommandLineTool(options, cancellationToken);");
sb.AppendLine(" }");
}
-
- private static string GetMethodName(CliCommandDefinition command, CommandTreeNode node)
- {
- return GetMethodNameFromCommand(command);
- }
-
- private static string GetMethodNameFromCommand(CliCommandDefinition command)
- {
- if (command.CommandParts.Length == 0)
- {
- return "Execute";
- }
-
- var lastPart = command.CommandParts[^1];
- var parts = lastPart.Split('-', StringSplitOptions.RemoveEmptyEntries);
- return string.Join("", parts.Select(p =>
- char.ToUpperInvariant(p[0]) + (p.Length > 1 ? p[1..].ToLowerInvariant() : "")));
- }
-
- private static string EscapeXmlComment(string text) => GeneratorUtils.EscapeXmlComment(text);
}