Skip to content

Commit

Permalink
add basic Symbols
Browse files Browse the repository at this point in the history
diff with what was officially approved in dotnet/runtime#68578 (comment):
- CliSymbol, CliArgument, CliOption and CliCommand moved to new System.CommandLine.Parsing library
- abstract types moved from System.CommandLine to System.CommandLine.Symbols namespace as they should be rarely used
- HelpOption and VersionOption moved to System.CommandLine.Help library
- CliRootCommand moved to main System.CommandLine package
- CliSymbol.Description made virtual, to allow for customization like loading lazily from resources
- CliSymbol extended with Terminating property that let's the parser know that given symbol terminates parsing (example: help)
- CliSymbol.HelpName removed, as symbols are now unaware of help
- CliCOmmand.Add and CliCommand.Children made virtual to allow for CliRootCommand extend with Directives
  • Loading branch information
adamsitnik committed Oct 18, 2023
1 parent 0e9086d commit fe9e38a
Show file tree
Hide file tree
Showing 17 changed files with 415 additions and 0 deletions.
20 changes: 20 additions & 0 deletions System.CommandLine.Help/HelpOption.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
namespace System.CommandLine.Help;

public sealed class HelpOption : CliOption<bool>
{
/// <summary>
/// When added to a <see cref="CliCommand"/>, it configures the application to show help when one of the following options are specified on the command line:
/// <code>
/// -h
/// /h
/// --help
/// -?
/// /?
/// </code>
/// </summary>
public HelpOption() : base("--help", new[] { "-h", "/h", "-?", "/?" })
{
Recursive = true;
Terminating = true;
}
}
13 changes: 13 additions & 0 deletions System.CommandLine.Help/System.CommandLine.Help.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>10.0</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\System.CommandLine.Parsing\System.CommandLine.Parsing.csproj" />
</ItemGroup>

</Project>
11 changes: 11 additions & 0 deletions System.CommandLine.Help/VersionOption.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace System.CommandLine.Help;

public sealed class VersionOption : CliOption<bool>
{
/// <summary>
/// When added to a <see cref="CliCommand"/>, it enables the use of a <c>--version</c> option, which when specified in command line input will short circuit normal command handling and instead write out version information before exiting.
/// </summary>
public VersionOption() : base("--version", Array.Empty<string>())
{
}
}
17 changes: 17 additions & 0 deletions System.CommandLine.Parsing/Symbols/CliArgument.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// DESIGN: not included in System.CommandLine namespace since most of the users won't ever need to use CliArgument type
namespace System.CommandLine.Symbols;

/// <summary>
/// A symbol defining a value that can be passed on the command line to a <see cref="CliCommand">command</see> or <see cref="CliOption">option</see>.
/// </summary>
public abstract class CliArgument : CliSymbol
{
private protected CliArgument(string name) : base(name) { }

// DESIGN: HelpName is not included, as Help was moved out of main package

/// <summary>
/// Gets or sets the <see cref="Type" /> that the argument token(s) will be converted to.
/// </summary>
public abstract Type ValueType { get; }
}
16 changes: 16 additions & 0 deletions System.CommandLine.Parsing/Symbols/CliArgument_T.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using System.CommandLine.Symbols;

// DESIGN: this type will be frequently used and that it's why in the main namespace
namespace System.CommandLine;

public class CliArgument<T> : CliArgument
{
/// <summary>
/// Initializes a new instance of the Argument class.
/// </summary>
/// <param name="name">The name of the argument. It's not used for parsing, only when displaying Help or creating parse errors.</param>>
public CliArgument(string name) : base(name) { }

/// <inheritdoc />
public override Type ValueType => typeof(T);
}
90 changes: 90 additions & 0 deletions System.CommandLine.Parsing/Symbols/CliCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
using System.Collections;
using System.Collections.Generic;
using System.CommandLine.Symbols;
using System.ComponentModel;

// DESIGN: this type will be frequently used and that it's why in the main namespace
namespace System.CommandLine;

