Skip to content

Commit a5a9d45

Browse files
authored
Merge pull request #748 from jonsequitur/FileSystemInfo-binding
FileSystemInfo binding
2 parents 0843500 + 76d4fa9 commit a5a9d45

File tree

7 files changed

+206
-54
lines changed

7 files changed

+206
-54
lines changed
Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
11
// Copyright (c) .NET Foundation and contributors. All rights reserved.
22
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
33

4-
using System.Collections.Generic;
5-
64
namespace System.CommandLine.Tests.Binding
75
{
86
public class BindingTestCase
97
{
108
private readonly Action<object> _assertBoundValue;
11-
12-
public BindingTestCase(
9+
10+
private BindingTestCase(
1311
string commandLine,
1412
Type parameterType,
15-
Action<object> assertBoundValue)
13+
Action<object> assertBoundValue,
14+
string variationName)
1615
{
1716
_assertBoundValue = assertBoundValue;
17+
VariationName = variationName;
1818
CommandLine = commandLine;
1919
ParameterType = parameterType;
2020
}
@@ -23,23 +23,21 @@ public BindingTestCase(
2323

2424
public Type ParameterType { get; }
2525

26+
public string VariationName { get; }
27+
2628
public void AssertBoundValue(object value)
2729
{
2830
_assertBoundValue(value);
2931
}
3032

3133
public static BindingTestCase Create<T>(
3234
string commandLine,
33-
Action<T> assertBoundValue) =>
35+
Action<T> assertBoundValue,
36+
string variationName = null) =>
3437
new BindingTestCase(
3538
commandLine,
3639
typeof(T),
37-
o => assertBoundValue((T)o)
38-
);
39-
}
40-
41-
public class BindingTestSet : Dictionary<Type, BindingTestCase>
42-
{
43-
public void Add(BindingTestCase testCase) => Add(testCase.ParameterType, testCase);
40+
o => assertBoundValue((T) o),
41+
variationName);
4442
}
45-
}
43+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Copyright (c) .NET Foundation and contributors. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
using System.Collections.Generic;
5+
6+
namespace System.CommandLine.Tests.Binding
7+
{
8+
public class BindingTestSet : Dictionary<(Type type, string variationName), BindingTestCase>
9+
{
10+
public void Add(BindingTestCase testCase)
11+
{
12+
Add((testCase.ParameterType, testCase.VariationName), testCase);
13+
}
14+
15+
public BindingTestCase this[Type type] => base[(type, null)];
16+
}
17+
}

src/System.CommandLine.Tests/Binding/ModelBindingCommandHandlerTests.cs

Lines changed: 141 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
using System.Reflection;
1111
using System.Threading.Tasks;
1212
using FluentAssertions;
13+
using FluentAssertions.Execution;
1314
using Xunit;
1415

1516
namespace System.CommandLine.Tests.Binding
@@ -251,6 +252,28 @@ public async Task When_argument_type_is_not_known_until_binding_then_int_paramet
251252
received.Should().Be(123);
252253
}
253254

