Skip to content

fluent APIs should return least restrictive result #1964

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 3 commits into from
Nov 14, 2022
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
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,6 @@ System.CommandLine
public System.Boolean HasDefaultValue { get; }
public System.String HelpName { get; set; }
public System.Type ValueType { get; }
public Argument AcceptLegalFileNamesOnly()
public Argument AcceptLegalFilePathsOnly()
public Argument AcceptOnlyFromAmong(System.String[] values)
public Argument AddCompletions(System.String[] completions)
public Argument AddCompletions(System.Func<System.CommandLine.Completions.CompletionContext,System.Collections.Generic.IEnumerable<System.String>> completionsDelegate)
public Argument AddCompletions(System.Func<System.CommandLine.Completions.CompletionContext,System.Collections.Generic.IEnumerable<System.CommandLine.Completions.CompletionItem>> completionsDelegate)
public System.Void AddValidator(System.Action<System.CommandLine.Parsing.ArgumentResult> validate)
public System.Collections.Generic.IEnumerable<System.CommandLine.Completions.CompletionItem> GetCompletions(System.CommandLine.Completions.CompletionContext context)
public System.Object GetDefaultValue()
Expand All @@ -28,6 +22,12 @@ System.CommandLine
.ctor(System.String name, Func<System.CommandLine.Parsing.ArgumentResult,T> parse, System.Boolean isDefault = False, System.String description = null)
.ctor(Func<System.CommandLine.Parsing.ArgumentResult,T> parse, System.Boolean isDefault = False)
public System.Type ValueType { get; }
public Argument<T> AcceptLegalFileNamesOnly()
public Argument<T> AcceptLegalFilePathsOnly()
public Argument<T> AcceptOnlyFromAmong(System.String[] values)
public Argument<T> AddCompletions(System.String[] completions)
public Argument<T> AddCompletions(System.Func<System.CommandLine.Completions.CompletionContext,System.Collections.Generic.IEnumerable<System.String>> completionsDelegate)
public Argument<T> AddCompletions(System.Func<System.CommandLine.Completions.CompletionContext,System.Collections.Generic.IEnumerable<System.CommandLine.Completions.CompletionItem>> completionsDelegate)
public struct ArgumentArity : System.ValueType, System.IEquatable<ArgumentArity>
public static ArgumentArity ExactlyOne { get; }
public static ArgumentArity OneOrMore { get; }
Expand Down Expand Up @@ -193,12 +193,6 @@ System.CommandLine
public ArgumentArity Arity { get; set; }
public System.Boolean IsRequired { get; set; }
public System.Type ValueType { get; }
public Option AcceptLegalFileNamesOnly()
public Option AcceptLegalFilePathsOnly()
public Option AcceptOnlyFromAmong(System.String[] values)
public Option AddCompletions(System.String[] completions)
public Option AddCompletions(System.Func<System.CommandLine.Completions.CompletionContext,System.Collections.Generic.IEnumerable<System.String>> completionsDelegate)
public Option AddCompletions(System.Func<System.CommandLine.Completions.CompletionContext,System.Collections.Generic.IEnumerable<System.CommandLine.Completions.CompletionItem>> completionsDelegate)
public System.Void AddValidator(System.Action<System.CommandLine.Parsing.OptionResult> validate)
public System.Collections.Generic.IEnumerable<System.CommandLine.Completions.CompletionItem> GetCompletions(System.CommandLine.Completions.CompletionContext context)
public System.Boolean HasAliasIgnoringPrefix(System.String alias)
Expand All @@ -213,6 +207,12 @@ System.CommandLine
.ctor(System.String[] aliases, Func<System.CommandLine.Parsing.ArgumentResult,T> parseArgument, System.Boolean isDefault = False, System.String description = null)
.ctor(System.String name, Func<T> defaultValueFactory, System.String description = null)
.ctor(System.String[] aliases, Func<T> defaultValueFactory, System.String description = null)
public Option<T> AcceptLegalFileNamesOnly()
public Option<T> AcceptLegalFilePathsOnly()
public Option<T> AcceptOnlyFromAmong(System.String[] values)
public Option<T> AddCompletions(System.String[] completions)
public Option<T> AddCompletions(System.Func<System.CommandLine.Completions.CompletionContext,System.Collections.Generic.IEnumerable<System.String>> completionsDelegate)
public Option<T> AddCompletions(System.Func<System.CommandLine.Completions.CompletionContext,System.Collections.Generic.IEnumerable<System.CommandLine.Completions.CompletionItem>> completionsDelegate)
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
15 changes: 15 additions & 0 deletions src/System.CommandLine.Tests/ArgumentTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using System.Linq;
using System.Threading.Tasks;
using Xunit;
using System.CommandLine.Completions;