/// <summary>
/// Represents a specific action that the application performs.
/// </summary>
/// <remarks>
/// Use the Command object for actions that correspond to a specific string (the command name). See
/// <see cref="RootCommand"/> for simple applications that only have one action. For example, <c>dotnet run</c>
/// uses <c>run</c> as the command.
/// </remarks>
public class CliCommand : CliSymbol, IEnumerable<CliSymbol>
{
/// <summary>
/// Initializes a new instance of the Command class.
/// </summary>
/// <param name="name">The name of the command.</param>
/// <param name="description">The description of the command, shown in help.</param>
public CliCommand(string name, string? description = null) : base(name) => Description = description;

/// <summary>
/// Gets the child symbols.
/// </summary>
// DESIGN: it's virtual so CliRootCommand can add Directives
public virtual IEnumerable<CliSymbol> Children
{
get
{
foreach (var command in Subcommands)
yield return command;

foreach (var option in Options)
yield return option;

foreach (var argument in Arguments)
yield return argument;
}
}

/// <summary>
/// Represents all of the arguments for the command.
/// </summary>
public IList<CliArgument> Arguments { get; } = new List<CliArgument>();

/// <summary>
/// Represents all of the options for the command, including global options that have been applied to any of the command's ancestors.
/// </summary>
public IList<CliOption> Options { get; } = new List<CliOption>();

/// <summary>
/// Represents all of the subcommands for the command.
/// </summary>
public IList<CliCommand> Subcommands { get; } = new List<CliCommand>();

/// <summary>
/// Gets the unique set of strings that can be used on the command line to specify the command.
/// </summary>
/// <remarks>The collection does not contain the <see cref="CliSymbol.Name"/> of the Command.</remarks>
public ICollection<string> Aliases { get; } = new HashSet<string>();

/// <summary>
/// Adds a <see cref="CliSymbol"/> to the command.
/// </summary>
/// <param name="symbol">The symbol to add to the command.</param>
[EditorBrowsable(EditorBrowsableState.Never)] // hide from intellisense, it's public for C# duck typing
// DESIGN: it's virtual so CliRootCommand can add Directives
public virtual void Add(CliSymbol symbol)
{
switch (symbol)
{
case CliOption option: Options.Add(option); break;
case CliArgument argument: Arguments.Add(argument); break;
case CliCommand command: Subcommands.Add(command); break;
default: throw new NotSupportedException();
}
}

/// <summary>
/// Represents all of the symbols for the command.
/// </summary>
public IEnumerator<CliSymbol> GetEnumerator() => Children.GetEnumerator();

/// <inheritdoc />
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
19 changes: 19 additions & 0 deletions System.CommandLine.Parsing/Symbols/CliDirective.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
namespace System.CommandLine.Symbols;

/// <summary>
/// The purpose of directives is to provide cross-cutting functionality that can apply across command-line apps.
/// Because directives are syntactically distinct from the app's own syntax, they can provide functionality that applies across apps.
///
/// A directive must conform to the following syntax rules:
/// * It's a token on the command line that comes after the app's name but before any subcommands or options.
/// * It's enclosed in square brackets.
/// * It doesn't contain spaces.
/// </summary>
public class CliDirective : CliSymbol
{
/// <summary>
/// Initializes a new instance of the Directive class.
/// </summary>
/// <param name="name">The name of the directive. It can't contain whitespaces.</param>
public CliDirective(string name) : base(name) { }
}
37 changes: 37 additions & 0 deletions System.CommandLine.Parsing/Symbols/CliOption.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using System.Collections.Generic;

// DESIGN: not included in System.CommandLine namespace since most of the users won't ever need to use CliOption type
namespace System.CommandLine.Symbols;

/// <summary>
/// A symbol defining a named parameter and a value for that parameter.
/// </summary>
public abstract class CliOption : CliSymbol
{
private protected CliOption(string name) : base(name) { }

// DESIGN: HelpName is not included, as Help was moved out of main package

/// <summary>
/// When set to true, this option will be applied to the command and recursively to subcommands.
/// It will not apply to parent commands.
/// </summary>
public bool Recursive { get; set; }

/// <summary>
/// Indicates whether the option is required when its parent command is invoked.
/// </summary>
/// <remarks>When an option is required and its parent command is invoked without it, an error results.</remarks>
public bool Required { get; set; }

/// <summary>
/// Gets the unique set of strings that can be used on the command line to specify the Option.
/// </summary>
/// <remarks>The collection does not contain the <see cref="CliSymbol.Name"/> of the Option.</remarks>
public ICollection<string> Aliases { get; } = new HashSet<string>();

/// <summary>
/// Gets or sets the <see cref="Type" /> that the argument token(s) will be converted to.
/// </summary>
public abstract Type ValueType { get; }
}
20 changes: 20 additions & 0 deletions System.CommandLine.Parsing/Symbols/CliOption_T.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using System.CommandLine.Symbols;

// DESIGN: this type will be frequently used and that it's why in the main namespace
namespace System.CommandLine;

public class CliOption<T> : CliOption
{
/// <summary>
/// Initializes a new instance of the Option class.
/// </summary>
/// <param name="name">The name of the option. It's used for parsing, displaying Help and creating parse errors.</param>>
/// <param name="aliases">Optional aliases. Used for parsing, suggestions and displayed in Help.</param>
public CliOption(string name, params string[] aliases) : base(name)
{
foreach (string alias in aliases) Aliases.Add(alias);
}

/// <inheritdoc />
public override Type ValueType => typeof(T);
}
38 changes: 38 additions & 0 deletions System.CommandLine.Parsing/Symbols/CliSymbol.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using System.Collections.Generic;

// DESIGN: not included in System.CommandLine namespace since most of the users won't ever need to use CliSymbol type
namespace System.CommandLine.Symbols;

public abstract class CliSymbol
{
// DESIGN: we don't allow for custom symbols by design. The users can derive only from CliCommand, CliArgument<T>, CliOption<T> and Directive.
private protected CliSymbol(string name) => Name = name;

// DESIGN: it's virtual so the users can customize the behavior. Example: lazily load the description from resources
/// <summary>
/// Gets or sets the description of the symbol.
/// </summary>
public virtual string? Description { get; set; }

/// <summary>
/// Indicates that the symbol terminates a command line parsing.
/// Example: help.
/// </summary>
// DESIGN: simple things should be simple, advanced possible. Setting it to true requires creating a custom derived type.
public bool Terminating { get; protected set; } = false;

/// <summary>
/// Gets the name of the symbol.
/// </summary>
public string Name { get; }

/// <summary>
/// Gets or sets a value indicating whether the symbol is hidden.
/// </summary>
public bool Hidden { get; set; }

/// <summary>
/// Gets the parent symbols.
/// </summary>
public IEnumerable<CliSymbol> Parents => throw new NotImplementedException();
}
9 changes: 9 additions & 0 deletions System.CommandLine.Parsing/System.CommandLine.Parsing.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>netstandard2.0;net8.0</TargetFrameworks>
<LangVersion>10.0</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup>

</Project>
37 changes: 37 additions & 0 deletions System.CommandLine.sln
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.8.34110.38
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.CommandLine.Parsing", "System.CommandLine.Parsing\System.CommandLine.Parsing.csproj", "{BEDEF4C5-9913-4FFA-94B1-1C7D1CE84AFD}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.CommandLine", "System.CommandLine\System.CommandLine.csproj", "{1D8CAF32-F227-44CE-AFF1-E6E745EFCA8C}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.CommandLine.Help", "System.CommandLine.Help\System.CommandLine.Help.csproj", "{277CF12C-6E74-45CF-86E2-F19A39E4A6B9}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{BEDEF4C5-9913-4FFA-94B1-1C7D1CE84AFD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BEDEF4C5-9913-4FFA-94B1-1C7D1CE84AFD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BEDEF4C5-9913-4FFA-94B1-1C7D1CE84AFD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BEDEF4C5-9913-4FFA-94B1-1C7D1CE84AFD}.Release|Any CPU.Build.0 = Release|Any CPU
{1D8CAF32-F227-44CE-AFF1-E6E745EFCA8C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1D8CAF32-F227-44CE-AFF1-E6E745EFCA8C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1D8CAF32-F227-44CE-AFF1-E6E745EFCA8C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1D8CAF32-F227-44CE-AFF1-E6E745EFCA8C}.Release|Any CPU.Build.0 = Release|Any CPU
{277CF12C-6E74-45CF-86E2-F19A39E4A6B9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{277CF12C-6E74-45CF-86E2-F19A39E4A6B9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{277CF12C-6E74-45CF-86E2-F19A39E4A6B9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{277CF12C-6E74-45CF-86E2-F19A39E4A6B9}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {1F4FC9E8-F6C7-4BA3-B947-720A288E9242}
EndGlobalSection
EndGlobal
35 changes: 35 additions & 0 deletions System.CommandLine/Symbols/CliRootCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using System.Collections.Generic;
using System.CommandLine.Help;
using System.CommandLine.Symbols;

// DESIGN: this type will be frequently used, that it's why in the main namespace
namespace System.CommandLine;

public class CliRootCommand : CliCommand
{
public CliRootCommand(string name, string? description = null) : base(name, description)
{
Options.Add(new HelpOption());
Options.Add(new VersionOption());
}

/// <summary>
/// Represents all of the directives for the command.
/// </summary>
public IList<CliDirective> Directives { get; } = new List<CliDirective>();

public override void Add(CliSymbol symbol)
{
if (symbol is CliDirective directive) Directives.Add(directive);
else base.Add(symbol);
}

public override IEnumerable<CliSymbol> Children
{
get
{
foreach (CliSymbol child in base.Children) yield return child;
foreach (CliDirective directive in Directives) yield return directive;
}
}
}
16 changes: 16 additions & 0 deletions System.CommandLine/Symbols/DiagramDirective.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// DESIGN: this type will be referenced by CliRootCommand and enabled by default, that is why it's not in the main namespace
namespace System.CommandLine.Symbols;

/// <summary>
/// Enables the use of the <c>[diagram]</c> directive, which when specified on the command line will short
/// circuit normal command handling and display a diagram explaining the parse result for the command line input.
/// </summary>
/// Example: dotnet [diagram] build -c Release -f net7.0
/// Output: [ dotnet [ build [ -c <Release> ] [ -f <net7.0> ] ] ]
public sealed class DiagramDirective : CliDirective
{
/// <param name="errorExitCode">If the parse result contains errors, this exit code will be used when the process exits.</param>
public DiagramDirective() : base("diagram") { }

public int ParseErrorReturnValue { get; set; } = 1;
}
Loading

0 comments on commit fe9e38a

Please sign in to comment.