255+
[Fact]
256+
public void When_argument_type_is_more_specific_than_parameter_type_then_parameter_is_bound_correctly()
257+
{
258+
FileSystemInfo received = null;
259+
260+
var root = new RootCommand
261+
{
262+
new Option<DirectoryInfo>("-f")
263+
};
264+
root.Handler = CommandHandler.Create<FileSystemInfo>(f => received = f);
265+
var path = $"{Directory.GetCurrentDirectory()}{Path.DirectorySeparatorChar}";
266+
267+
root.Invoke($"-f {path}");
268+
269+
received.Should()
270+
.BeOfType<DirectoryInfo>()
271+
.Which
272+
.FullName
273+
.Should()
274+
.Be(path);
275+
}
276+
254277
[Theory]
255278
[InlineData(typeof(ClassWithCtorParameter<int>), false)]
256279
[InlineData(typeof(ClassWithCtorParameter<int>), true)]
@@ -260,10 +283,23 @@ public async Task When_argument_type_is_not_known_until_binding_then_int_paramet
260283
[InlineData(typeof(ClassWithCtorParameter<string>), true)]
261284
[InlineData(typeof(ClassWithSetter<string>), false)]
262285
[InlineData(typeof(ClassWithSetter<string>), true)]
286+
263287
[InlineData(typeof(FileInfo), false)]
264288
[InlineData(typeof(FileInfo), true)]
265289
[InlineData(typeof(FileInfo[]), false)]
266290
[InlineData(typeof(FileInfo[]), true)]
291+
292+
[InlineData(typeof(DirectoryInfo), false)]
293+
[InlineData(typeof(DirectoryInfo), true)]
294+
[InlineData(typeof(DirectoryInfo[]), false)]
295+
[InlineData(typeof(DirectoryInfo[]), true)]
296+
297+
[InlineData(typeof(FileSystemInfo), true, nameof(ExistingFile))]
298+
[InlineData(typeof(FileSystemInfo), true, nameof(ExistingDirectory))]
299+
[InlineData(typeof(FileSystemInfo), true, nameof(NonexistentPathWithTrailingSlash))]
300+
[InlineData(typeof(FileSystemInfo), true, nameof(NonexistentPathWithTrailingAltSlash))]
301+
[InlineData(typeof(FileSystemInfo), true, nameof(NonexistentPathWithoutTrailingSlash))]
302+
267303
[InlineData(typeof(string[]), false)]
268304
[InlineData(typeof(string[]), true)]
269305
[InlineData(typeof(List<string>), false)]
@@ -274,9 +310,10 @@ public async Task When_argument_type_is_not_known_until_binding_then_int_paramet
274310
[InlineData(typeof(List<int>), true)]
275311
public async Task Handler_method_receives_option_arguments_bound_to_the_specified_type(
276312
Type type,
277-
bool useDelegate)
313+
bool useDelegate,
314+
string variation = null)
278315
{
279-
var testCase = _bindingCases[type];
316+
var testCase = _bindingCases[(type, variation)];
280317

281318
ICommandHandler handler;
282319
if (!useDelegate)
@@ -318,7 +355,7 @@ public async Task Handler_method_receives_option_arguments_bound_to_the_specifie
318355

319356
var boundValue = ((BoundValueCapturer)invocationContext.InvocationResult).BoundValue;
320357

321-
boundValue.Should().BeOfType(testCase.ParameterType);
358+
boundValue.Should().BeAssignableTo(testCase.ParameterType);
322359

323360
testCase.AssertBoundValue(boundValue);
324361
}
@@ -443,14 +480,93 @@ public void Apply(InvocationContext context)
443480
o => o.Value.Should().Be("123")),
444481

445482
BindingTestCase.Create<FileInfo>(
446-
Path.Combine(Directory.GetCurrentDirectory(), "file1.txt"),
447-
o => o.FullName.Should().Be(Path.Combine(Directory.GetCurrentDirectory(), "file1.txt"))),
483+
Path.Combine(ExistingDirectory(), "file1.txt"),
484+
o => o.FullName
485+
.Should()
486+
.Be(Path.Combine(ExistingDirectory(), "file1.txt"))),
448487

