commandline icon indicating copy to clipboard operation
commandline copied to clipboard

Add support for validation and custom types

Open ericnewton76 opened this issue 7 years ago • 26 comments

Issue by clmcgrath Monday Jun 26, 2017 at 18:21 GMT Originally opened as https://github.com/gsscoder/commandline/issues/455


Add some sort of hook or attribute to plug validation into the parser pipeline

similar some similar frameworks use attributes or factory registration to allow an entrypoint for custom validation and type mapping (DI support would be awesome as well ;) )

a typical and widely used usage scenario for this would be filesystem path validation and mapping to IO classes such as FileInfo / DirectoryInfo an seemingly easy way to to add this type of validation functionality would be to allow validation attributes that inherit from a validation base class or interface and run custom or pre-written code

some examples of validation provided in similar frameworks are

[FileExists] 
[DirectoryExists] 
[FileNotExists]
[DirectoryNotExists] 
[RegExValidation]

Powerargs provides a special class called a Reviver for custom type mapping as well that the framework looks for to try and map values to a custom or complex type ie path string => FileInfo

It is very hard to find a framework that does all 3 of these well currently and make this library a go to framework for myself and many other developers i am sure

as CLP 2.x is still in beta and major rewrite phase i think now would be a good time to look at feature expansion as well to increase the power of this framework

ericnewton76 avatar Nov 04 '17 18:11 ericnewton76

Comment by stanislavromanov Wednesday Jul 26, 2017 at 13:56 GMT


IMHO this would be overkill and it destroys the single-responsibility-principle aspect of this library.

ericnewton76 avatar Nov 04 '17 18:11 ericnewton76

Comment by nemec Wednesday Jul 26, 2017 at 17:55 GMT


@stanislavromanov what if it was set up to allow you to plug in an existing validation library (e.g. FluentValidation) that's run after all parsing or after each argument is parsed? The validations mentioned in the OP would either be built in (but optional) or compiled to a separate nuget and you could always design your own.

ericnewton76 avatar Nov 04 '17 18:11 ericnewton76

Comment by clmcgrath Wednesday Jul 26, 2017 at 23:40 GMT


@stanislavromanov, i agree and disagree with you on this, without some sort of validation mechanism or custom mapping mechanism it really limits what can be used as an argument ie : FileInfo DirectoryInfo mapping, DateTime , TimeSpan, DateTimeOffset , are a few examples for mapping , being able to validate path exists is a good example of a custom validation , or an argument is in specified range .. I don't think allowing business rules in your parsing pipeline violates SRP and is a quite common requirement and a feature of many libraries,

ericnewton76 avatar Nov 04 '17 18:11 ericnewton76

Comment by ohadschn Thursday Nov 02, 2017 at 19:43 GMT


+1 from me.

If I have to do my own validation, not only do I need to write code for annoying things like parsing and regex matching, I also don't get the benefits of automatic help text display on parsing errors (including the text that explains exactly what went wrong in the parsing).

ericnewton76 avatar Nov 04 '17 18:11 ericnewton76

Fighting this battle right now as well validating if a file actually exists on disk. I don't see an easy way to add a custom error to the help text and display the help text after successful parsing is done.

Is this possible?

ejohn20 avatar Mar 12 '18 21:03 ejohn20

So realistically, this is a parsing library and all the validation is around successful parsing of strings to numbers etc.

[FileExists], [DirectoryExists], [NetworkServerReachable], [AmazonServiceHasItem] and similar, would definitely be out-of-scope for a library like this.

For these things, your best bet is probably after successful parsing, run through your use-case validations, (ie check file specified exists, or the amazon service has the item, etc) and invoke the Help to display usage, and display your validation failure message afterwards in red or different color... "File specified 'xxx.txt' doesn't exist". And, by convention, exit with a non-zero exit code.

ericnewton76 avatar Mar 13 '18 16:03 ericnewton76

That would work. Do you know what snippet I would need to run in order to pull the current help usage text? Right now, I have this:

