Skip to content

Commit 2272271

Browse files
committed
Initial implementation of parameter validation
* Adds support for CommandOption.IsRequired() and CommandArgument.IsRequired(). * Adds support for using ValidationAttribute on option and argument properties * Add samples for validation * Add ConsoleExtensions
1 parent 3835c76 commit 2272271

30 files changed

+840
-36
lines changed

CommandLineUtils.sln

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
Microsoft Visual Studio Solution File, Format Version 12.00
22
# Visual Studio 15
3-
VisualStudioVersion = 15.0.27004.2009
3+
VisualStudioVersion = 15.0.27004.2010
44
MinimumVisualStudioVersion = 15.0.26124.0
55
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{95D4B35E-0A21-4D64-8BAF-27DD6C019FC5}"
66
EndProject
@@ -42,6 +42,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HelloWorld.Attributes", "sa
4242
EndProject
4343
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Prompt", "samples\Prompt\Prompt.csproj", "{E6E5047D-2CCD-4824-9E71-9976079D2749}"
4444
EndProject
45+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Validation", "samples\Validation\Validation.csproj", "{72E4413E-9CDE-4850-93E9-4A83673EC0B0}"
46+
EndProject
47+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Validation.Attributes", "samples\Validation.Attributes\Validation.Attributes.csproj", "{6FBE4220-95D5-4AB9-8586-0CF1C9D6C931}"
48+
EndProject
4549
Global
4650
GlobalSection(SolutionConfigurationPlatforms) = preSolution
4751
Debug|Any CPU = Debug|Any CPU
@@ -80,6 +84,14 @@ Global
8084
{E6E5047D-2CCD-4824-9E71-9976079D2749}.Debug|Any CPU.Build.0 = Debug|Any CPU
8185
{E6E5047D-2CCD-4824-9E71-9976079D2749}.Release|Any CPU.ActiveCfg = Release|Any CPU
8286
{E6E5047D-2CCD-4824-9E71-9976079D2749}.Release|Any CPU.Build.0 = Release|Any CPU
87+
{72E4413E-9CDE-4850-93E9-4A83673EC0B0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
88+
{72E4413E-9CDE-4850-93E9-4A83673EC0B0}.Debug|Any CPU.Build.0 = Debug|Any CPU
89+
{72E4413E-9CDE-4850-93E9-4A83673EC0B0}.Release|Any CPU.ActiveCfg = Release|Any CPU
90+
{72E4413E-9CDE-4850-93E9-4A83673EC0B0}.Release|Any CPU.Build.0 = Release|Any CPU
91+
{6FBE4220-95D5-4AB9-8586-0CF1C9D6C931}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
92+
{6FBE4220-95D5-4AB9-8586-0CF1C9D6C931}.Debug|Any CPU.Build.0 = Debug|Any CPU
93+
{6FBE4220-95D5-4AB9-8586-0CF1C9D6C931}.Release|Any CPU.ActiveCfg = Release|Any CPU
94+
{6FBE4220-95D5-4AB9-8586-0CF1C9D6C931}.Release|Any CPU.Build.0 = Release|Any CPU
8395
EndGlobalSection
8496
GlobalSection(SolutionProperties) = preSolution
8597
HideSolutionNode = FALSE
@@ -93,6 +105,8 @@ Global
93105
{074F1099-BEC5-496D-AA37-75A427A437E7} = {60B279C6-091B-4F3E-B21F-D71001C94660}
94106
{1820AD90-F8CD-4E0A-9DC1-6E79F44C2225} = {60B279C6-091B-4F3E-B21F-D71001C94660}
95107
{E6E5047D-2CCD-4824-9E71-9976079D2749} = {60B279C6-091B-4F3E-B21F-D71001C94660}
108+
{72E4413E-9CDE-4850-93E9-4A83673EC0B0} = {60B279C6-091B-4F3E-B21F-D71001C94660}
109+
{6FBE4220-95D5-4AB9-8586-0CF1C9D6C931} = {60B279C6-091B-4F3E-B21F-D71001C94660}
96110
EndGlobalSection
97111
GlobalSection(ExtensibilityGlobals) = postSolution
98112
SolutionGuid = {55FD25E0-565D-49F9-9370-28DA7196E539}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// Copyright (c) Nate McMaster.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.ComponentModel.DataAnnotations;
6+
using McMaster.Extensions.CommandLineUtils;
7+
8+
class Program
9+
{
10+
static int Main(string[] args) => CommandLineApplication.Execute<Program>(args);
11+
12+
[Required]
13+
[Option(Description = "Required. The message")]
14+
private string Message { get; }
15+
16+
[Required]
17+
[EmailAddress]
18+
[Option("--to <EMAIL>", Description = "Required. The recipient.")]
19+
public string To { get; }
20+
21+
[Required]
22+
[EmailAddress]
23+
[Option("--from <EMAIL>", Description = "Required. The sender.")]
24+
public string From { get; }
25+
26+
[Option(Description = "The colors should be red or blue")]
27+
[RedOrBlue]
28+
public string Color { get; }
29+
30+
private void OnExecute()
31+
{
32+
Console.WriteLine("From = " + From);
33+
Console.WriteLine("To = " + To);
34+
Console.WriteLine("Message = " + Message);
35+
}
36+
}
37+
38+
class RedOrBlueAttribute : ValidationAttribute
39+
{
40+
public RedOrBlueAttribute()
41+
: base("The value for {0} must be 'red' or 'blue'")
42+
{
43+
}
44+
45+
protected override ValidationResult IsValid(object value, ValidationContext context)
46+
{
47+
if (value == null || (value is string str && str != "red" && str != "blue"))
48+
{
49+
return new ValidationResult(FormatErrorMessage(context.DisplayName));
50+
}
51+
52+
return ValidationResult.Success;
53+
}
54+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFramework>netcoreapp2.0</TargetFramework>
6+
</PropertyGroup>
7+
8+
<ItemGroup>
9+
<ProjectReference Include="..\..\src\CommandLineUtils\McMaster.Extensions.CommandLineUtils.csproj" />
10+
</ItemGroup>
11+
12+
</Project>

samples/Validation/Program.cs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// Copyright (c) Nate McMaster.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.ComponentModel.DataAnnotations;
6+
using McMaster.Extensions.CommandLineUtils;
7+
using McMaster.Extensions.CommandLineUtils.Validation;
8+
9+
class Program
10+
{
11+
static int Main(string[] args)
12+
{
13+
var app = new CommandLineApplication();
14+
15+
var optionMessage = app.Option("-m|--message <MSG>", "Required. The message.", CommandOptionType.SingleValue)
16+
.IsRequired();
17+
18+
var optionReceiver = app.Option("--to <EMAIL>", "Required. The recipient.", CommandOptionType.SingleValue)
19+
.IsRequired();
20+
21+
var optionSender = app.Option("--from <EMAIL>", "Required. The sender.", CommandOptionType.SingleValue)
22+
.IsRequired();
23+
24+
var optionColor = app.Option("--color <COLOR>", "The color. Should be 'red' or 'blue'.", CommandOptionType.SingleValue);
25+
optionColor.Validators.Add(new MustBeBlueOrRedValidator());
26+
27+
app.OnExecute(() =>
28+
{
29+
Console.WriteLine("From = " + optionSender.Value());
30+
Console.WriteLine("To = " + optionReceiver.Value());
31+
Console.WriteLine("Message = " + optionMessage.Value());
32+
});
33+
34+
return app.Execute(args);
35+
}
36+
}
37+
38+
class MustBeBlueOrRedValidator : IOptionValidator
39+
{
40+
public ValidationResult GetValidationResult(CommandOption option, ValidationContext context)
41+
{
42+
// This validator only runs if there is a value
43+
if (!option.HasValue()) return ValidationResult.Success;
44+
var val = option.Value();
45+
46+
if (val != "red" && val != "blue")
47+
{
48+
return new ValidationResult($"The value for --{option.LongName} must be 'red' or 'blue'");
49+
}
50+
51+
return ValidationResult.Success;
52+
}
53+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFramework>netcoreapp2.0</TargetFramework>
6+
</PropertyGroup>
7+
8+
<ItemGroup>
9+
<ProjectReference Include="..\..\src\CommandLineUtils\McMaster.Extensions.CommandLineUtils.csproj" />
10+
</ItemGroup>
11+
12+
</Project>

src/CommandLineUtils/Attributes/ArgumentAttribute.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
5-
using System.Collections;
65
using System.Reflection;
76

87
namespace McMaster.Extensions.CommandLineUtils

src/CommandLineUtils/Attributes/OptionAttribute.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System;
55
using System.Reflection;
6+
using McMaster.Extensions.CommandLineUtils.Validation;
67

78
namespace McMaster.Extensions.CommandLineUtils
89
{

src/CommandLineUtils/Attributes/OptionAttributeBase.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
5-
using System.Reflection;
65

76
namespace McMaster.Extensions.CommandLineUtils
87
{

src/CommandLineUtils/CommandArgument.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33
// This file has been modified from the original form. See Notice.txt in the project root for more information.
44

5+
using System;
56
using System.Collections.Generic;
67
using System.Linq;
8+
using McMaster.Extensions.CommandLineUtils.Validation;
79

810
namespace McMaster.Extensions.CommandLineUtils
911
{
@@ -51,5 +53,11 @@ public CommandArgument()
5153
/// The first value from <see cref="Values"/>, if any.
5254
/// </summary>
5355
public string Value => Values.FirstOrDefault();
56+
57+
/// <summary>
58+
/// A collection of validators that execute before invoking <see cref="CommandLineApplication.OnExecute(Func{int})"/>.
59+
/// When validation fails, <see cref="CommandLineApplication.ValidationErrorHandler"/> is invoked.
60+
/// </summary>
61+
public ICollection<IArgumentValidator> Validators { get; } = new List<IArgumentValidator>();
5462
}
5563
}

src/CommandLineUtils/CommandLineApplication.Execute.cs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// This file has been modified from the original form. See Notice.txt in the project root for more information.
44

55
using System;
6+
using System.ComponentModel.DataAnnotations;
67
using System.Reflection;
78
using System.Threading.Tasks;
89

@@ -50,6 +51,11 @@ public static int Execute<TApp>(IConsole console, params string[] args)
5051
return 0;
5152
}
5253

54+
if (bindResult.ValidationResult != ValidationResult.Success)
55+
{
56+
return HandleValidationError<TApp>(console, bindResult);
57+
}
58+
5359
var invoker = ExecuteMethodInvoker.Create(bindResult.Target.GetType());
5460
switch (invoker)
5561
{
@@ -98,6 +104,11 @@ public static async Task<int> ExecuteAsync<TApp>(IConsole console, params string
98104
return 0;
99105
}
100106

107+
if (bindResult.ValidationResult != ValidationResult.Success)
108+
{
109+
return HandleValidationError<TApp>(console, bindResult);
110+
}
111+
101112
var invoker = ExecuteMethodInvoker.Create(bindResult.Target.GetType());
102113
switch (invoker)
103114
{
@@ -110,6 +121,24 @@ public static async Task<int> ExecuteAsync<TApp>(IConsole console, params string
110121
}
111122
}
112123

124+
private static int HandleValidationError<TApp>(IConsole console, BindContext bindResult)
125+
{
126+
var method = typeof(TApp).GetTypeInfo().GetMethod("OnValidationError", BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
127+
if (method == null)
128+
{
129+
return bindResult.App.DefaultValidationErrorHandler(bindResult.ValidationResult);
130+
}
131+
132+
var arguments = ReflectionHelper.BindParameters(method, console, bindResult);
133+
var result = method.Invoke(bindResult.Target, arguments);
134+
if (method.ReturnType == typeof(int))
135+
{
136+
return (int)result;
137+
}
138+
139+
return 1;
140+
}
141+
113142
private static BindContext Bind<TApp>(IConsole console, string[] args) where TApp : class, new()
114143
{
115144
if (console == null)

0 commit comments

Comments
 (0)