449488
BindingTestCase.Create<FileInfo[]>(
450-
$"{Path.Combine(Directory.GetCurrentDirectory(), "file1.txt")} {Path.Combine(Directory.GetCurrentDirectory(), "file2.txt")}",
489+
$"{Path.Combine(ExistingDirectory(), "file1.txt")} {Path.Combine(ExistingDirectory(), "file2.txt")}",
451490
o => o.Select(f => f.FullName)
452491
.Should()
453-
.BeEquivalentTo(new[] { Path.Combine(Directory.GetCurrentDirectory(), "file1.txt"), Path.Combine(Directory.GetCurrentDirectory(), "file2.txt") })),
492+
.BeEquivalentTo(new[]
493+
{
494+
Path.Combine(ExistingDirectory(), "file1.txt"),
495+
Path.Combine(ExistingDirectory(), "file2.txt")
496+
})),
497+
498+
BindingTestCase.Create<DirectoryInfo>(
499+
ExistingDirectory(),
500+
fsi => fsi.Should()
501+
.BeOfType<DirectoryInfo>()
502+
.Which
503+
.FullName
504+
.Should()
505+
.Be(ExistingDirectory())),
506+
507+
BindingTestCase.Create<DirectoryInfo[]>(
508+
$"{ExistingDirectory()} {ExistingDirectory()}",
509+
fsi => fsi.Should()
510+
.BeAssignableTo<IEnumerable<DirectoryInfo>>()
511+
.Which
512+
.Select(d => d.FullName)
513+
.Should()
514+
.BeEquivalentTo(new[]
515+
{
516+
ExistingDirectory(),
517+
ExistingDirectory()
518+
})),
519+
520+
BindingTestCase.Create<FileSystemInfo>(
521+
ExistingFile(),
522+
fsi => fsi.Should()
523+
.BeOfType<FileInfo>()
524+
.Which
525+
.FullName
526+
.Should()
527+
.Be(ExistingFile()),
528+
variationName: nameof(ExistingFile)),
529+
530+
BindingTestCase.Create<FileSystemInfo>(
531+
ExistingDirectory(),
532+
fsi => fsi.Should()
533+
.BeOfType<DirectoryInfo>()
534+
.Which
535+
.FullName
536+
.Should()
537+
.Be(ExistingDirectory()),
538+
variationName: nameof(ExistingDirectory)),
539+
540+
BindingTestCase.Create<FileSystemInfo>(
541+
NonexistentPathWithTrailingSlash(),
542+
fsi => fsi.Should()
543+
.BeOfType<DirectoryInfo>()
544+
.Which
545+
.FullName
546+
.Should()
547+
.Be(NonexistentPathWithTrailingSlash()),
548+
variationName: nameof(NonexistentPathWithTrailingSlash)),
549+
550+
BindingTestCase.Create<FileSystemInfo>(
551+
NonexistentPathWithTrailingAltSlash(),
552+
fsi => fsi.Should()
553+
.BeOfType<DirectoryInfo>()
554+
.Which
555+
.FullName
556+
.Should()
557+
.Be(NonexistentPathWithTrailingSlash(),
558+
"DirectoryInfo replaces Path.AltDirectorySeparatorChar with Path.DirectorySeparatorChar on Windows"),
559+
variationName: nameof(NonexistentPathWithTrailingAltSlash)),
560+
561+
BindingTestCase.Create<FileSystemInfo>(
562+
NonexistentPathWithoutTrailingSlash(),
563+
fsi => fsi.Should()
564+
.BeOfType<FileInfo>()
565+
.Which
566+
.FullName
567+
.Should()
568+
.Be(NonexistentPathWithoutTrailingSlash()),
569+
variationName: nameof(NonexistentPathWithoutTrailingSlash)),
454570

455571
BindingTestCase.Create<string[]>(
456572
"one two",
@@ -468,5 +584,23 @@ public void Apply(InvocationContext context)
468584
"1 2",
469585
o => o.Should().BeEquivalentTo(new List<int> { 1, 2 }))
470586
};
587+
588+
private static string NonexistentPathWithoutTrailingSlash()
589+
{
590+
return Path.Combine(
591+
ExistingDirectory(),
592+
"does-not-exist");
593+
}
594+
595+
private static string NonexistentPathWithTrailingSlash() =>
596+
NonexistentPathWithoutTrailingSlash() + Path.DirectorySeparatorChar;
597+
private static string NonexistentPathWithTrailingAltSlash() =>
598+
NonexistentPathWithoutTrailingSlash() + Path.AltDirectorySeparatorChar;
599+
600+
private static string ExistingFile() =>
601+
Directory.GetFiles(ExistingDirectory()).FirstOrDefault() ??
602+
throw new AssertionFailedException("No files found in current directory");
603+
604+
private static string ExistingDirectory() => Directory.GetCurrentDirectory();
471605
}
472606
}