private static Options parseArgs(string[] args)
        {
            ParserResult<Options> result = Parser.Default.ParseArguments<Options>(args);
            
            if (result.Tag == ParserResultType.NotParsed)
            {
                return null;
            }
            else
            {
                var parsedResult = result as Parsed<Options>;
                Options o = parsedResult.Value;

                if (!File.Exists(o.Solution))
                {
                    Console.WriteLine($"Solution file does not exist: {o.Solution}");
                    return null;
                }

                return parsedResult.Value;
            }
        }

I would love to be able to pull the "usage" text from the parsedResult somehow and include that in the error message.

ejohn20 avatar Mar 13 '18 17:03 ejohn20

You almost need to return ParserResult<Options> from that instead, so you won't have a null return that you'll have to guard against.

Next, I'd have a ValidateOptions method that would then roll through the various specified options and check them for validity, and return a ValidationResults type of structure,

Which would then be processed by a ParseAndValidate method that would be responsible for analyzing the ParserResult<Options> from parseArgs and deciding if it needs to display usage and/or exit with a non-zero error code. Realistically a non-existing file wouldn't generate a usage banner, but your ParseAndValidate method could do that, and calling the GetHelp routines to output the usage banner.

Sorry i'm a little vague on the GetHelp routines, mainly because I picked up the project trying to push the v2 changes into a release state after the original author disappeared into contract work (I'm assuming)

On Tue, Mar 13, 2018 at 1:24 PM, Eric Johnson [email protected] wrote:

That would work. Do you know what snippet I would need to run in order to pull the current help usage text? Right now, I have this:

private static Options parseArgs(string[] args) { ParserResult<Options> result = Parser.Default.ParseArguments<Options>(args);

        if (result.Tag == ParserResultType.NotParsed)
        {
            return null;
        }
        else
        {
            var parsedResult = result as Parsed<Options>;
            Options o = parsedResult.Value;

            if (!File.Exists(o.Solution))
            {
                Console.WriteLine($"Solution file does not exist: {o.Solution}");
                return null;
            }

            return parsedResult.Value;
        }
    }

I would love to be able to pull the "usage" text from the parsedResult somehow and include that in the error message.

— You are receiving this because you authored the thread. Reply to this email directly, view it on GitHub https://github.com/commandlineparser/commandline/issues/146#issuecomment-372748635, or mute the thread https://github.com/notifications/unsubscribe-auth/ACG_zOoZud1x5vqP9DeTMGzTqBdPNl0Yks5teADZgaJpZM4QSHQ7 .

--

E.Newton Senior Software Craftsman Cell: 407-497-1429 EMail: [email protected]

ericnewton76 avatar Mar 13 '18 17:03 ericnewton76

The GetHelp routines are a bit difficult to use since most of it is marked internal, but you are in luck - with ParserResult<Options> result it's quite easy to generate the help text:

var help = HelpText.AutoBuild(result);
Console.WriteLine(help.ToString());

nemec avatar Mar 13 '18 17:03 nemec

Unfortunately, AutoBuild<T>() probably won't help. We're probably only interested in running a validator over input that successfully parsed. The parser already catches syntax snafus for us. The role of the validator is to catch semantic errors. In which case, our abstract ParserResult<T> is instantiated as a Parsed<T> object. The first thing AutoBuild<T>() does is to throw an ArgumentException if its parserResult argument isn't a NotParsed<T> object.

You run into a similar issue if you try to use DefaultParsingErrorHandler<T> to roll your own version of AutoBuild<T>(). Meanwhile, NotParsed<T> has no public constructors (all internal), so we can't build our own failure to give to AutoBuild<T>().

Oh, and even if we were running a validator on a NotParsed<T> result, there's no way to add errors to an existing NotParsed<T> object, so AutoBuild<T> doesn't really help there, either.

All that said, I think it's debatable whether invoking CommandLineParser's help methods are even appropriate for this kind of problem. Why print a usage error if the specified file doesn't exist? That isn't a usage problem.

On the other hand, I can see some cases where it might make sense. For instance, if my program is something like: dosomething --repeat -3. This parses because -3 is a valid integer token. But it's a nonsensical response, and my HelpText for the repeat options is probably going to tell the user that only positive values are allowed.