namespace System.CommandLine.Tests
{
Expand Down Expand Up @@ -783,6 +784,20 @@ public void Argument_of_enum_can_limit_enum_members_as_valid_values()
.BeEquivalentTo(new[] { $"Argument 'Fuschia' not recognized. Must be one of:\n\t'Red'\n\t'Green'" });
}

[Fact]
public void Argument_of_T_fluent_APIs_return_Argument_of_T()
{
Argument<string> argument = new Argument<string>("--path")
.AcceptOnlyFromAmong("text")
.AddCompletions("test")
.AddCompletions(ctx => Array.Empty<string>())
.AddCompletions(ctx => Array.Empty<CompletionItem>())
.AcceptLegalFileNamesOnly()
.AcceptLegalFilePathsOnly();

argument.Should().BeOfType<Argument<string>>();
}

protected override Symbol CreateSymbol(string name)
{
return new Argument<string>(name);
Expand Down
15 changes: 15 additions & 0 deletions src/System.CommandLine.Tests/OptionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using FluentAssertions;
using System.CommandLine.Completions;
using System.CommandLine.Parsing;
using System.Linq;
using Xunit;
Expand Down Expand Up @@ -384,6 +385,20 @@ public void Option_of_enum_can_limit_enum_members_as_valid_values()
.Should()
.BeEquivalentTo(new[] { $"Argument 'Fuschia' not recognized. Must be one of:\n\t'Red'\n\t'Green'" });
}

[Fact]
public void Option_of_T_fluent_APIs_return_Option_of_T()
{
Option<string> option = new Option<string>("--path")
.AcceptOnlyFromAmong("text")
.AddCompletions("test")
.AddCompletions(ctx => Array.Empty<string>())
.AddCompletions(ctx => Array.Empty<CompletionItem>())
.AcceptLegalFileNamesOnly()
.AcceptLegalFilePathsOnly();

option.Should().BeOfType<Option<string>>();
}

protected override Symbol CreateSymbol(string name) => new Option<string>(name);
}
Expand Down
108 changes: 0 additions & 108 deletions src/System.CommandLine/Argument.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
using System.CommandLine.Parsing;
using System.CommandLine.Completions;
using System.Linq;
using System.IO;

