Giter VIP home page Giter VIP logo

akamsteeg / atlex.commandlinearguments Goto Github PK

View Code? Open in Web Editor NEW
2.0 2.0 0.0 238 KB

AtleX.CommandLineArguments is a helper library to facilitate parsing command line arguments into a strongly-typed object

Home Page: https://www.nuget.org/packages/AtleX.CommandLineArguments/

License: MIT License

C# 100.00%
commandline command-line argument-parser argument arguments cli library netstandard netframework

atlex.commandlinearguments's Introduction

Hi there 👋

atlex.commandlinearguments's People

Contributors

akamsteeg avatar

Stargazers

 avatar  avatar

Watchers

 avatar  avatar

atlex.commandlinearguments's Issues

Simplify naming of classes

Currently, some of the class names are really long. Examples:

  • WindowsStyleCommandLineArgumentsConfiguration
  • LinuxStyleCommandLineArgumentsParser

We don't need to include CommandLineArguments in all names, so we can shorten those to:

  • WindowsStyleConfiguration
  • LinuxStyleParser

Set explicit sizes for collections of built-in validators and type parsers

Currently, we don't set explicit sizes for the List<T>s with the built-in validators and type parsers:

Suggestion: Since we add only a single validator the default initial capacity of that list of 4 is good enough. That leaves room for validators the user adds. For the type parsers, I suggest we add the nearest power of two that's equal to or greater than the number of built-in type parsers we add. Currently, that's 11 so set a capacity of 16.

Enable SourceLink

The whole symbols package stuff is replaced by SourceLink, so we should add/enable that too.

Add ITypeParser interface