This example blurs the line between syntax and semantics. If the parser were more sophisticated, we could specify that only positive integer tokens are allowed in the context of a repeat option, but we just don't have that capability, so it falls through to semantic checking.

tmillican avatar Apr 03 '18 18:04 tmillican

@tmillican I am of the same mind on this topic. There's definitely a difference between specifying an invalid file and having a filename parsed into the wrong variable. Syntax vs semantics.

ericnewton76 avatar Apr 04 '18 04:04 ericnewton76

@ericnewton76:

Speaking to the original issue, I feel like this might be a case where inversion of control is more headache than its worth. Rather than concocting some way for CommandLineParser to call a plug-in validator, it might be a lot easier and cleaner to just provide more/better opportunities for the recipient of parser results to call back into the parser's output logic.

I'm even less familiar with HelpText and friends, but as @nemec points out, most of the help logic is marked internal, so about the only control we have over output is the onError delegate argument of AutoBuild<T>(). That's fine except that HelpText doesn't expose enough to inject error messages (without getting really hacky) and, of course, it only gets invoked in the context of errors.

If we had more building blocks like BuildHeaderText(), BuildOptionsText(), etc. then you could suppress and inject output pretty much any way you'd like, whether or not the parsing succeeded.

Because there are cases of actual syntax checking that are simply too complicated or specialized to fit into a general command line parser. For instance, a company might have a standard format for project numbers. That's a syntax issue, but I think it's fair to say that it's beyond the scope of CommandLineParser's niche. So it would be good to be able to augment it with a more sophisticated validator somehow.

tmillican avatar Apr 05 '18 20:04 tmillican

If you can provide a custom validator instance, used for specific custom validation, it should handle minimum modifications in the CommandLine lib, and respect SRP. I think to something like this :

            parsed.MapResult(
                (Verb1Options opts, Validator validator) =>
                { 
                    if(opts.ValueToCheck != "thegoodvalue")
                        validator.ValidationError("The value should be like thegoodvalue");
                    return 0;
                },
                (Verb2Options opts, Validator validator) =>
                {
                    ....
                    return 0;
                },
                errs => 1);

If a single validator.ValidationError() call happens, then we should call the errs => 1 delegate, and WriteOut the custom message on the console.

rducom avatar Apr 08 '18 15:04 rducom

Is there still interest to add validation capabilities to the framework?

I am thinking about adding to Option an optional Func<Error> which contains user-provided validation logic and runs after parsing is done.

The approach listed by @rducom would also be good IMHO.

In any case, I think it's an useful addition to the library.

lupino3 avatar Nov 05 '18 13:11 lupino3

I did a first "draft" thinking about custom validations in 3a689998bdad1dc959f3c5dbff526064c57b1925. Maybe somebody will have a look before providing more validations. A test case shows the general usage.

steven-r avatar Nov 24 '18 03:11 steven-r

Is there any progress on this? If not, at the current state of things, what would be the best place to perform such validation as checking if the provided path is valid?

mmajcica avatar Jul 21 '19 10:07 mmajcica

I would like an abstract base class or interface like:

public class MyOptions : IValidatedOptions {
    [Option('s', "something", Required=true, HelpText="Some option")]
    public string Something {get;set;}

    // From Interface
    public bool Validate(){
      if(!Util.CheckSomething(Something))
        return false; // this automatically tells CommandLineParser to fail the parsing and print the help.
    }    
}

Alternatively instead of bool it could return some kind of new ValidationError("Reason for failing").

If the parser sees options implement that interface - runs Validate() - if not, works as it works now.

I of course could easily write that in, but let's hear from maintainers first if they are considering any type of validation strategies.

p-kaczynski avatar Aug 02 '19 09:08 p-kaczynski

@p-kaczynski

considering any type of validation strategies.

The library support validation in property setter as described here:

It integrate the validation Exception Messages with ERROR(s) in help.

moh-hassan avatar Aug 02 '19 21:08 moh-hassan