namespace System.CommandLine
{
Expand Down Expand Up @@ -174,11 +173,6 @@ public void SetDefaultValueFactory(Func<ArgumentResult, object?> defaultValueFac

internal virtual bool HasCustomParser => false;

internal static Argument None() => new Argument<bool>
{
Arity = ArgumentArity.Zero
};

internal void AddAllowedValues(IReadOnlyList<string> values)
{
if (AllowedValues is null)
Expand All @@ -204,108 +198,6 @@ public override IEnumerable<CompletionItem> GetCompletions(CompletionContext con
/// <inheritdoc />
string IValueDescriptor.ValueName => Name;

/// <summary>
/// Adds completions for the argument.
/// </summary>
/// <param name="completions">The completions to add.</param>
/// <returns>The configured argument.</returns>
public Argument AddCompletions(params string[] completions)
{
Completions.Add(completions);
return this;
}

/// <summary>
/// Adds completions for the argument.
/// </summary>
/// <param name="completionsDelegate">A function that will be called to provide completions.</param>
/// <returns>The option being extended.</returns>
public Argument AddCompletions(Func<CompletionContext, IEnumerable<string>> completionsDelegate)
{
Completions.Add(completionsDelegate);
return this;
}

/// <summary>
/// Adds completions for the argument.
/// </summary>
/// <param name="completionsDelegate">A function that will be called to provide completions.</param>
/// <returns>The configured argument.</returns>
public Argument AddCompletions(Func<CompletionContext, IEnumerable<CompletionItem>> completionsDelegate)
{
Completions.Add(completionsDelegate);
return this;
}

/// <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>
/// <returns>The configured argument.</returns>
public Argument AcceptOnlyFromAmong(params string[] values)
{
AllowedValues?.Clear();
AddAllowedValues(values);
Completions.Clear();
Completions.Add(values);

return this;
}

/// <summary>
/// Configures the argument to accept only values representing legal file paths.
/// </summary>
/// <returns>The configured argument.</returns>
public Argument AcceptLegalFilePathsOnly()
{
var invalidPathChars = Path.GetInvalidPathChars();

AddValidator(result =>
{
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.ErrorMessage = result.LocalizationResources.InvalidCharactersInPath(token.Value[invalidCharactersIndex]);
}
}
});

return this;
}

/// <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>
/// <returns>The configured argument.</returns>
public Argument AcceptLegalFileNamesOnly()
{
var invalidFileNameChars = Path.GetInvalidFileNameChars();

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

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

return this;
}

/// <summary>
/// Parses a command line string value using the argument.
/// </summary>
Expand Down
105 changes: 105 additions & 0 deletions src/System.CommandLine/Argument{T}.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Collections.Generic;
using System.CommandLine.Binding;
using System.CommandLine.Completions;
using System.CommandLine.Parsing;
using System.IO;

namespace System.CommandLine
{
Expand Down Expand Up @@ -116,5 +119,107 @@ public Argument(Func<ArgumentResult, T> parse, bool isDefault = false) : this(nu

/// <inheritdoc />
public override Type ValueType => typeof(T);

/// <summary>
/// Adds completions for the argument.
/// </summary>
/// <param name="completions">The completions to add.</param>
/// <returns>The configured argument.</returns>
public Argument<T> AddCompletions(params string[] completions)
{
Completions.Add(completions);
return this;
}

/// <summary>
/// Adds completions for the argument.
/// </summary>
/// <param name="completionsDelegate">A function that will be called to provide completions.</param>
/// <returns>The option being extended.</returns>
public Argument<T> AddCompletions(Func<CompletionContext, IEnumerable<string>> completionsDelegate)
{
Completions.Add(completionsDelegate);
return this;
}

/// <summary>
/// Adds completions for the argument.
/// </summary>
/// <param name="completionsDelegate">A function that will be called to provide completions.</param>
/// <returns>The configured argument.</returns>
public Argument<T> AddCompletions(Func<CompletionContext, IEnumerable<CompletionItem>> completionsDelegate)
{
Completions.Add(completionsDelegate);
return this;
}

/// <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>
/// <returns>The configured argument.</returns>
public Argument<T> AcceptOnlyFromAmong(params string[] values)
{
AllowedValues?.Clear();
AddAllowedValues(values);
Completions.Clear();
Completions.Add(values);

return this;
}

/// <summary>
/// Configures the argument to accept only values representing legal file paths.
/// </summary>
/// <returns>The configured argument.</returns>
public Argument<T> AcceptLegalFilePathsOnly()
{
var invalidPathChars = Path.GetInvalidPathChars();

AddValidator(result =>
{
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.ErrorMessage = result.LocalizationResources.InvalidCharactersInPath(token.Value[invalidCharactersIndex]);
}
}
});

return this;
}

/// <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>
/// <returns>The configured argument.</returns>
public Argument<T> AcceptLegalFileNamesOnly()
{
var invalidFileNameChars = Path.GetInvalidFileNameChars();

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

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

return this;
}
}
}
4 changes: 1 addition & 3 deletions src/System.CommandLine/Help/HelpOption.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ internal class HelpOption : Option<bool>
private string? _description;

public HelpOption(string[] aliases, Func<LocalizationResources> getLocalizationResources)
: base(aliases)
: base(aliases, null, new Argument<bool> { Arity = ArgumentArity.Zero })
Copy link
Member

Choose a reason for hiding this comment

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

Could new Argument<bool> { Arity = ArgumentArity.Zero } be in a static and be re-used here and on VerisonOption?

Copy link
Member Author

Choose a reason for hiding this comment

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

IIRC I've tried it in the past (when I was working on SCL perf) and it was impossible as some of the methods were allowing for mutation, but I am not 100% sure if this is the case now.

Once we are done with API-redesign and re-layering we are going to profile the startup scenario again and if it pops up, try to cache it. But initializing static fields also has a cost, so this will have to be carefully verified.

{
_localizationResources = getLocalizationResources;
DisallowBinding = true;
Expand All @@ -32,8 +32,6 @@ public override string? Description
set => _description = value;
}

internal override Argument Argument => Argument.None();

internal override bool IsGreedy => false;

public override bool Equals(object? obj)
Expand Down
4 changes: 1 addition & 3 deletions src/System.CommandLine/Help/VersionOption.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ internal class VersionOption : Option<bool>
private readonly CommandLineBuilder _builder;
private string? _description;

public VersionOption(CommandLineBuilder builder) : base("--version")
public VersionOption(CommandLineBuilder builder) : base("--version", null, new Argument<bool> { Arity = ArgumentArity.Zero })
{
_builder = builder;

Expand Down Expand Up @@ -58,8 +58,6 @@ public override string? Description
set => _description = value;
}

internal override Argument Argument => Argument.None();

internal override bool IsGreedy => false;

public override bool Equals(object? obj)
Expand Down
Loading