Skip to content

some ergonomic improvements #2559

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
May 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ _NCrunch_*/
_ReSharper.*/
buildlog.txt
nCrunchTemp*
.NCrunch_System.CommandLine/
TestResults/

.fake
Expand Down
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<PropertyGroup>
<NoWarn>$(NoWarn);NU5125;CS0618</NoWarn>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<LangVersion>10.0</LangVersion>
<LangVersion>13.0</LangVersion>
<PackageProjectUrl>https://github.com/dotnet/command-line-api</PackageProjectUrl>
</PropertyGroup>

Expand Down
6 changes: 4 additions & 2 deletions System.CommandLine.v3.ncrunchsolution
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@
<Settings>
<AllowParallelTestExecution>True</AllowParallelTestExecution>
<CustomBuildProperties>
<Value>TargetFrameworks = net7.0</Value>
<Value>TargetFramework = net7.0</Value>
<Value>TargetFrameworks = net8.0</Value>
<Value>TargetFramework = net8.0</Value>
</CustomBuildProperties>
<EnableRDI>False</EnableRDI>
<RdiConfigured>True</RdiConfigured>
<SolutionConfigured>True</SolutionConfigured>
</Settings>
</SolutionConfiguration>
13 changes: 13 additions & 0 deletions core.slnf
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"solution": {
"path": "System.CommandLine.sln",
"projects": [
"src\\System.CommandLine.ApiCompatibility.Tests\\System.CommandLine.ApiCompatibility.Tests.csproj",
"src\\System.CommandLine.DragonFruit\\System.CommandLine.DragonFruit.csproj",
"src\\System.CommandLine.NamingConventionBinder.Tests\\System.CommandLine.NamingConventionBinder.Tests.csproj",
"src\\System.CommandLine.NamingConventionBinder\\System.CommandLine.NamingConventionBinder.csproj",
"src\\System.CommandLine.Tests\\System.CommandLine.Tests.csproj",
"src\\System.CommandLine\\System.CommandLine.csproj"
]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,6 @@
public Func<System.CommandLine.Parsing.ArgumentResult,T> DefaultValueFactory { get; set; }
public System.Boolean HasDefaultValue { get; }
public System.Type ValueType { get; }
public System.Void AcceptLegalFileNamesOnly()
public System.Void AcceptLegalFilePathsOnly()
public System.Void AcceptOnlyFromAmong(System.String[] values)
public struct ArgumentArity : System.ValueType, System.IEquatable<ArgumentArity>
public static ArgumentArity ExactlyOne { get; }
public static ArgumentArity OneOrMore { get; }
Expand All @@ -35,6 +32,9 @@
public static Argument<System.IO.DirectoryInfo> AcceptExistingOnly(this Argument<System.IO.DirectoryInfo> argument)
public static Argument<System.IO.FileSystemInfo> AcceptExistingOnly(this Argument<System.IO.FileSystemInfo> argument)
public static Argument<T> AcceptExistingOnly<T>(this Argument<T> argument)
public static Argument<T> AcceptLegalFileNamesOnly<T>(this Argument<T> argument)
public static Argument<T> AcceptLegalFilePathsOnly<T>(this Argument<T> argument)
public static Argument<T> AcceptOnlyFromAmong<T>(this Argument<T> argument, System.String[] values)
public class Command : Symbol, System.Collections.IEnumerable
.ctor(System.String name, System.String description = null)
public System.CommandLine.Invocation.CommandLineAction Action { get; set; }
Expand Down Expand Up @@ -105,9 +105,9 @@
public Func<System.CommandLine.Parsing.ArgumentResult,T> CustomParser { get; set; }
public Func<System.CommandLine.Parsing.ArgumentResult,T> DefaultValueFactory { get; set; }
public System.Type ValueType { get; }
public System.Void AcceptLegalFileNamesOnly()
public System.Void AcceptLegalFilePathsOnly()
public System.Void AcceptOnlyFromAmong(System.String[] values)
public Option<T> AcceptLegalFileNamesOnly()
public Option<T> AcceptLegalFilePathsOnly()
public Option<T> AcceptOnlyFromAmong(System.String[] values)
public static class OptionValidation
public static Option<System.IO.FileInfo> AcceptExistingOnly(this Option<System.IO.FileInfo> option)
public static Option<System.IO.DirectoryInfo> AcceptExistingOnly(this Option<System.IO.DirectoryInfo> option)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ private sealed class DummyStateHoldingHandler : BindingHandler
public override Task<int> InvokeAsync(ParseResult parseResult, CancellationToken cancellationToken = default) => Task.FromResult(0);
}