@moh-hassan

This is fairly limited, as we don’t know the order in which the properties are set, and if an option might or might not be valid depending on another parameter, there isn’t a way to do it.

p-kaczynski avatar Aug 08 '19 22:08 p-kaczynski

@p-kaczynski The limitation of Property Setter validation can be enhanced by submitting a new PR. I find your approach of IValidatedOptions is promising and it can be executed at the final stage of Parsing.

Also,an alternative, to minimize the overhead of parser validation, why not validation is executed as a first step in WithParsed method specially this validation is the business logic responsibility.

moh-hassan avatar Aug 11 '19 08:08 moh-hassan

Python's command line parser uses the type= option to do this. https://docs.python.org/2/library/argparse.html#type

donnyv avatar Aug 19 '19 19:08 donnyv

@donnyv Yes, Python validation is similar to the property setter validation in commandLineParser. What about if validation is applied per property and one property is depending on another one?

moh-hassan avatar Aug 19 '19 20:08 moh-hassan

The way I do this in the meantime is by adding a Validate method to my options class:

[Verb("open", HelpText = "Open the foo file")]
public class FooOptions
{
    [Option('f', "fooPath", Required = true, HelpText = "Foo")]
    public string FooPath { get; }

    protected FooOptions(string fooPath)
    {
        FooPath = fooPath;
    }

    public void Validate()
    {
        if (!File.Exists(FooPath))
        {
            throw new ArgumentValidationException($"Foo path does not exist!");
        }
    }
}

The exception I'm throwing is just a custom type:

class ArgumentValidationException : Exception
{
    public ArgumentValidationException(string message) : base(message) { }
}

And in my parsing code I do something like this:

static int Main(string[] args)
{
    try
    {
        return Parser.Default.ParseArguments<FooOptions>(args)
            .MapResult(
                (FooOptions opts) =>
                {
                    opts.Validate();
                    //do something with opts.FooPath
                    return 0;
                },
                errors => 1);
    }
    catch (ArgumentValidationException e)
    {
        Console.WriteLine("Invalid arguments detected: {0}", e);
        return 1;
    }
}

ohadschn avatar Aug 21 '19 15:08 ohadschn

@ohadschn How about custom types? Like ObjectId in Mongodb C# library.

donnyv avatar Aug 21 '19 16:08 donnyv

@donnyv presumably a custom type can be initialized by primitives. In the case of MongoDB ObjectId it looks like it can be created from a string. So the options class could look like this:

[Verb("get", HelpText = "get a Mongo object")]
public class MongoOptions
{
    private readonly string _objectIdStr;
    [Option('o', "objectId", Required = true, HelpText = "MongoDB Object ID")]
    public string ObjectIdStr => 
       throw new InvalidOperationException($"You should use the {nameof(ObjectId)} property");
    
    private ObjectId _objectId;
    public ObjectId ObjectId => _objectId;

    public MongoOptions(string objectIdStr)
    {
        _objectIdStr = objectIdStr;
    }
    
    public void Init()
    {
        try
        {
            _objectId = new ObjectId(_objectIdStr);
        }
        catch (Exception e)
        {
            throw new ArgumentException("Error initializing MongoDB object ID", e);
        }
        
        Validate();
    }

    public void Validate()
    {
        if (_objectId.CreationTime < DateTime.Now.AddYears(-3))
        {
            throw new ArgumentValidationException($"Object too old!");
        }
    }
}

And you would call Init instead of Validate in your parsing logic (e.g. Program.Main).

ohadschn avatar Aug 22 '19 20:08 ohadschn

Hi,

Any news about this subject ? I need to do a custom validation, and when this custom validation fails, I would like to use the error flow (RunErrors) and not the normal flow (Run) :

            var parserResult = parser.ParseArguments(args, optionsClasses);
            parserResult.MapResult(Run, errors => RunErrors(errors, parserResult, args));

I found the idea of @p-kaczynski very good : If we have an interface like he describe IValidatedOptions, it's exactly my need...

Regards Sybaris

sybaris avatar Sep 16 '23 07:09 sybaris