src/System.CommandLine/Binding/ArgumentConverter.cs

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Collections.Generic;
66
using System.CommandLine.Parsing;
77
using System.ComponentModel;
8+
using System.IO;
89
using System.Linq;
910
using System.Reflection;
1011
using static System.CommandLine.Binding.ArgumentConversionResult;
@@ -13,6 +14,25 @@ namespace System.CommandLine.Binding
1314
{
1415
internal static class ArgumentConverter
1516
{
17+
private static readonly Dictionary<Type, Func<string, object>> _converters = new Dictionary<Type, Func<string, object>>
18+
{
19+
[typeof(FileSystemInfo)] = value =>
20+
{
21+
if (Directory.Exists(value))
22+
{
23+
return new DirectoryInfo(value);
24+
}
25+
26+
if (value.EndsWith(Path.DirectorySeparatorChar.ToString()) ||
27+
value.EndsWith(Path.AltDirectorySeparatorChar.ToString()))
28+
{
29+
return new DirectoryInfo(value);
30+
}
31+
32+
return new FileInfo(value);
33+
}
34+
};
35+
1636
internal static ArgumentConversionResult ConvertObject(
1737
IArgument argument,
1838
Type type,
@@ -54,7 +74,7 @@ private static ArgumentConversionResult ConvertString(
5474
{
5575
type ??= typeof(string);
5676

57-
if (TypeDescriptor.GetConverter(type) is TypeConverter typeConverter)
77+
if (TypeDescriptor.GetConverter(type) is { } typeConverter)
5878
{
5979
if (typeConverter.CanConvertFrom(typeof(string)))
6080
{
@@ -71,6 +91,20 @@ private static ArgumentConversionResult ConvertString(
7191
}
7292
}
7393

94+
if (_converters.TryGetValue(type, out var convert))
95+
{
96+
try
97+
{
98+
return Success(
99+
argument,
100+
convert(value));
101+
}
102+
catch (Exception)
103+
{
104+
return Failure(argument, type, value);
105+
}
106+
}
107+
74108
if (type.TryFindConstructorWithSingleParameterOfType(
75109
typeof(string), out (ConstructorInfo ctor, ParameterDescriptor parameterDescriptor) tuple))
76110
{

src/System.CommandLine/Binding/Binder.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ namespace System.CommandLine.Binding
99
internal static class Binder
1010
{
1111
internal static bool IsMatch(this string parameterName, string alias) =>
12-
String.Equals(alias?.RemovePrefix()
12+
string.Equals(alias?.RemovePrefix()
1313
.Replace("-", ""),
1414
parameterName,
1515
StringComparison.OrdinalIgnoreCase);

src/System.CommandLine/Option{T}.cs

Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -80,31 +80,6 @@ public Option(
8080

8181
Argument = new Argument<T>(getDefaultValue);
8282
}
83-
public Option(
84-
string alias,
85-
T defaultValue,
86-
string description = null) : base(alias, description)
87-
{
88-
if (defaultValue is null)
89-
{
90-
throw new ArgumentNullException(nameof(defaultValue));
91-
}
92-
93-
Argument = new Argument<T>(() => defaultValue);
94-
}
95-
96-
public Option(
97-
string[] aliases,
98-
T defaultValue,
99-
string description = null) : base(aliases, description)
100-
{
101-
if (defaultValue is null)
102-
{
103-
throw new ArgumentNullException(nameof(defaultValue));
104-
}
105-
106-
Argument = new Argument<T>(() => defaultValue);
107-
}
10883

10984
public override Argument Argument
11085
{

src/System.CommandLine/Symbol.cs

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -126,12 +126,6 @@ public void AddAlias(string alias)
126126
}
127127
}
128128

129-
protected void ClearAliases()
130-
{
131-
_aliases.Clear();
132-
_rawAliases.Clear();
133-
}
134-
135129
public bool HasAlias(string alias)
136130
{
137131
if (string.IsNullOrWhiteSpace(alias))

0 commit comments

Comments
 (0)