In v2.0.0 we introduced the ICommandLineArgumentsParser (#8) and IHelpWriter (#11) interfaces but for a TypeParser there's no interface. For consistency, we should add a 'ITypeParser` interface too, so users don't need get confused about sometimes having to extend an abstract class and sometimes implement an interface.

Proposal:

public interface ITypeParser
{
    /// <summary>
    /// Gets the <see cref="Type"/> this <see cref="TypeParser{T}"/> handles
    /// </summary>
    Type Type { get; }

    /// <summary>
    /// Tries to parse the specified value to the specified parse result
    /// </summary>
    /// <param name="value">
    /// The value to parse
    /// </param>
    /// <param name="parseResult">
    /// The parsed value
    /// </param>
    /// <returns>
    /// True if value was converted successfully; otherwise, false
    /// </returns>
    bool TryParse(string value, out object parseResult);
}

We must also update the configuration and all usages to support the interface instead of the current abstract base class TypeParser.

Replace .NET Standard 1.5 with .NET Standard 2.0

.NET Standard 2.0 has a much richer API and in some parts, e.g. reflection, also a lot simpler. We should investigate the possibility of moving to NET Standard 2.0 (or later) for this release.

Release 2.1.0

Release steps for v2.1.0:

  • Make sure al milestone 2.1.0 issues (except #40) are merged and closed
  • Finish #40
  • Change version numbers to 2.1.0 and update release notes (commit and push)
  • Release in VSTS
  • Tag release and push
  • Release on Github

Removed obsolete comment from ArgumentPropertiesHelper

/// <remarks>
/// Do not cache an instance of this object in a field or property. It's quite
/// big and allocates some stuff itself. By instantiating it as close to the
/// code that needs it and null it afterwards, the garbage collector can do
/// its work
/// </remarks>

This comment in ArgumentPropertiesHelper is not valid anymore since e254161

Simplify HelpWriter to make implementing it less confusing

Implementing a new HelpWriter is quite a confusing affair. You have to override a public method and a protected method that's seemingly not called from anywhere and you get some stuff for free but no idea what.

We need to clear this mess up and making implementing this class more straight-forward and less magic.

(And do keep in mind that in #11 we're going to introduce an IHelpWriter interface)

General code cleanup

There are quite some unused usings and redundant empty lines and whitespace in the code. This is a general issue to do some housekeeping on the code to fix those.

Add ThrowHelper to remove throwing exceptions from the hot path

A method is not eligable for inlining when there's a throw in it.

Suggestion: Investigate the pros and cons of adding a ThrowHelper class that moves throwing exceptions out of the hot path. Implement if the results are favourable.

Example

// Before:
if (argument == null)
  throw new ArgumentNullException(nameof(argument)

// After:
if (argument == null)
  ThrowHelper.ThrowArgumentNullException(nameof(argument));

Introduce an interface for CommandLineArgumentsParser

Currently, anyone who wishes to add a new CommandLineArgumentsParser needs to inherit from that abstract base class. While it's a great way to quickly whip up a new parser it also locks you into the reflection-based property setting etc.

Proposal:

To allow for more flexibility we introduce a new interface ICommandLineArgumentsParser

public interface ICommandLineArgumentsParser
{
	/// <summary>
	/// Parse the specified arguments to the specified type
	/// </summary>
	/// <typeparam name="T">
	/// The <see cref="Arguments"/> to parse to
	/// </typeparam>
	/// <param name="arguments">
	/// The arguments to parse
	/// </param>
	/// <param name="validators">
	/// The <see cref="IEnumerable{T}"/> of <see cref="ArgumentValidator"/> to validate the arguments with
	/// </param>
	/// <param name="typeParsers">
	/// The <see cref="IEnumerable{T}"/> of <see cref="TypeParser"/> to parse the argument values with
	/// </param>
	/// <returns>
	/// The <see cref="ParseResult{T}"/>
	/// </returns>
	public ParseResult<T> Parse<T>(string[] arguments, IEnumerable<ArgumentValidator> validators, IEnumerable<TypeParser> typeParsers)
		where T : Arguments, new()
}

CommandLineArgumentsParser implements this interface and in the settings we expect an ICommandLineArgumentsParser instead of the current CommandLineArgumentsParser. This leaves all current parsers intact, but it gives users more freedom when writing their own parsers.

Run benchmarks on .NET Core too

Currently we only run benchmarks on the full .NET Framework, but since we support .NET Core via .NET Standard we should run our benchmarks on that too.

Restructure repo to enable hotfixes and stable release path

Currently, the repo has a Master and we work with feature branches from Master to add and improve and fix things. When those are done they get merged back to Master. However, that leads to an unstable Master between releases, so if we ever have to do an out-of-band release we're in trouble.

Proposal: Introduce a Development branch from Master. Feature branches will be created from Development and when we're ready for release we'll merge back to Master. Releases will be created and tagged from Master. Hotfixes will be created from Master and merged with Development.

ArgumentPropertiesHelper.FillProperty() iterates over all properties to find the one to set the value for, but the correct property is already known outside the method

ArgumentPropertiesHelper.FillProperty<T>(T, string, string) iterates over all properties in T to find the one with the specified name, and it sets the value for that property. However, at the call site we most likely already know the property we want to set.

Suggestion: Change the signature to FillProperty<T>(T, PropertyInfo, string) and set the value for that PropertyInfo directly.

Remove unused usings from built-in TypeParsers

Introduce an interface for HelpWriter

Implementing a new HelpWriter is quite a confusing affair. You have to override a public method and a protected method that's seemingly not called from anywhere and you get some stuff for free but no idea what.

So, in the style of improvements to CommandLineArgumentsParser in #8 I'd like to introduce an interface IHelpWriter to give users a more flexible way to really write their own HelpWriter implementations. They can implement IHelpWriter however they want without being locked to the way HelpWriter works.

Proposed interface:

public interface IHelpWriter
{
	/// <summary>
	/// Write the help for the specified <see cref="Arguments"/> object
	/// </summary>
	/// <typeparam name="T">
	/// The type of the <see cref="Arguments"/> to write the help for
	/// </typeparam>
	/// <param name="argumentsToWriteHelpFor">
	/// The <see cref="Arguments"/> object to write the help for
	/// </param>
	void Write<T>(T argumentsToWriteHelpFor)
			where T : Arguments, new()
}

HelpWriter will implement this interface. And of course in the configuration we should accept an IHelpWriter instead of a HelpWriter.

Argument validation should be moved out from the argument parsers

Currently, it's the task of the ICommandLineArgumentsParser implementations to validate the arguments using the supplied ArgumentValidator instances. Since this is the same for all parsers (and it's hard to get right) we should move it out to the call site.

Mark type parsers and validators that don't need to be public, internal

In #9 we reworked the configuration system and now we implicitly load all provided type parsers and validators by default. So, we don't need them to be public anymore. Now they polute the public API but that doesn't serve any purpose now.

Suggestion: Mark all default type parsers and validators as internal.

PrefixedKeyConsoleHelpWriter is abstract but shouldn't be

Currently, PrefixedKeyConsoleHelpWriter is an abstract class:

However, I don't think that it should be. It's a complete implementation of IHelpWriter that can work on its own. Now we're forcing people to extend it, just to call the constructor of the abstract base class PrefixedKeyConsoleHelpWriter. It's clean, because it forces strong-typed help writers, but it also makes introducing a new one for a 3rd-party thing much more involved.

Suggestion: Remove the abstract modified from PrefixedKeyConsoleHelpWriter.

Name of the IsRequired parameter to the ArgumentHelpDetails constructor is not according to the naming guidelines

/// <param name="IsRequired">
/// True when the argument is required, false otherwise
/// </param>
public ArgumentHelpDetails(string argumentName, string description, bool IsRequired)
{
if (string.IsNullOrWhiteSpace(argumentName))
throw new ArgumentNullException(nameof(argumentName));
this.Argument = argumentName;
this.Description = description ?? throw new ArgumentNullException(nameof(description));
this.IsRequired = IsRequired;

The IsRequired parameter to ArgumentHelpDetails(string, string, bool) is not according to spec. It should be isRequired. That also removes the confusion IntelliSense now has.

WindowsStyleCommandLineArgumentsParser is case-sensitive but all parsers should be case-insensitive

The protected override bool TryFindRawArgumentValue(string[] allCommandLineArguments, string argumentToFind, out string value) method in WindowsStyleCommandLineArgumentsParser is case-sensitive, but all parsers should be case-insensitive unless explicitly configured as case-sensitive.

The issue is in the if (string.Compare(key, argumentToFind) == 0) on line#46. We should make that comparison case-insensitive by passing an extra argument ignoreCase: true to the Compare().

Add IArgumentValidator interface

In v2.0.0 we introduced the ICommandLineArgumentsParser (#8) and IHelpWriter (#11) interfaces, but there's no IArgumentValidator interface. We still force users to inherit from ArgumentValidator. For consistency, we need to add an IArgumentValidator interface too.

Proposal:

public interface IArgumentValidator
{
/// <summary>
    /// Try Validating the argument by the specified <see cref="PropertyInfo"/>
    /// </summary>
    /// <param name="argumentPropertyInfo">
    /// The <see cref="PropertyInfo"/> of the command line argument to validate
    /// </param>
    /// <param name="isSpecified">
    /// Whether the argument is specified on the command line or not
    /// </param>
    /// <param name="originalValue">
    /// The original value, as specified on the command line
    /// </param>
    /// <param name="validationError">
    /// If the validation fails, this contains the <see cref="ValidationError"/>
    /// or null otherwise
    /// </param>
    /// <returns>
    /// True when the argument is valid, false otherwise
    /// </returns>
    bool TryValidate(PropertyInfo argumentPropertyInfo, bool isSpecified, string originalValue, out ValidationError validationError);

}

Simplify ArgumentPropertiesHelper.TryFillCustomType<T>()

Currently, ArgumentPropertiesHelper.TryFillCustomType<T>() does a lot of things:

foreach (var currentTypeParser in typeParsers)
{
if (currentTypeParser.Type == property.PropertyType)
{
result = currentTypeParser.TryParse(value, out var parseResult);
if (result)
{
property.SetValue(arguments, parseResult);
break;
}
}
}

It find the correct TypeParser, tries to parse the parameter value and sets the corresponding property in the Arguments. We need to simplify that, and I think we can achieve that by refactoring finding the correct type parser to a separate method, because that makes it much more a 'SRP' method with a lot less levels of indentation.

Change argument parsing to a pluggable thing so we can support custom types

Currently, the parsing of argument values is limited to the following types:

  • Bool
  • Byte
  • Char
  • DateTime
  • Decimal
  • Double
  • Float
  • Int
  • Long
  • Short
  • String
  • Any enum

We want to support custom types too.

Suggestion: Add a custom TypeParser/TypeParser<T> (abstract) base class of which the implementations can parse the string value of an argument supplied on the CLI to the specified type.

Arguments should be validated after all arguments are parsed, not during parsing

var isValid = validationHelper.TryValidate(currentProperty,
argumentIsSpecified,
argumentValue,
out var validationErrors);
if (!isValid)
{
allValidationErrors.AddRange(validationErrors);
}

We validate an argument as soon as it's parsed, but we should defer that until after all arguments are parsed. That gives validators the possibility to be conditional, e.g. "if argument A is true, argument B should be either 1 or 2 but not 0".

This means we should also give the complete Arguments object to ValidationHelper and pass it on to the ArgumentValidator too. That makes this a breaking change.

Remove TypeParser<T>?

Currently, there's a TypeParser which has an abstract bool TryParse(string, out object) method. There's also a generic TypeParser<T> that has a bool TryParse(string, out T) method. The idea behind that was to avoid boxing, but since we use reflection (with PropertyInfo.SetValue(), that uses object anyway) in ArgumentPropertiesHelper to set the value of the properties of the Arguments implementation, this isn't going to work.

Investigate: Can we remove TypeParser<T> and let all built-in type parsers just inherit from TypeParser?

Things to consider: A 3rd-party ICommandLineArgumentsParser might work without reflection and can possibly use the generic TypeParser<T> methods to avoid boxing.

Improve/rewrite the configuration system

I think the current settings system with CommandLineArgumentsConfiguration being some sort of base class for settings and a ConfigurationBuilder to provide a fluent interface for creating configurations is confusing.

CommandLineArgumentsConfiguration was supposed to be a base class from which users could inherit to create their own configurations. We had a DefaultCommandLineArgumentsConfiguration class in the past that did just that but that was removed in 1b2137b. However, now it's just a normal class you can instantiate and fill the properties and then pass it on. So, you can inherit from it, instantiate it or use the ConfigurationBuilder. How am I going to explain that?

So, we need something different that's easier to use but I'm not sure what. As soon as I can think of something I'm going to write it down in this issue. Any suggestions are welcome.

Increase version to 2.0.0 and update release notes

(Blocked by #26)

FINAL TASK BEFORE RELEASE

Checklist:

  1. Create a release/v1.0.0 release branch from master
  2. Ensure all feature & fix branches are merged back to release/v2.0.0
  3. Update version (assembly + package) to 2.0.0 in release/v2.0.0
  4. Update the release notes with all finished tasks
  5. Update readme.md with changed target frameworks
  6. Merge release/v2.0.0 back to master
  7. Create a new v2.0.0 release on Github with the release notes
  8. Tag release v2.0.0 from master
  9. Push release/v1.0.0 to origin
  10. Push master to origin
  11. Push tag v2.0.0 to origin
  12. Delete branch release/v2.0.0 locally and remote
  13. Release through VSTS

CommandLineArgument.Configuration.set_Configuration() throws the wrong error

Currently, CommandLineArgument.Configuration.set_Configuration() throws an incorrect InvalidOperationException when the value is null, with a weird message too:

_ = value ?? throw new InvalidOperationException("Cannot display help without a configuration");

Suggestion: Change this to a ArgumentNullException and put that into the ValidateConfiguration(CommandLineArgumentsConfiguration) method

ArgumentPropertiesHelper and ValidationHelper can easily be static, but aren't

The ArgumentPropertiesHelper and ValidationHelper can easily be static, but they aren't. Making them static reduces GC load and calling static methods is cheaper, so we should make them static.

Because these classes are internal, it doesn't change the external API so no major or minor release is needed for this.

Requesting help is not always correctly handled by the built-in parsers

Currently, there's a protected virtual bool ContainsHelpArgument(string[] allCommandLineArguments) in CommandLineArgumentsParser that looks for an argument help (case-insensitive). And different implementations can override it. But now when a parser does not override this with its own implementation you don't have a help argument in the same style as the other arguments.

Suggested solution: Change ContainsHelpArgument(string[]) in CommandLineArgumentsParser to protected abstract so parsers must implement it.

Pros:

  • Consistency

Cons:

  • Breaking API change in beta
  • More implementation work when writing new parsers.

Investigate which TFM's we want to support in 2.0.0

Currently, the library supports three Target Framework Monikers:

  1. NET45
  2. NET46
  3. NETSTANDARD1.5

However, this seems a bit redundant. If we support .NET 4.5, the lowest full framework TFM we can target at the moment, we also implicitly support 4.6, 4.6.1, 4.7, etc. Why do we explicitly specify .NET 4.6 then?

With .NET Standard 1.5 we can run on the full framework in any application targeting .NET 4.6.2 or higher and in .NET Core 1.0 or higher. For consumers being stuck (for the moment?) at older versions of .NET we can give them 4.5.

So, in summary: Wouldn't it be enough to just target NET45 and NETSTANDARD1.5?

Release v2.0.0-beta1

  1. Ensure all feature & fix branches are merged back to release/v2.0.0
  2. Update assembly version to 2.0.0 in release/v2.0.0
  3. Update package version to 2.0.0-beta1 in release/v2.0.0
  4. Update the release notes with all finished tasks
  5. Update readme.md with changed target frameworks (#10 )
  6. Create a new v2.0.0-beta1 release on Github with the release notes
  7. Tag release v2.0.0-beta1 from release/v2.0.0
  8. Push release/v2.0.0 to origin
  9. Push tag v2.0.0-beta1 to origin
  10. Release through VSTS

Move configuration to parameter on CommandLineArguments.TryParse()

Currently, CommandLineArguments has a static Configuration property to set the config. However, that means that the config can never be garbage collected and it can be huge with all the validators and type parsers.

Suggestion: Add overloads to the CommandLineArguments.TryParse() methods that take the config as a parameter. Let the current TryParse() methods use the AutoDetectConfiguration (which is the default config now).

A new instance of AutoDetectConfiguration is always created, even when the user supplies their own configuration

We always create a new instance of AutoDetectConfiguration in CommandLineArguments:

private static CommandLineArgumentsConfiguration _configuration = new AutoDetectConfiguration();

Later, when the user supplies their own Configuration we don't need that anymore so it's wasted cycles and memory.

Suggestion: Create a new instance of AutoDetectConfiguration in the getter of CommandLineArguments.Configuration when the backing field _configuration is null and assign it to the field before returning it.

A new List<ValidationError> is always created, even when the validation succeeds

In ValidationHelper.TryValidate<T>(PropertyInfo, bool, string, out IEnumerable<ValidationError>) we always allocate a List<ValidationError> even when we don't have validation errors for that property:

var currentValidationErrors = new List<ValidationError>();
foreach (var currentValidator in this.argumentValidators)
{
var isValid = currentValidator.TryValidate(parsedPropertyToValidate, isSpecified, originalValue, out ValidationError validationError);
if (!isValid)
{
currentValidationErrors.Add(validationError);
result = false;
}
}

Suggestion: Only create the List<ValidationError> when we have a validation error. Otherwise, use Enumerable.Empty<ValidationError>() to return an empty IEnumerable<ValidationError>. We need to do that because users don't expect those things to be null and they call LINQ methods on them.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.