diff --git a/appveyor.yml b/appveyor.yml index 827ded7f..ff6d4476 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,6 +1,6 @@ #version should be only changed with RELEASE eminent, see RELEASE.md -version: 2.7.83-beta-{build} +version: 2.8.0-beta-{build} image: Visual Studio 2019 @@ -15,8 +15,23 @@ init: if ($env:APPVEYOR_REPO_TAG -eq "true") { $ver = $env:APPVEYOR_REPO_TAG_NAME if($ver.StartsWith("v") -eq $true) { $ver = $ver.Substring(1) } - Update-AppveyorBuild -Version $ver + $env:PACKAGE_VERSION = $ver + } else { + $env:PACKAGE_VERSION = $env:APPVEYOR_BUILD_VERSION } + - ps: | + Write-Host "PACKAGE_VERSION:$env:PACKAGE_VERSION | APPVEYOR_BUILD_VERSION='$env:APPVEYOR_BUILD_VERSION'" -ForegroundColor Yellow + Write-Host "APPVEYOR_REPO_TAG_NAME:$env:APPVEYOR_REPO_TAG_NAME'" -ForegroundColor Yellow + +skip_commits: + files: + - docs/* + - art/* + - '**/*.md' + #- .travis.yml + - .gitignore + - .editorconfig + message: /updated readme.*|update readme.*s|update docs.*|update version.*|update changelog.*/ environment: matrix: @@ -24,13 +39,13 @@ environment: - BUILD_TARGET: fsharp build_script: -- cmd: dotnet build src/CommandLine/ -c Release --version-suffix %APPVEYOR_BUILD_VERSION% /p:BuildTarget=%BUILD_TARGET% +- cmd: dotnet build src/CommandLine/ -c Release --version-suffix %PACKAGE_VERSION% /p:BuildTarget=%BUILD_TARGET% test_script: - cmd: dotnet test tests/CommandLine.Tests/ /p:BuildTarget=%BUILD_TARGET% after_test: -- cmd: dotnet pack src/CommandLine/ -c Release --version-suffix %APPVEYOR_BUILD_VERSION% /p:BuildTarget=%BUILD_TARGET% +- cmd: dotnet pack src/CommandLine/ -c Release --version-suffix %PACKAGE_VERSION% /p:BuildTarget=%BUILD_TARGET% artifacts: - path: 'src/CommandLine/bin/Release/*.nupkg' @@ -57,3 +72,4 @@ deploy: artifact: 'NuGetPackages' on: APPVEYOR_REPO_TAG: true + diff --git a/demo/ReadText.Demo/Options.cs b/demo/ReadText.Demo/Options.cs index ed4db350..3b14014a 100644 --- a/demo/ReadText.Demo/Options.cs +++ b/demo/ReadText.Demo/Options.cs @@ -27,7 +27,7 @@ interface IOptions string FileName { get; set; } } - [Verb("head", HelpText = "Displays first lines of a file.")] + [Verb("head", true, HelpText = "Displays first lines of a file.")] class HeadOptions : IOptions { public uint? Lines { get; set; } @@ -62,4 +62,4 @@ class TailOptions : IOptions public string FileName { get; set; } } -} \ No newline at end of file +} diff --git a/src/CommandLine/CommandLine.csproj b/src/CommandLine/CommandLine.csproj index b05cbb04..4dac1db1 100644 --- a/src/CommandLine/CommandLine.csproj +++ b/src/CommandLine/CommandLine.csproj @@ -4,20 +4,20 @@ CommandLine Library netstandard2.0;net40;net45;net461 - $(DefineConstants);CSX_EITHER_INTERNAL;CSX_REM_EITHER_BEYOND_2;CSX_ENUM_INTERNAL;ERRH_INTERNAL;ERRH_DISABLE_INLINE_METHODS;CSX_MAYBE_INTERNAL;CSX_REM_EITHER_FUNC;CSX_REM_CRYPTORAND + $(DefineConstants);CSX_EITHER_INTERNAL;CSX_REM_EITHER_BEYOND_2;CSX_ENUM_INTERNAL;ERRH_INTERNAL;CSX_MAYBE_INTERNAL;CSX_REM_EITHER_FUNC;CSX_REM_CRYPTORAND;ERRH_ADD_MAYBE_METHODS $(DefineConstants);SKIP_FSHARP true ..\..\CommandLine.snk true CommandLineParser CommandLineParser.FSharp - gsscoder;nemec;ericnewton76 + gsscoder;nemec;ericnewton76;moh-hassan Command Line Parser Library $(VersionSuffix) 0.0.0 Terse syntax C# command line parser for .NET. For FSharp support see CommandLineParser.FSharp. The Command Line Parser Library offers to CLR applications a clean and concise API for manipulating command line arguments and related tasks. Terse syntax C# command line parser for .NET with F# support. The Command Line Parser Library offers to CLR applications a clean and concise API for manipulating command line arguments and related tasks. - Copyright (c) 2005 - 2018 Giacomo Stelluti Scala & Contributors + Copyright (c) 2005 - 2020 Giacomo Stelluti Scala & Contributors License.md CommandLine20.png https://github.com/commandlineparser/commandline diff --git a/src/CommandLine/Core/InstanceBuilder.cs b/src/CommandLine/Core/InstanceBuilder.cs index 4aff4080..0ae564b5 100644 --- a/src/CommandLine/Core/InstanceBuilder.cs +++ b/src/CommandLine/Core/InstanceBuilder.cs @@ -98,10 +98,10 @@ public static ParserResult Build( var validationErrors = specPropsWithValue.Validate(SpecificationPropertyRules.Lookup(tokens)); var allErrors = - tokenizerResult.SuccessfulMessages() + tokenizerResult.SuccessMessages() .Concat(missingValueErrors) - .Concat(optionSpecPropsResult.SuccessfulMessages()) - .Concat(valueSpecPropsResult.SuccessfulMessages()) + .Concat(optionSpecPropsResult.SuccessMessages()) + .Concat(valueSpecPropsResult.SuccessMessages()) .Concat(validationErrors) .Concat(setPropertyErrors) .Memoize(); diff --git a/src/CommandLine/Core/InstanceChooser.cs b/src/CommandLine/Core/InstanceChooser.cs index 86917233..f3ab9b99 100644 --- a/src/CommandLine/Core/InstanceChooser.cs +++ b/src/CommandLine/Core/InstanceChooser.cs @@ -23,6 +23,15 @@ public static ParserResult Choose( bool autoVersion, IEnumerable nonFatalErrors) { + var verbs = Verb.SelectFromTypes(types); + var defaultVerbs = verbs.Where(t => t.Item1.IsDefault); + + int defaultVerbCount = defaultVerbs.Count(); + if (defaultVerbCount > 1) + return MakeNotParsed(types, new MultipleDefaultVerbsError()); + + var defaultVerb = defaultVerbCount == 1 ? defaultVerbs.First() : null; + Func> choose = () => { var firstArg = arguments.First(); @@ -31,25 +40,52 @@ public static ParserResult Choose( nameComparer.Equals(command, firstArg) || nameComparer.Equals(string.Concat("--", command), firstArg); - var verbs = Verb.SelectFromTypes(types); - return (autoHelp && preprocCompare("help")) ? MakeNotParsed(types, MakeHelpVerbRequestedError(verbs, arguments.Skip(1).FirstOrDefault() ?? string.Empty, nameComparer)) : (autoVersion && preprocCompare("version")) ? MakeNotParsed(types, new VersionRequestedError()) - : MatchVerb(tokenizer, verbs, arguments, nameComparer, ignoreValueCase, parsingCulture, autoHelp, autoVersion, nonFatalErrors); + : MatchVerb(tokenizer, verbs, defaultVerb, arguments, nameComparer, ignoreValueCase, parsingCulture, autoHelp, autoVersion, nonFatalErrors); }; return arguments.Any() ? choose() - : MakeNotParsed(types, new NoVerbSelectedError()); + : (defaultVerbCount == 1 + ? MatchDefaultVerb(tokenizer, verbs, defaultVerb, arguments, nameComparer, ignoreValueCase, parsingCulture, autoHelp, autoVersion, nonFatalErrors) + : MakeNotParsed(types, new NoVerbSelectedError())); + } + + private static ParserResult MatchDefaultVerb( + Func, IEnumerable, Result, Error>> tokenizer, + IEnumerable> verbs, + Tuple defaultVerb, + IEnumerable arguments, + StringComparer nameComparer, + bool ignoreValueCase, + CultureInfo parsingCulture, + bool autoHelp, + bool autoVersion, + IEnumerable nonFatalErrors) + { + return !(defaultVerb is null) + ? InstanceBuilder.Build( + Maybe.Just>(() => defaultVerb.Item2.AutoDefault()), + tokenizer, + arguments, + nameComparer, + ignoreValueCase, + parsingCulture, + autoHelp, + autoVersion, + nonFatalErrors) + : MakeNotParsed(verbs.Select(v => v.Item2), new BadVerbSelectedError(arguments.First())); } private static ParserResult MatchVerb( Func, IEnumerable, Result, Error>> tokenizer, IEnumerable> verbs, + Tuple defaultVerb, IEnumerable arguments, StringComparer nameComparer, bool ignoreValueCase, @@ -71,7 +107,7 @@ private static ParserResult MatchVerb( autoHelp, autoVersion, nonFatalErrors) - : MakeNotParsed(verbs.Select(v => v.Item2), new BadVerbSelectedError(arguments.First())); + : MatchDefaultVerb(tokenizer, verbs, defaultVerb, arguments, nameComparer, ignoreValueCase, parsingCulture, autoHelp, autoVersion, nonFatalErrors); } private static HelpVerbRequestedError MakeHelpVerbRequestedError( diff --git a/src/CommandLine/Core/SpecificationPropertyRules.cs b/src/CommandLine/Core/SpecificationPropertyRules.cs index 9122ee3a..5dc1a406 100644 --- a/src/CommandLine/Core/SpecificationPropertyRules.cs +++ b/src/CommandLine/Core/SpecificationPropertyRules.cs @@ -18,12 +18,35 @@ public static IEnumerable, IEnumerable, IEnumerable> EnforceMutuallyExclusiveSetAndGroupAreNotUsedTogether() + { + return specProps => + { + var options = + from sp in specProps + where sp.Specification.IsOption() + let o = (OptionSpecification)sp.Specification + where o.SetName.Length > 0 + where o.Group.Length > 0 + select o; + + if (options.Any()) + { + return from o in options + select new GroupOptionAmbiguityError(new NameInfo(o.ShortName, o.LongName)); + } + + return Enumerable.Empty(); + }; + } + private static Func, IEnumerable> EnforceGroup() { return specProps => @@ -36,14 +59,15 @@ where o.Group.Length > 0 select new { Option = o, - Value = sp.Value + Value = sp.Value, + DefaultValue = sp.Specification.DefaultValue }; var groups = from o in optionsValues group o by o.Option.Group into g select g; - var errorGroups = groups.Where(gr => gr.All(g => g.Value.IsNothing())); + var errorGroups = groups.Where(gr => gr.All(g => g.Value.IsNothing() && g.DefaultValue.IsNothing())); if (errorGroups.Any()) { diff --git a/src/CommandLine/Core/Tokenizer.cs b/src/CommandLine/Core/Tokenizer.cs index 9fd8863c..ba6f1ef5 100644 --- a/src/CommandLine/Core/Tokenizer.cs +++ b/src/CommandLine/Core/Tokenizer.cs @@ -74,7 +74,7 @@ public static Result, Error> ExplodeOptionList( var flattened = exploded.SelectMany(x => x); - return Result.Succeed(flattened, tokenizerResult.SuccessfulMessages()); + return Result.Succeed(flattened, tokenizerResult.SuccessMessages()); } public static IEnumerable Normalize( diff --git a/src/CommandLine/Core/Verb.cs b/src/CommandLine/Core/Verb.cs index 2fb6674d..3a7f12a3 100644 --- a/src/CommandLine/Core/Verb.cs +++ b/src/CommandLine/Core/Verb.cs @@ -12,12 +12,17 @@ sealed class Verb private readonly string name; private readonly string helpText; private readonly bool hidden; + private readonly bool isDefault; - public Verb(string name, string helpText, bool hidden = false) + public Verb(string name, string helpText, bool hidden = false, bool isDefault = false) { - this.name = name ?? throw new ArgumentNullException(nameof(name)); + if ( string.IsNullOrWhiteSpace(name)) + throw new ArgumentNullException(nameof(name)); + this.name = name; + this.helpText = helpText ?? throw new ArgumentNullException(nameof(helpText)); this.hidden = hidden; + this.isDefault = isDefault; } public string Name @@ -35,12 +40,18 @@ public bool Hidden get { return hidden; } } + public bool IsDefault + { + get => isDefault; + } + public static Verb FromAttribute(VerbAttribute attribute) { return new Verb( attribute.Name, attribute.HelpText, - attribute.Hidden + attribute.Hidden, + attribute.IsDefault ); } @@ -54,4 +65,4 @@ select Tuple.Create( type); } } -} \ No newline at end of file +} diff --git a/src/CommandLine/Error.cs b/src/CommandLine/Error.cs index e54dbf6a..9359bc91 100644 --- a/src/CommandLine/Error.cs +++ b/src/CommandLine/Error.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; +using System.Linq; namespace CommandLine { @@ -74,7 +75,16 @@ public enum ErrorType /// /// Value of type. /// - MissingGroupOptionError + MissingGroupOptionError, + /// + + /// Value of type. + /// + MultipleDefaultVerbsError + + /// Value of type. + /// + GroupOptionAmbiguityError } @@ -532,7 +542,7 @@ internal InvalidAttributeConfigurationError() } } - public sealed class MissingGroupOptionError : Error + public sealed class MissingGroupOptionError : Error, IEquatable, IEquatable { public const string ErrorMessage = "At least one option in a group must have value."; @@ -555,5 +565,49 @@ public IEnumerable Names { get { return names; } } + + public new bool Equals(Error obj) + { + var other = obj as MissingGroupOptionError; + if (other != null) + { + return Equals(other); + } + + return base.Equals(obj); + } + + public bool Equals(MissingGroupOptionError other) + { + if (other == null) + { + return false; + } + + return Group.Equals(other.Group) && Names.SequenceEqual(other.Names); + } + } + + public sealed class GroupOptionAmbiguityError : NamedError + { + public NameInfo Option; + + internal GroupOptionAmbiguityError(NameInfo option) + : base(ErrorType.GroupOptionAmbiguityError, option) + { + Option = option; + } + } + + /// + /// Models an error generated when multiple default verbs are defined. + /// + public sealed class MultipleDefaultVerbsError : Error + { + public const string ErrorMessage = "More than one default verb is not allowed."; + + internal MultipleDefaultVerbsError() + : base(ErrorType.MultipleDefaultVerbsError) + { } } } diff --git a/src/CommandLine/Infrastructure/ErrorHandling.cs b/src/CommandLine/Infrastructure/ErrorHandling.cs index 142e1461..8aee4bac 100644 --- a/src/CommandLine/Infrastructure/ErrorHandling.cs +++ b/src/CommandLine/Infrastructure/ErrorHandling.cs @@ -1,62 +1,17 @@ //Use project level define(s) when referencing with Paket. -//#define ERRH_INTERNAL // Uncomment this to set visibility to internal. -//#define ERRH_DISABLE_INLINE_METHODS // Uncomment this to enable method inlining when compiling for >= NET 4.5. -//#define ERRH_BUILTIN_TYPES // Uncomment this to use built-in Unit type, instead of extenral identical CSharpx.Unit. +//#define ERRH_INTERNAL // Uncomment or define at build time to set accessibility to internal. +//#define ERRH_ENABLE_INLINE_METHODS // Uncomment or define at build time to enable method inlining when compiling for >= NET 4.5. +//#define ERRH_ADD_MAYBE_METHODS // Uncomment or define at build time to add methods that use Maybe type using System; using System.Collections.Generic; using System.Linq; -using System.Runtime.CompilerServices; -#if !ERRH_BUILTIN_TYPES +#if ERRH_ADD_MAYBE_METHODS using CSharpx; #endif namespace RailwaySharp.ErrorHandling { - #region Unit Type -#if ERRH_BUILTIN_TYPES -#if !ERRH_INTERNAL - public -#endif - struct Unit : IEquatable - { - private static readonly Unit @default = new Unit(); - - public bool Equals(Unit other) - { - return true; - } - - public override bool Equals(object obj) - { - return obj is Unit; - } - - public override int GetHashCode() - { - return 0; - } - - public override string ToString() - { - return "()"; - } - - public static bool operator ==(Unit first, Unit second) - { - return true; - } - - public static bool operator !=(Unit first, Unit second) - { - return false; - } - - public static Unit Default { get { return @default; } } - } -#endif - #endregion - #if !ERRH_INTERNAL public #endif @@ -76,29 +31,28 @@ enum ResultType #endif abstract class Result { - private readonly ResultType tag; + private readonly ResultType _tag; protected Result(ResultType tag) { - this.tag = tag; + _tag = tag; } public ResultType Tag { - get { return tag; } + get { return _tag; } } public override string ToString() { - switch (Tag) - { - case ResultType.Ok: + switch (Tag) { + default: var ok = (Ok)this; return string.Format( "OK: {0} - {1}", ok.Success, string.Join(Environment.NewLine, ok.Messages.Select(v => v.ToString()))); - default: + case ResultType.Bad: var bad = (Bad)this; return string.Format( "Error: {0}", @@ -117,22 +71,24 @@ public override string ToString() #endif sealed class Ok : Result { - private readonly Tuple> value; + private readonly Tuple> _value; public Ok(TSuccess success, IEnumerable messages) : base(ResultType.Ok) { - this.value = Tuple.Create(success, messages); + if (messages == null) throw new ArgumentNullException(nameof(messages)); + + _value = Tuple.Create(success, messages); } public TSuccess Success { - get { return value.Item1; } + get { return _value.Item1; } } public IEnumerable Messages { - get { return value.Item2; } + get { return _value.Item2; } } } @@ -146,17 +102,19 @@ public IEnumerable Messages #endif sealed class Bad : Result { - private readonly IEnumerable messages; + private readonly IEnumerable _messages; public Bad(IEnumerable messages) : base(ResultType.Bad) { - this.messages = messages; + if (messages == null) throw new ArgumentException(nameof(messages)); + + _messages = messages; } public IEnumerable Messages { - get { return messages; } + get { return _messages; } } } @@ -170,6 +128,8 @@ static class Result /// public static Result FailWith(IEnumerable messages) { + if (messages == null) throw new ArgumentException(nameof(messages)); + return new Bad(messages); } @@ -178,6 +138,8 @@ public static Result FailWith(IEnumerabl /// public static Result FailWith(TMessage message) { + if (message == null) throw new ArgumentException(nameof(message)); + return new Bad(new[] { message }); } @@ -194,6 +156,8 @@ public static Result Succeed(TSuccess va /// public static Result Succeed(TSuccess value, TMessage message) { + if (message == null) throw new ArgumentException(nameof(message)); + return new Ok(value, new[] { message }); } @@ -202,6 +166,8 @@ public static Result Succeed(TSuccess va /// public static Result Succeed(TSuccess value, IEnumerable messages) { + if (messages == null) throw new ArgumentException(nameof(messages)); + return new Ok(value, messages); } @@ -210,13 +176,13 @@ public static Result Succeed(TSuccess va /// public static Result Try(Func func) { - try - { + if (func == null) throw new ArgumentException(nameof(func)); + + try { return new Ok( func(), Enumerable.Empty()); } - catch (Exception ex) - { + catch (Exception ex) { return new Bad( new[] { ex }); } @@ -231,7 +197,7 @@ static class Trial /// /// Wraps a value in a Success. /// -#if !ERRH_DISABLE_INLINE_METHODS +#if ERRH_ENABLE_INLINE_METHODS [MethodImpl(MethodImplOptions.AggressiveInlining)] #endif public static Result Ok(TSuccess value) @@ -242,7 +208,7 @@ public static Result Ok(TSuccess value) /// /// Wraps a value in a Success. /// -#if !ERRH_DISABLE_INLINE_METHODS +#if ERRH_ENABLE_INLINE_METHODS [MethodImpl(MethodImplOptions.AggressiveInlining)] #endif public static Result Pass(TSuccess value) @@ -253,29 +219,33 @@ public static Result Pass(TSuccess value /// /// Wraps a value in a Success and adds a message. /// -#if !ERRH_DISABLE_INLINE_METHODS +#if ERRH_ENABLE_INLINE_METHODS [MethodImpl(MethodImplOptions.AggressiveInlining)] #endif public static Result Warn(TMessage message, TSuccess value) { + if (message == null) throw new ArgumentException(nameof(message)); + return new Ok(value, new[] { message }); } /// /// Wraps a message in a Failure. /// -#if !ERRH_DISABLE_INLINE_METHODS +#if ERRH_ENABLE_INLINE_METHODS [MethodImpl(MethodImplOptions.AggressiveInlining)] #endif public static Result Fail(TMessage message) { + if (message == null) throw new ArgumentException(nameof(message)); + return new Bad(new[] { message }); } /// /// Returns true if the result was not successful. /// -#if !ERRH_DISABLE_INLINE_METHODS +#if ERRH_ENABLE_INLINE_METHODS [MethodImpl(MethodImplOptions.AggressiveInlining)] #endif public static bool Failed(Result result) @@ -286,7 +256,7 @@ public static bool Failed(Result result) /// /// Takes a Result and maps it with successFunc if it is a Success otherwise it maps it with failureFunc. /// -#if !ERRH_DISABLE_INLINE_METHODS +#if ERRH_ENABLE_INLINE_METHODS [MethodImpl(MethodImplOptions.AggressiveInlining)] #endif public static TResult Either( @@ -294,9 +264,11 @@ public static TResult Either( Func, TResult> failureFunc, Result trialResult) { + if (successFunc == null) throw new ArgumentException(nameof(successFunc)); + if (failureFunc == null) throw new ArgumentException(nameof(failureFunc)); + var ok = trialResult as Ok; - if (ok != null) - { + if (ok != null) { return successFunc(ok.Success, ok.Messages); } var bad = (Bad)trialResult; @@ -307,7 +279,7 @@ public static TResult Either( /// If the given result is a Success the wrapped value will be returned. /// Otherwise the function throws an exception with Failure message of the result. /// -#if !ERRH_DISABLE_INLINE_METHODS +#if ERRH_ENABLE_INLINE_METHODS [MethodImpl(MethodImplOptions.AggressiveInlining)] #endif public static TSuccess ReturnOrFail(Result result) @@ -325,13 +297,15 @@ public static TSuccess ReturnOrFail(Result /// Appends the given messages with the messages in the given result. /// -#if !ERRH_DISABLE_INLINE_METHODS +#if ERRH_ENABLE_INLINE_METHODS [MethodImpl(MethodImplOptions.AggressiveInlining)] #endif public static Result MergeMessages( IEnumerable messages, Result result) { + if (messages == null) throw new ArgumentException(nameof(messages)); + Func, Result> successFunc = (succ, msgs) => new Ok( @@ -347,13 +321,15 @@ public static Result MergeMessages( /// If the result is a Success it executes the given function on the value. /// Otherwise the exisiting failure is propagated. /// -#if !ERRH_DISABLE_INLINE_METHODS +#if ERRH_ENABLE_INLINE_METHODS [MethodImpl(MethodImplOptions.AggressiveInlining)] #endif public static Result Bind( Func> func, Result result) { + if (func == null) throw new ArgumentException(nameof(func)); + Func, Result> successFunc = (succ, msgs) => MergeMessages(msgs, func(succ)); @@ -366,7 +342,7 @@ public static Result Bind( /// /// Flattens a nested result given the Failure types are equal. /// -#if !ERRH_DISABLE_INLINE_METHODS +#if ERRH_ENABLE_INLINE_METHODS [MethodImpl(MethodImplOptions.AggressiveInlining)] #endif public static Result Flatten( @@ -374,46 +350,44 @@ public static Result Flatten( { return Bind(x => x, result); } - + /// /// If the wrapped function is a success and the given result is a success the function is applied on the value. /// Otherwise the exisiting error messages are propagated. /// -#if !ERRH_DISABLE_INLINE_METHODS +#if ERRH_ENABLE_INLINE_METHODS [MethodImpl(MethodImplOptions.AggressiveInlining)] #endif public static Result Apply( Result, TMessage> wrappedFunction, Result result) { - if (wrappedFunction.Tag == ResultType.Ok && result.Tag == ResultType.Ok) - { + if (wrappedFunction == null) throw new ArgumentException(nameof(wrappedFunction)); + + if (wrappedFunction.Tag == ResultType.Ok && result.Tag == ResultType.Ok) { var ok1 = (Ok, TMessage>)wrappedFunction; var ok2 = (Ok)result; return new Ok( ok1.Success(ok2.Success), ok1.Messages.Concat(ok2.Messages)); } - if (wrappedFunction.Tag == ResultType.Bad && result.Tag == ResultType.Ok) - { + if (wrappedFunction.Tag == ResultType.Bad && result.Tag == ResultType.Ok) { return new Bad(((Bad)result).Messages); } - if (wrappedFunction.Tag == ResultType.Ok && result.Tag == ResultType.Bad) - { + if (wrappedFunction.Tag == ResultType.Ok && result.Tag == ResultType.Bad) { return new Bad( ((Bad)result).Messages); } var bad1 = (Bad, TMessage>)wrappedFunction; var bad2 = (Bad)result; - return new Bad(bad1.Messages.Concat(bad2.Messages)); } /// /// Lifts a function into a Result container and applies it on the given result. /// -#if !ERRH_DISABLE_INLINE_METHODS +#if ERRH_ENABLE_INLINE_METHODS [MethodImpl(MethodImplOptions.AggressiveInlining)] #endif public static Result Lift( @@ -426,22 +400,22 @@ public static Result Lift( /// /// Promote a function to a monad/applicative, scanning the monadic/applicative arguments from left to right. /// -#if !ERRH_DISABLE_INLINE_METHODS +#if ERRH_ENABLE_INLINE_METHODS [MethodImpl(MethodImplOptions.AggressiveInlining)] #endif public static Result Lift2( Func> func, - Result a, - Result b) + Result first, + Result second) { - return Apply(Lift(func, a), b); + return Apply(Lift(func, first), second); } /// /// Collects a sequence of Results and accumulates their values. /// If the sequence contains an error the error will be propagated. /// -#if !ERRH_DISABLE_INLINE_METHODS +#if ERRH_ENABLE_INLINE_METHODS [MethodImpl(MethodImplOptions.AggressiveInlining)] #endif public static Result, TMessage> Collect( @@ -449,21 +423,18 @@ public static Result, TMessage> Collect, Result, TMessage>, Result, TMessage>>( - null, - (result, next) => - { - if (result.Tag == ResultType.Ok && next.Tag == ResultType.Ok) - { + null, + (result, next) => { + if (result.Tag == ResultType.Ok && next.Tag == ResultType.Ok) { var ok1 = (Ok, TMessage>)result; var ok2 = (Ok)next; return new Ok, TMessage>( - Enumerable.Empty().Concat(new[] { ok2.Success }).Concat(ok1.Success), + Enumerable.Empty().Concat(new [] { ok2.Success }).Concat(ok1.Success), ok1.Messages.Concat(ok2.Messages)); } if ((result.Tag == ResultType.Ok && next.Tag == ResultType.Bad) - || (result.Tag == ResultType.Bad && next.Tag == ResultType.Ok)) - { + || (result.Tag == ResultType.Bad && next.Tag == ResultType.Ok)) { var m1 = result.Tag == ResultType.Ok ? ((Ok, TMessage>)result).Messages : ((Bad)next).Messages; @@ -472,8 +443,9 @@ public static Result, TMessage> Collect)next).Messages; return new Bad, TMessage>(m1.Concat(m2)); } + var bad1 = (Bad, TMessage>)result; - var bad2 = (Bad)next; + var bad2 = (Bad)next; return new Bad, TMessage>(bad1.Messages.Concat(bad2.Messages)); }, x => x)); } @@ -490,19 +462,22 @@ static class ResultExtensions /// /// Allows pattern matching on Results. /// -#if !ERRH_DISABLE_INLINE_METHODS +#if ERRH_ENABLE_INLINE_METHODS [MethodImpl(MethodImplOptions.AggressiveInlining)] #endif public static void Match(this Result result, Action> ifSuccess, Action> ifFailure) { + if (ifSuccess == null) throw new ArgumentException(nameof(ifSuccess)); + if (ifFailure == null) throw new ArgumentException(nameof(ifFailure)); + var ok = result as Ok; - if (ok != null) - { + if (ok != null) { ifSuccess(ok.Success, ok.Messages); return; } + var bad = (Bad)result; ifFailure(bad.Messages); } @@ -510,26 +485,20 @@ public static void Match(this Result res /// /// Allows pattern matching on Results. /// -#if !ERRH_DISABLE_INLINE_METHODS +#if ERRH_ENABLE_INLINE_METHODS [MethodImpl(MethodImplOptions.AggressiveInlining)] #endif public static TResult Either(this Result result, Func, TResult> ifSuccess, Func, TResult> ifFailure) { - var ok = result as Ok; - if (ok != null) - { - return ifSuccess(ok.Success, ok.Messages); - } - var bad = (Bad)result; - return ifFailure(bad.Messages); + return Trial.Either(ifSuccess, ifFailure, result); } /// /// Lifts a Func into a Result and applies it on the given result. /// -#if !ERRH_DISABLE_INLINE_METHODS +#if ERRH_ENABLE_INLINE_METHODS [MethodImpl(MethodImplOptions.AggressiveInlining)] #endif public static Result Map(this Result result, @@ -542,7 +511,7 @@ public static Result Map(this Re /// Collects a sequence of Results and accumulates their values. /// If the sequence contains an error the error will be propagated. /// -#if !ERRH_DISABLE_INLINE_METHODS +#if ERRH_ENABLE_INLINE_METHODS [MethodImpl(MethodImplOptions.AggressiveInlining)] #endif public static Result, TMessage> Collect( @@ -555,18 +524,16 @@ public static Result, TMessage> Collect -#if !ERRH_DISABLE_INLINE_METHODS +#if ERRH_ENABLE_INLINE_METHODS [MethodImpl(MethodImplOptions.AggressiveInlining)] #endif public static Result, TMessage> Flatten(this Result>, TMessage> result) { - if (result.Tag == ResultType.Ok) - { + if (result.Tag == ResultType.Ok) { var ok = (Ok>, TMessage>)result; var values = ok.Success; var result1 = Collect(values); - if (result1.Tag == ResultType.Ok) - { + if (result1.Tag == ResultType.Ok) { var ok1 = (Ok, TMessage>)result1; return new Ok, TMessage>(ok1.Success, ok1.Messages); } @@ -581,7 +548,7 @@ public static Result, TMessage> Flatten -#if !ERRH_DISABLE_INLINE_METHODS +#if ERRH_ENABLE_INLINE_METHODS [MethodImpl(MethodImplOptions.AggressiveInlining)] #endif public static Result SelectMany(this Result result, @@ -595,7 +562,7 @@ public static Result SelectMany( /// If the result of the Func is a Success it maps it using the given Func. /// Otherwise the exisiting failure is propagated. /// -#if !ERRH_DISABLE_INLINE_METHODS +#if ERRH_ENABLE_INLINE_METHODS [MethodImpl(MethodImplOptions.AggressiveInlining)] #endif public static Result SelectMany( @@ -603,6 +570,9 @@ public static Result SelectMany> func, Func mapperFunc) { + if (func == null) throw new ArgumentException(nameof(func)); + if (mapperFunc == null) throw new ArgumentException(nameof(mapperFunc)); + Func> curriedMapper = suc => val => mapperFunc(suc, val); Func< Result, @@ -616,7 +586,7 @@ public static Result SelectMany /// Lifts a Func into a Result and applies it on the given result. /// -#if !ERRH_DISABLE_INLINE_METHODS +#if ERRH_ENABLE_INLINE_METHODS [MethodImpl(MethodImplOptions.AggressiveInlining)] #endif public static Result Select(this Result result, @@ -628,13 +598,12 @@ public static Result Select(this /// /// Returns the error messages or fails if the result was a success. /// -#if !ERRH_DISABLE_INLINE_METHODS +#if ERRH_ENABLE_INLINE_METHODS [MethodImpl(MethodImplOptions.AggressiveInlining)] #endif public static IEnumerable FailedWith(this Result result) { - if (result.Tag == ResultType.Ok) - { + if (result.Tag == ResultType.Ok) { var ok = (Ok)result; throw new Exception( string.Format("Result was a success: {0} - {1}", @@ -648,13 +617,12 @@ public static IEnumerable FailedWith(this Result /// Returns the result or fails if the result was an error. /// -#if !ERRH_DISABLE_INLINE_METHODS +#if ERRH_ENABLE_INLINE_METHODS [MethodImpl(MethodImplOptions.AggressiveInlining)] #endif public static TSuccess SucceededWith(this Result result) { - if (result.Tag == ResultType.Ok) - { + if (result.Tag == ResultType.Ok) { var ok = (Ok)result; return ok.Success; } @@ -663,5 +631,34 @@ public static TSuccess SucceededWith(this Result m.ToString())))); } + + /// + /// Returns messages in case of success, otherwise an empty sequence. + /// + public static IEnumerable SuccessMessages(this Result result) + { + if (result.Tag == ResultType.Ok) { + var ok = (Ok)result; + return ok.Messages; + } + return Enumerable.Empty(); + } + +#if ERRH_ADD_MAYBE_METHODS +#if ERRH_ENABLE_INLINE_METHODS + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + /// + /// Builds a Maybe type instance from a Result one. + /// + public static Maybe ToMaybe(this Result result) + { + if (result.Tag == ResultType.Ok) { + var ok = (Ok)result; + return Maybe.Just(ok.Success); + } + return Maybe.Nothing(); + } +#endif } } \ No newline at end of file diff --git a/src/CommandLine/Infrastructure/ResultExtensions.cs b/src/CommandLine/Infrastructure/ResultExtensions.cs deleted file mode 100644 index bdc2a480..00000000 --- a/src/CommandLine/Infrastructure/ResultExtensions.cs +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright 2005-2015 Giacomo Stelluti Scala & Contributors. All rights reserved. See License.md in the project root for license information. - -using System.Collections.Generic; -using System.Linq; - -using CSharpx; -using RailwaySharp.ErrorHandling; - -namespace CommandLine.Infrastructure -{ - static class ResultExtensions - { - public static IEnumerable SuccessfulMessages(this Result result) - { - if (result.Tag == ResultType.Ok) - { - var ok = (Ok)result; - return ok.Messages; - } - return Enumerable.Empty(); - } - - public static Maybe ToMaybe(this Result result) - { - if (result.Tag == ResultType.Ok) - { - var ok = (Ok)result; - return Maybe.Just(ok.Success); - } - return Maybe.Nothing(); - } - } -} \ No newline at end of file diff --git a/src/CommandLine/Text/SentenceBuilder.cs b/src/CommandLine/Text/SentenceBuilder.cs index d58bf9c7..1863dea2 100644 --- a/src/CommandLine/Text/SentenceBuilder.cs +++ b/src/CommandLine/Text/SentenceBuilder.cs @@ -158,6 +158,14 @@ public override Func FormatError "' (", string.Join(", ", missingGroupOptionError.Names.Select(n => n.NameText)), ") is required."); + + case ErrorType.MultipleDefaultVerbsError: + return MultipleDefaultVerbsError.ErrorMessage; + + case ErrorType.GroupOptionAmbiguityError: + var groupOptionAmbiguityError = (GroupOptionAmbiguityError)error; + return "Both SetName and Group are not allowed in option: (".JoinTo(groupOptionAmbiguityError.Option.NameText, ")"); + } throw new InvalidOperationException(); }; diff --git a/src/CommandLine/VerbAttribute.cs b/src/CommandLine/VerbAttribute.cs index 5515bd20..57318b3a 100644 --- a/src/CommandLine/VerbAttribute.cs +++ b/src/CommandLine/VerbAttribute.cs @@ -12,6 +12,7 @@ namespace CommandLine public class VerbAttribute : Attribute { private readonly string name; + private readonly bool isDefault; private Infrastructure.LocalizableAttributeProperty helpText; private Type resourceType; @@ -19,12 +20,14 @@ public class VerbAttribute : Attribute /// Initializes a new instance of the class. /// /// The long name of the verb command. - /// Thrown if is null, empty or whitespace. - public VerbAttribute(string name) + /// Whether the verb is the default verb. + /// Thrown if is null, empty or whitespace and is false. + public VerbAttribute(string name, bool isDefault = false) { if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("name"); - this.name = name; + this.name = name ; + this.isDefault = isDefault; helpText = new Infrastructure.LocalizableAttributeProperty(nameof(HelpText)); resourceType = null; } @@ -62,5 +65,13 @@ public Type ResourceType get => resourceType; set => resourceType =helpText.ResourceType = value; } + + /// + /// Gets whether this verb is the default verb. + /// + public bool IsDefault + { + get => isDefault; + } } } diff --git a/tests/CommandLine.Tests/Fakes/Options_With_Multiple_Groups.cs b/tests/CommandLine.Tests/Fakes/Options_With_Multiple_Groups.cs new file mode 100644 index 00000000..8f2d21ab --- /dev/null +++ b/tests/CommandLine.Tests/Fakes/Options_With_Multiple_Groups.cs @@ -0,0 +1,20 @@ +namespace CommandLine.Tests.Fakes +{ + public class Options_With_Multiple_Groups + { + [Option('v', "version")] + public string Version { get; set; } + + [Option("option11", Group = "err-group")] + public string Option11 { get; set; } + + [Option("option12", Group = "err-group")] + public string Option12 { get; set; } + + [Option("option21", Group = "err-group2")] + public string Option21 { get; set; } + + [Option("option22", Group = "err-group2")] + public string Option22 { get; set; } + } +} diff --git a/tests/CommandLine.Tests/Fakes/Simple_Options_With_OptionGroup_MutuallyExclusiveSet.cs b/tests/CommandLine.Tests/Fakes/Simple_Options_With_OptionGroup_MutuallyExclusiveSet.cs new file mode 100644 index 00000000..52ead41c --- /dev/null +++ b/tests/CommandLine.Tests/Fakes/Simple_Options_With_OptionGroup_MutuallyExclusiveSet.cs @@ -0,0 +1,14 @@ +namespace CommandLine.Tests.Fakes +{ + public class Simple_Options_With_OptionGroup_MutuallyExclusiveSet + { + [Option(HelpText = "Define a string value here.", Group = "test", SetName = "setname", Default = "qwerty123")] + public string StringValue { get; set; } + + [Option('s', "shortandlong", HelpText = "Example with both short and long name.", Group = "test", SetName = "setname")] + public string ShortAndLong { get; set; } + + [Option('x', HelpText = "Define a boolean or switch value here.")] + public bool BoolValue { get; set; } + } +} diff --git a/tests/CommandLine.Tests/Fakes/Simple_Options_With_OptionGroup_WithOptionDefaultValue.cs b/tests/CommandLine.Tests/Fakes/Simple_Options_With_OptionGroup_WithOptionDefaultValue.cs new file mode 100644 index 00000000..9ae3a59e --- /dev/null +++ b/tests/CommandLine.Tests/Fakes/Simple_Options_With_OptionGroup_WithOptionDefaultValue.cs @@ -0,0 +1,14 @@ +namespace CommandLine.Tests.Fakes +{ + public class Simple_Options_With_OptionGroup_WithOptionDefaultValue + { + [Option(HelpText = "Define a string value here.", Required = true, Group = "test", Default = "qwerty123")] + public string StringValue { get; set; } + + [Option('s', "shortandlong", HelpText = "Example with both short and long name.", Required = true, Group = "test")] + public string ShortAndLong { get; set; } + + [Option('x', HelpText = "Define a boolean or switch value here.")] + public bool BoolValue { get; set; } + } +} diff --git a/tests/CommandLine.Tests/Fakes/Verb_Fakes.cs b/tests/CommandLine.Tests/Fakes/Verb_Fakes.cs index 133c65b5..83426bd7 100644 --- a/tests/CommandLine.Tests/Fakes/Verb_Fakes.cs +++ b/tests/CommandLine.Tests/Fakes/Verb_Fakes.cs @@ -85,4 +85,25 @@ class Verb_With_Option_And_Value_Of_String_Type [Value(0)] public string PosValue { get; set; } } -} \ No newline at end of file + + [Verb("default1", true)] + class Default_Verb_One + { + [Option('t', "test-one")] + public bool TestValueOne { get; set; } + } + + [Verb("default2", true)] + class Default_Verb_Two + { + [Option('t', "test-two")] + public bool TestValueTwo { get; set; } + } + + [Verb(null, true)] + class Default_Verb_With_Empty_Name + { + [Option('t', "test")] + public bool TestValue { get; set; } + } +} diff --git a/tests/CommandLine.Tests/Unit/Core/InstanceBuilderTests.cs b/tests/CommandLine.Tests/Unit/Core/InstanceBuilderTests.cs index c2cbb77a..643878fd 100644 --- a/tests/CommandLine.Tests/Unit/Core/InstanceBuilderTests.cs +++ b/tests/CommandLine.Tests/Unit/Core/InstanceBuilderTests.cs @@ -1110,6 +1110,34 @@ public void Options_In_Group_With_No_Values_Generates_MissingGroupOptionError() ((NotParsed)result).Errors.Should().BeEquivalentTo(expectedResult); } + [Fact] + public void Options_In_Group_With_No_Values_Generates_MissingGroupOptionErrors() + { + // Fixture setup + var optionNames1 = new List + { + new NameInfo("", "option11"), + new NameInfo("", "option12") + }; + var optionNames2 = new List + { + new NameInfo("", "option21"), + new NameInfo("", "option22") + }; + var expectedResult = new[] + { + new MissingGroupOptionError("err-group", optionNames1), + new MissingGroupOptionError("err-group2", optionNames2) + }; + + // Exercize system + var result = InvokeBuild( + new[] { "-v 10.42" }); + + // Verify outcome + ((NotParsed)result).Errors.Should().BeEquivalentTo(expectedResult); + } + [Theory] [InlineData("-v", "10.5", "--option1", "test1", "--option2", "test2")] [InlineData("-v", "10.5", "--option1", "test1")] @@ -1164,6 +1192,35 @@ public void Options_In_Group_Ignore_Option_Group_If_Option_Group_Name_Empty() errors.Should().BeEquivalentTo(expectedResult); } + [Fact] + public void Options_In_Group_Use_Option_Default_Value_When_Available() + { + // Exercize system + var result = InvokeBuild(new string[] { "-x" }); + + // Verify outcome + result.Should().BeOfType>(); + } + + [Fact] + public void Options_In_Group_Do_Not_Allow_Mutually_Exclusive_Set() + { + var expectedResult = new[] + { + new GroupOptionAmbiguityError(new NameInfo("", "stringvalue")), + new GroupOptionAmbiguityError(new NameInfo("s", "shortandlong")) + }; + + // Exercize system + var result = InvokeBuild(new string[] { "-x" }); + + // Verify outcome + result.Should().BeOfType>(); + var errors = ((NotParsed)result).Errors; + + errors.Should().BeEquivalentTo(expectedResult); + } + private class ValueWithNoSetterOptions { [Value(0, MetaName = "Test", Default = 0)] diff --git a/tests/CommandLine.Tests/Unit/Core/TokenizerTests.cs b/tests/CommandLine.Tests/Unit/Core/TokenizerTests.cs index 61c7f184..32d79b4f 100644 --- a/tests/CommandLine.Tests/Unit/Core/TokenizerTests.cs +++ b/tests/CommandLine.Tests/Unit/Core/TokenizerTests.cs @@ -116,7 +116,7 @@ public void Should_return_error_if_option_format_with_equals_is_not_correct() var result = Tokenizer.Tokenize(args, name => NameLookupResult.OtherOptionFound, token => token); - var tokens = result.SuccessfulMessages(); + var tokens = result.SuccessMessages(); Assert.NotNull(tokens); Assert.Equal(2, tokens.Count()); diff --git a/tests/CommandLine.Tests/Unit/ParserTests.cs b/tests/CommandLine.Tests/Unit/ParserTests.cs index fc2ae150..fe8c4d65 100644 --- a/tests/CommandLine.Tests/Unit/ParserTests.cs +++ b/tests/CommandLine.Tests/Unit/ParserTests.cs @@ -824,5 +824,50 @@ public void Blank_lines_are_inserted_between_verbs() // Teardown } + + [Fact] + public void Parse_default_verb_implicit() + { + var parser = Parser.Default; + parser.ParseArguments(new[] { "-t" }) + .WithNotParsed(errors => throw new InvalidOperationException("Must be parsed.")) + .WithParsed(args => + { + Assert.True(args.TestValueOne); + }); + } + + [Fact] + public void Parse_default_verb_explicit() + { + var parser = Parser.Default; + parser.ParseArguments(new[] { "default1", "-t" }) + .WithNotParsed(errors => throw new InvalidOperationException("Must be parsed.")) + .WithParsed(args => + { + Assert.True(args.TestValueOne); + }); + } + + [Fact] + public void Parse_multiple_default_verbs() + { + var parser = Parser.Default; + parser.ParseArguments(new string[] { }) + .WithNotParsed(errors => Assert.IsType(errors.First())) + .WithParsed(args => throw new InvalidOperationException("Should not be parsed.")); + } + + [Fact] + public void Parse_default_verb_with_empty_name() + { + var parser = Parser.Default; + parser.ParseArguments(new[] { "-t" }) + .WithNotParsed(errors => throw new InvalidOperationException("Must be parsed.")) + .WithParsed(args => + { + Assert.True(args.TestValue); + }); + } } }