/// <summary>
/// Gets the binding context for the specified parse result.
/// </summary>
/// <param name="parseResult">The parse result from the command line parsing.</param>
/// <returns>The binding context associated with the parse result.</returns>
public static BindingContext GetBindingContext(this ParseResult parseResult)
{
// parsing resulted with no handler or it was not created yet, we fake it to just store the BindingContext between the calls
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@

namespace System.CommandLine.NamingConventionBinder
{
/// <summary>
/// Represents a handler that provides binding functionality for command-line actions.
/// </summary>
/// <remarks>This abstract class serves as a base for implementing custom binding logic in command-line
/// applications. It provides a mechanism to retrieve or initialize a <see cref="BindingContext"/> for the current
/// invocation.</remarks>
public abstract class BindingHandler : AsynchronousCommandLineAction
{
private BindingContext? _bindingContext;
Expand Down
89 changes: 89 additions & 0 deletions src/System.CommandLine/ArgumentValidation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,95 @@ public static Argument<T> AcceptExistingOnly<T>(this Argument<T> argument)
return argument;
}

/// <summary>
/// Configures the argument to accept only values representing legal file names.
/// </summary>
/// <remarks>A parse error will result, for example, if file path separators are found in the parsed value.</remarks>
public static Argument<T> AcceptLegalFileNamesOnly<T>(this Argument<T> argument)
{
argument.Validators.Add(static result =>
{
var invalidFileNameChars = Path.GetInvalidFileNameChars();

for (var i = 0; i < result.Tokens.Count; i++)
{
var token = result.Tokens[i];
var invalidCharactersIndex = token.Value.IndexOfAny(invalidFileNameChars);

if (invalidCharactersIndex >= 0)
{
result.AddError(LocalizationResources.InvalidCharactersInFileName(token.Value[invalidCharactersIndex]));
}
}
});

return argument;
}


Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: extra line

Suggested change

/// <summary>
/// Configures the argument to accept only values representing legal file paths.
/// </summary>
public static Argument<T> AcceptLegalFilePathsOnly<T>(this Argument<T> argument)
{
argument.Validators.Add(static result =>
{
var invalidPathChars = Path.GetInvalidPathChars();

for (var i = 0; i < result.Tokens.Count; i++)
{
var token = result.Tokens[i];

// File class no longer check invalid character
// https://blogs.msdn.microsoft.com/jeremykuhne/2018/03/09/custom-directory-enumeration-in-net-core-2-1/
var invalidCharactersIndex = token.Value.IndexOfAny(invalidPathChars);

if (invalidCharactersIndex >= 0)
{
result.AddError(LocalizationResources.InvalidCharactersInPath(token.Value[invalidCharactersIndex]));
}
}
});

return argument;
}

/// <summary>
/// Configures the argument to accept only the specified values, and to suggest them as command line completions.
/// </summary>
/// <param name="argument">The argument to configure.</param>
/// <param name="values">The values that are allowed for the argument.</param>
public static Argument<T> AcceptOnlyFromAmong<T>(
this Argument<T> argument,
params string[] values)
{
if (values is not null && values.Length > 0)
{
argument.Validators.Clear();
argument.Validators.Add(UnrecognizedArgumentError);
argument.CompletionSources.Clear();
argument.CompletionSources.Add(values);
}

return argument;

void UnrecognizedArgumentError(ArgumentResult argumentResult)
{
for (var i = 0; i < argumentResult.Tokens.Count; i++)
{
var token = argumentResult.Tokens[i];

if (token.Symbol is null || token.Symbol == argument)
{
if (Array.IndexOf(values, token.Value) < 0)
{
argumentResult.AddError(LocalizationResources.UnrecognizedArgument(token.Value, values));
}
}
}
}
}

private static void FileOrDirectoryExists<T>(ArgumentResult result)
where T : FileSystemInfo
{
Expand Down
81 changes: 0 additions & 81 deletions src/System.CommandLine/Argument{T}.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@
using System.Collections.Generic;
using System.CommandLine.Parsing;
using System.Diagnostics.CodeAnalysis;
using System.IO;

namespace System.CommandLine
{
/// <inheritdoc cref="Argument" />
Expand Down Expand Up @@ -84,85 +82,6 @@ public Argument(string name) : base(name)
return DefaultValueFactory.Invoke(argumentResult);
}

/// <summary>
/// Configures the argument to accept only the specified values, and to suggest them as command line completions.
/// </summary>
/// <param name="values">The values that are allowed for the argument.</param>
public void AcceptOnlyFromAmong(params string[] values)
{
if (values is not null && values.Length > 0)
{
Validators.Clear();
Validators.Add(UnrecognizedArgumentError);
CompletionSources.Clear();
CompletionSources.Add(values);
}

void UnrecognizedArgumentError(ArgumentResult argumentResult)
{
for (var i = 0; i < argumentResult.Tokens.Count; i++)
{
var token = argumentResult.Tokens[i];

if (token.Symbol is null || token.Symbol == this)
{
if (Array.IndexOf(values, token.Value) < 0)
{
argumentResult.AddError(LocalizationResources.UnrecognizedArgument(token.Value, values));
}
}
}
}
}

/// <summary>
/// Configures the argument to accept only values representing legal file paths.
/// </summary>
public void AcceptLegalFilePathsOnly()
{
Validators.Add(static result =>
{
var invalidPathChars = Path.GetInvalidPathChars();

for (var i = 0; i < result.Tokens.Count; i++)
{
var token = result.Tokens[i];

// File class no longer check invalid character
// https://blogs.msdn.microsoft.com/jeremykuhne/2018/03/09/custom-directory-enumeration-in-net-core-2-1/
var invalidCharactersIndex = token.Value.IndexOfAny(invalidPathChars);

if (invalidCharactersIndex >= 0)
{
result.AddError(LocalizationResources.InvalidCharactersInPath(token.Value[invalidCharactersIndex]));
}
}
});
}

/// <summary>
/// Configures the argument to accept only values representing legal file names.
/// </summary>
/// <remarks>A parse error will result, for example, if file path separators are found in the parsed value.</remarks>
public void AcceptLegalFileNamesOnly()
{
Validators.Add(static result =>
{
var invalidFileNameChars = Path.GetInvalidFileNameChars();

for (var i = 0; i < result.Tokens.Count; i++)
{
var token = result.Tokens[i];
var invalidCharactersIndex = token.Value.IndexOfAny(invalidFileNameChars);

if (invalidCharactersIndex >= 0)
{
result.AddError(LocalizationResources.InvalidCharactersInFileName(token.Value[invalidCharactersIndex]));
}
}
});
}

[UnconditionalSuppressMessage("ReflectionAnalysis", "IL3050", Justification = "https://github.com/dotnet/command-line-api/issues/1638")]
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2091", Justification = "https://github.com/dotnet/command-line-api/issues/1638")]
internal static T? CreateDefaultValue()
Expand Down
1 change: 0 additions & 1 deletion src/System.CommandLine/OptionValidation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ public static Option<T> AcceptExistingOnly<T>(this Option<T> option)
where T : IEnumerable<FileSystemInfo>
{
option._argument.AcceptExistingOnly();

return option;
}
}
Expand Down
18 changes: 15 additions & 3 deletions src/System.CommandLine/Option{T}.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,17 +51,29 @@ public Func<ArgumentResult, T>? DefaultValueFactory
/// Configures the option to accept only the specified values, and to suggest them as command line completions.
/// </summary>
/// <param name="values">The values that are allowed for the option.</param>
public void AcceptOnlyFromAmong(params string[] values) => _argument.AcceptOnlyFromAmong(values);
public Option<T> AcceptOnlyFromAmong(params string[] values)
{
_argument.AcceptOnlyFromAmong(values);
return this;
}

/// <summary>
/// Configures the option to accept only values representing legal file paths.
/// </summary>
public void AcceptLegalFilePathsOnly() => _argument.AcceptLegalFilePathsOnly();
public Option<T> AcceptLegalFilePathsOnly()
{
_argument.AcceptLegalFilePathsOnly();
return this;
}

/// <summary>
/// Configures the option to accept only values representing legal file names.
/// </summary>
/// <remarks>A parse error will result, for example, if file path separators are found in the parsed value.</remarks>
public void AcceptLegalFileNamesOnly() => _argument.AcceptLegalFileNamesOnly();
public Option<T> AcceptLegalFileNamesOnly()
{
_argument.AcceptLegalFileNamesOnly();
return this;
}
}
}