diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..c6020604 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,31 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Either fork from this fiddle and paste link here: https://dotnetfiddle.net/mh9CjX + +or + +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Additional context** +Add any other context about the problem here. diff --git a/.gitignore b/.gitignore index 55f900a1..4969d4de 100644 --- a/.gitignore +++ b/.gitignore @@ -37,5 +37,7 @@ artifacts/* *.DotSettings.user # Visual Studio 2015 cache/options directory .vs/ +# Rider +.idea/ [R|r]elease/** diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..eea8ad12 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,174 @@ +# Changelog +All notable changes to this project will be documented in this file. + +CommandLineParser project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [2.9.0-preview2] + +### Added +- Properly assign arguments after a double dash to values, fix #605 by [@robnasby, PR# 610](https://github.com/commandlineparser/commandline/pull/610). + +### Changed +- Drop "Add multi-instance option support". + + +## [2.9.0-preview1] - 2020-7-24 + +### Added +- Add multi-instance option support by [@rmunn and @tydunkel, PR# 594](https://github.com/commandlineparser/commandline/pull/594). +- Fix unparsing FileInfo and DirectoryInfo by[@kapsiR, PR# 627](https://github.com/commandlineparser/commandline/pull/627). +- Move Errors and Value up to the abstract class definition, fixes #543 , #165 by [@johnjaylward, PR# 634](https://github.com/commandlineparser/commandline/pull/634). +- Add support for flags enums, fixes #247, #599 and #582 by [@shaosss, PR# 623](https://github.com/commandlineparser/commandline/pull/623). +- Implement verb aliases, fixes #6, #517 by[@johnjaylward, PR# 636](https://github.com/commandlineparser/commandline/pull/636). +- Add a new method FormatCommandLineArgs to unparse commandline to array of string, Fix #375 and #628 . + - Also, add SplitArgs method to split commandline, fix #665 by[@moh-hassan, PR# 662](https://github.com/commandlineparser/commandline/pull/662) and [commit cccae2db](https://github.com/commandlineparser/commandline/commit/cccae2db749c2ebf25125bfd18e05427be0adbcf). +- Allow single dash as a value, fix #300 and #574 by [@moh-hassan, PR# 669](https://github.com/commandlineparser/commandline/pull/669). + + +## [2.8.0] - 2020-5-1 +## [2.8.0-preview4] - 2020-4-30 +## [2.8.0-preview1] - 2020-3-14 + +### Added +- Added support for async programming for `WithParsed and WithNotParsed` by [@joseangelmt, PR# 390 ](https://github.com/commandlineparser/commandline/pull/390). +- Publish a new symbol packages with source link support for c# and F# (.snupkg) to improved package debugging experience by [@moh-hassan, PR#554](https://github.com/commandlineparser/commandline/pull/554) +- Add default verb support by [@Artentus, PR# 556](https://github.com/commandlineparser/commandline/pull/556). +- Add more details for localized attribute properties by [@EdmondShtogu, PR# 558](https://github.com/commandlineparser/commandline/pull/558) +- Support Default in Group Options and raise error if both SetName and Group are applied on option by [@hadzhiyski, PR# 575](https://github.com/commandlineparser/commandline/pull/575). +- Support mutable types without empty constructor that only does explicit implementation of interfaces by [@pergardebrink, PR#590](https://github.com/commandlineparser/commandline/pull/590). + + +### Changed +- Tests cleanup by [@gsscoder, PR# 560](https://github.com/commandlineparser/commandline/pull/560). +- Upgraded parts of CSharpx from Version 1.6.2-alpha by [@gsscoder, PR# 561](https://github.com/commandlineparser/commandline/pull/561). +- Upgraded RailwaySharp from Version 1.1.0 by [@gsscoder, PR# 562](https://github.com/commandlineparser/commandline/pull/562). +- SkipDefault is being respected by [Usage] Examples by [@kendfrey, PR# 565](https://github.com/commandlineparser/commandline/pull/565). +- Remove useless testing code by [@gsscoder, PR# 568](https://github.com/commandlineparser/commandline/pull/568). +- Remove constraint on T for ParseArguments with factory (required by issue #70) by [@pergardebrink](https://github.com/commandlineparser/commandline/pull/590). +- Update nuget api key by [@ericnewton76](https://github.com/commandlineparser/commandline/commit/2218294550e94bcbc2b76783970541385eaf9c07) + +### Fixed +- Fix #579 Unable to parse TimeSpan given from the FormatCommandLine by [@gsscoder, PR# 580](https://github.com/commandlineparser/commandline/pull/580). +- Fix issue #339 for using custom struct having a constructor with string parameter by [moh-hassan, PR# 588](https://github.com/commandlineparser/commandline/pull/588). +- Fix issue #409 to avoid IOException break in Debug mode in WPF app by [moh-hassan, PR# 589 ](https://github.com/commandlineparser/commandline/pull/589). + + +## [2.7.82] - 2020-1-1 +## [2.7.0] - 2020-1-1 +### Added +- Add option groups feature by [@hadzhiyski](https://github.com/commandlineparser/commandline/pull/552) - When one or more options has group set, at least one of these properties should have set value (they behave as required). +- Add a new overload method for AutoBuild to enable HelpText customization by [@moh-hassan](https://github.com/commandlineparser/commandline/pull/557). +- Improve spacing in HelpText by [@asherber](https://github.com/commandlineparser/commandline/pull/494) by adding a new option in the HelpText. +- Add a new option "SkipDefault" in UnParserSettings by [@moh-hassan](https://github.com/commandlineparser/commandline/pull/550) to add the ability of skipping the options with a default value and fix [#541](https://github.com/commandlineparser/commandline/issues/541). +- Generate a new symbolic nuget Package by [@moh-hassan](https://github.com/commandlineparser/commandline/pull/554) to Improve the debugging of Applications with the NuGet package using [symbols experience](https://github.com/NuGet/Home/wiki/NuGet-Package-Debugging-&-Symbols-Improvements). +- Add Support to [SourceLink](https://github.com/dotnet/sourcelink/blob/master/docs/README.md) in the nuget package [@moh-hassan](https://github.com/commandlineparser/commandline/pull/554). + +### Changed +- Remove the Exception when both CompanyAttribute and CopyRightAttribute are null in the Excuting assembly and set the copyright text to a default value by [@moh-hassan](https://github.com/commandlineparser/commandline/pull/557). +- Change the default copyright to include current year instead of 1 by [@moh-hassan](https://github.com/commandlineparser/commandline/pull/557). +- Enabling c# 8 and Vs2019 image in Appveyor. + +### Fixed +- Fix NullReferenceException when creating a default immutable instance by [@0xced](https://github.com/commandlineparser/commandline/pull/495). +- Fix issue [#496](https://github.com/commandlineparser/commandline/issues/496) - Cryptic error message with immutable option class by[@moh-hassan](https://github.com/commandlineparser/commandline/pull/555). +- Fix UnParserExtensions.FormatCommandLine by [@moh-hassan](https://github.com/commandlineparser/commandline/pull/550) to resolve: + - Fix Quote for Options of type DatTime [#502](https://github.com/commandlineparser/commandline/issues/502) and [#528](https://github.com/commandlineparser/commandline/issues/258). + - Fix Quote for options of type TimeSpan and DateTimeOffset. + - Fix Nullable type [#305](https://github.com/commandlineparser/commandline/issues/305) + +- Fix nuget Licence in nuget package by [@moh-hassan](https://github.com/commandlineparser/commandline/pull/549) and fix issue [#545](https://github.com/commandlineparser/commandline/issues/545). +- Fix PackageIconUrl warning in nuget package by [@moh-hassan](https://github.com/commandlineparser/commandline/pull/551). +- Fix immutable nullException, Improve exception message when immutable type can't be created +- Fix Custom help for verbs issue[#529](https://github.com/commandlineparser/commandline/issues/529) by [@moh-hassan](https://github.com/commandlineparser/commandline/pull/557). +- Fix --help switch throwing exception in F# [#366](https://github.com/commandlineparser/commandline/issues/366) +by [@WallaceKelly](https://github.com/commandlineparser/commandline/pull/493) + +## [2.6.0] - 2019-07-31 +### Added +- Support HelpText localization with ResourceType property by [@tkouba](https://github.com/commandlineparser/commandline/pull/356). +- Add demo for complete localization of command line help using resources by[@tkouba](https://github.com/commandlineparser/commandline/pull/485). +- Localize VerbAttribute by [@moh-hassan](https://github.com/commandlineparser/commandline/pull/473). +- Improve support for multiline help text by [@NeilMacMullen](https://github.com/commandlineparser/commandline/pull/456/). +- Reorder options in auto help text (issue #482) [@b3b00](https://github.com/commandlineparser/commandline/pull/484). +- Add IsHelp() and IsVersion() Extension methods to mange HelpText errors by [@moh-hassan](https://github.com/commandlineparser/commandline/pull/467). + +### Fixed +- Fix issues for HelpText.AutoBuild configuration (issues #224 , # 259) by [@moh-hassan](https://github.com/commandlineparser/commandline/pull/467). +- Test maintainance: add missed tests and removing xUnit1013 warning by [@moh-hassan](https://github.com/commandlineparser/commandline/pull/462). +- Fix issue #104 of nullable enum by [@moh-hassan](https://github.com/commandlineparser/commandline/pull/453). +- Fix issue #418, modify version screen to print a new line at the end by [@moh-hassan](https://github.com/commandlineparser/commandline/pull/443). + + +## [2.5.0] - 2019-04-27 +### Added +- Add support to NET40 and NET45 for both CSharp and FSharp by [@moh-hassan](https://github.com/commandlineparser/commandline/pull/430). + + +### Changed +- Proposed changes for enhancement by [@Wind010](https://github.com/commandlineparser/commandline/pull/314), cover:appveyor.yml, ReflectionExtensions.cs and error.cs. +- Enhance the CSharp demo to run in multi-target net40;net45;netcoreapp2.0;netcoreapp2.1 by [@moh-hassan](https://github.com/commandlineparser/commandline/pull/430). +- Added explicit support for .NET 4.6.1 and .NET Core 2.0 by [@ravenpride](https://github.com/commandlineparser/commandline/pull/400). +- Convert commandline project to multi-target project netstandard2.0;net40;net45;net461. +- Convert commandline Test to multi-target project net461;netcoreapp2.0. + + + +### Fixed +- Fix the null EntryAssembly Exception in unit test of net4x projects: issues #389,#424 by [@moh-hassan](https://github.com/commandlineparser/commandline/pull/430). +- Fix the test case 'Add unit tests for Issue #389 and #392 +- Fix CSC error CS7027: Error signing output with public key from file 'CommandLine.snk' -- Invalid public key in appveyor CI. +- Fix the error CS0234: The type or namespace name 'FSharp' for net40 Framework. +- Fix Mis-typed CommandLine.BaseAttribute.Default results in ArgumentException: Object of type 'X' cannot be converted to type 'Y' (issue #189) by[@Wind010](https://github.com/commandlineparser/commandline/pull/314). + + + + +## [2.4.3] - 2019-01-09 +### Added +- Add support to NetStandard2.0 by [@ViktorHofer](https://github.com/commandlineparser/commandline/pull/307) +- Add strong name signing [@ViktorHofer](https://github.com/commandlineparser/commandline/pull/307) +- Added AutoBuild and AutoVersion properties to control adding of implicit 'help' and 'version' options/verbs by [@Athari](https://github.com/commandlineparser/commandline/pull/256). +- Added simpler C# Quick Start example at readme.md by [@lythix](https://github.com/commandlineparser/commandline/pull/274). +- Add validate feature in Set parameter, and throw exception, and show usage,Issue #283 by[@e673](https://github.com/commandlineparser/commandline/pull/286). + + +### Deprecated +- Drop support for NET40 and NET45 + + +### Removed +- Disable faulty tests in netsatbdard2.0 and enable testing in CI. + + +### Fixed +- Fix grammar error in specification error message by [@DillonAd](https://github.com/commandlineparser/commandline/pull/276). +- Fix HelpText.AutoBuild Usage spacing by[@ElijahReva](https://github.com/commandlineparser/commandline/pull/280). +- Fix type at readme.md file by [@matthewjberger](https://github.com/commandlineparser/commandline/pull/304) +- Fix not showing correct header info, issue #34 by[@tynar](https://github.com/commandlineparser/commandline/pull/312). +- Fix title of assembly renders oddly issue-#197 by [@Yiabiten](https://github.com/commandlineparser/commandline/pull/344). +- Fix nuget apikey by [@ericnewton76](https://github.com/commandlineparser/commandline/pull/386). +- Fix missing fsharp from github release deployment by @ericnewton76. +- Fix to Display Width Tests by [@Oddley](https://github.com/commandlineparser/commandline/pull/278). +- Fixing DisplayWidth for newer Mono by [@Oddley](https://github.com/commandlineparser/commandline/pull/279). + + +## [2.3.0] - 2018-08-13 +### Added +- Properly handle CaseInsensitiveEnumValues flag fixing issue #198 by [@niklaskarl](https://github.com/commandlineparser/commandline/pull/231). + +### Changed +- Updated README examples quick start example for c# and Vb.net to work with the new API by [@loligans](https://github.com/commandlineparser/commandline/pull/218). +- Updated README by [@ericnewton76](https://github.com/commandlineparser/commandline/pull/208). +- Update copyright in unit tests +- Patching appveyor dotnet csproj +- Updates to appveyor to create a build matrix + +### Fixed +- hotfix/issue #213 fsharp dependency by [@ericnewton76](https://github.com/commandlineparser/commandline/pull/215). + + +## [2.2.1] - 2018-01-10 + +## [2.2.0] - 2018-01-07 + +## [1.9.71.2] - 2013-02-27: The starting bascode version diff --git a/CommandLine.sln b/CommandLine.sln index 06356a14..102837f1 100644 --- a/CommandLine.sln +++ b/CommandLine.sln @@ -3,9 +3,11 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 VisualStudioVersion = 15.0.27703.2042 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommandLine", "src\CommandLine\CommandLine.csproj", "{E1BD3C65-49C3-49E7-BABA-C60980CB3F20}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommandLine", "src\CommandLine\CommandLine.csproj", "{E1BD3C65-49C3-49E7-BABA-C60980CB3F20}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommandLine.Tests", "tests\CommandLine.Tests\CommandLine.Tests.csproj", "{0A15C4D2-B3E9-43AB-8155-1B39F7AC8A5E}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommandLine.Tests", "tests\CommandLine.Tests\CommandLine.Tests.csproj", "{0A15C4D2-B3E9-43AB-8155-1B39F7AC8A5E}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{1361E8B1-D0E1-493E-B8C1-7380A7B7C472}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -25,6 +27,9 @@ Global GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {0A15C4D2-B3E9-43AB-8155-1B39F7AC8A5E} = {1361E8B1-D0E1-493E-B8C1-7380A7B7C472} + EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {5B5A476C-82FB-49FB-B592-5224D9005186} EndGlobalSection diff --git a/CommandLine.snk b/CommandLine.snk index 96087a73..6b0b6501 100644 Binary files a/CommandLine.snk and b/CommandLine.snk differ diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 00000000..3db45ca4 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,18 @@ + + + CS1591;CS0219;8002;NU5125 + $(MSBuildThisFileDirectory) + false + + + $(DefineConstants);NETFRAMEWORK + + + + + runtime; build; native; contentfiles; analyzers + all + + + + diff --git a/README.md b/README.md index 7f11109e..79a16fa7 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![Build status](https://ci.appveyor.com/api/projects/status/p61dj8udxs2aocmo/branch/master?svg=true)](https://ci.appveyor.com/project/commandlineparser/commandline/branch/master) [![NuGet](https://img.shields.io/nuget/dt/commandlineparser.svg)](http://nuget.org/packages/commandlineparser) -[![NuGet](https://img.shields.io/nuget/v/commandlineparser.svg)](http://nuget.org/packages/commandlineparser) -[![NuGet](https://img.shields.io/nuget/vpre/commandlineparser.svg)](http://nuget.org/packages/commandlineparser) +[![NuGet](https://img.shields.io/nuget/v/commandlineparser.svg)](https://www.nuget.org/packages/CommandLineParser/) +[![NuGet](https://img.shields.io/nuget/vpre/commandlineparser.svg)](https://www.nuget.org/packages/CommandLineParser/) [![Join the Gitter chat!](https://badges.gitter.im/gsscoder/commandline.svg)](https://gitter.im/gsscoder/commandline?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) # Command Line Parser Library for CLR and NetStandard @@ -14,22 +14,40 @@ The Command Line Parser Library offers CLR applications a clean and concise API C:\Project> NuGet Install CommandLineParser ``` +# Nightly Build + +Nightly version of the CommandLineParser can be downloaded from github [Releases](https://github.com/commandlineparser/commandline/releases). + +The Last new features and fixes, read [changelog](https://github.com/commandlineparser/commandline/blob/master/CHANGELOG.md) + + _NOTE: Mentioned F# Support is provided via ```CommandLineParser.FSharp``` package with FSharp dependencies._ __This library provides _hassle free_ command line parsing with a constantly updated API since 2005.__ # At a glance: -- Compatible with __.NET Framework 4.0+__, __Mono 2.1+ Profile__, and __.NET Core__ +- Compatible with __.NET Framework 4.0+__, __Mono 2.1+ Profile__, __.NET Standard__ and __.NET Core__ - Doesn't depend on other packages (No dependencies beyond standard base libraries) -- One line parsing using default singleton: `CommandLine.Parser.Default.ParseArguments(...)`. +- One line parsing using default singleton: `CommandLine.Parser.Default.ParseArguments(...)` and three overload methods. - Automatic or one line help screen generator: `HelpText.AutoBuild(...)`. -- Supports `--help`, `--version`, `version` and `help [verb]` by default. +- Supports `--help`, `--version`, `version` and `help [verb]` by default with customization. - Map to sequences (via `IEnumerable` and similar) and scalar types, including Enums and `Nullable`. -- You can also map to every type with a constructor that accepts a string (like `System.Uri`). -- Define [verb commands](https://github.com/commandlineparser/commandline/wiki#verbs) similar to `git commit -a`. +- You can also map to every type with a constructor that accepts a string (like `System.Uri`) for reference and value types. +- Verbs can be array of types collected from Plugins or IoC container. +- Define [verb commands](https://github.com/commandlineparser/commandline/wiki/Verbs) similar to `git commit -a`. +- Support default verb. +- Support Mutable and Immutable types. +- Support HelpText localization. +- Support ordering of options in HelpText. +- Support [Mutually Exclusive Options](https://github.com/commandlineparser/commandline/wiki/Mutually-Exclusive-Options) and [Option groups](https://github.com/commandlineparser/commandline/wiki/Option-Groups). +- Support named and value options. +- Support Asynchronous programming with async and await. - Unparsing support: `CommandLine.Parser.Default.FormatCommandLine(T options)`. - CommandLineParser.FSharp package is F#-friendly with support for `option<'a>`, see [demo](https://github.com/commandlineparser/commandline/blob/master/demo/fsharp-demo.fsx). _NOTE: This is a separate NuGet package._ +- Include wiki documentation with lot of examples ready to run online. +- Support Source Link and symbolic nuget package snupkg. +- Tested in Windows, Linux Ubuntu 18.04 and Mac OS. - Most of features applies with a [CoC](http://en.wikipedia.org/wiki/Convention_over_configuration) philosophy. - C# demo: source [here](https://github.com/commandlineparser/commandline/tree/master/demo/ReadText.Demo). @@ -48,7 +66,7 @@ You can utilize the parser library in several ways: C# Quick Start: -```csharp +```cs using System; using CommandLine; @@ -83,9 +101,13 @@ namespace QuickStart } ``` -C# Examples: +## C# Examples: + +
+ Click to expand! + +```cs -```csharp class Options { [Option('r', "read", Required = true, HelpText = "Input files to be processed.")] @@ -109,14 +131,31 @@ class Options static void Main(string[] args) { CommandLine.Parser.Default.ParseArguments(args) - .WithParsed(opts => RunOptionsAndReturnExitCode(opts)) - .WithNotParsed((errs) => HandleParseError(errs)); + .WithParsed(RunOptions) + .WithNotParsed(HandleParseError); +} +static void RunOptions(Options opts) +{ + //handle options } +static void HandleParseError(IEnumerable errs) +{ + //handle errors +} + ``` -F# Examples: +
+ +Demo to show IEnumerable options and other usage: [Online Demo](https://dotnetfiddle.net/wrcAxr) + +## F# Examples: + +
+ Click to expand! ```fsharp + type options = { [] files : seq; [] verbose : bool; @@ -130,10 +169,15 @@ let main argv = | :? Parsed as parsed -> run parsed.Value | :? NotParsed as notParsed -> fail notParsed.Errors ``` +
+ +## VB.NET Example: -VB.NET: +
+ Click to expand! + +```vb -```VB.NET Class Options @@ -159,14 +203,19 @@ Sub Main(ByVal args As String()) .WithNotParsed(Function(errs As IEnumerable(Of [Error])) 1) End Sub ``` +
-### For verbs: +## For verbs: 1. Create separate option classes for each verb. An options base class is supported. 2. Call ParseArguments with all the verb attribute decorated options classes. 3. Use MapResult to direct program flow to the verb that was parsed. -C# example: +### C# example: + + +
+ Click to expand! ```csharp [Verb("add", HelpText = "Add file contents to the index.")] @@ -191,10 +240,15 @@ int Main(string[] args) { errs => 1); } ``` +
-VB.NET example: +### VB.NET example: -```VB.NET + +
+ Click to expand! + +```vb Public Class AddOptions 'Normal options here @@ -218,10 +272,14 @@ Function Main(ByVal args As String()) As Integer ) End Function ``` +
-F# Example: +### F# Example: -```fsharp +
+ Click to expand! + +```fs open CommandLine [] @@ -248,6 +306,11 @@ let main args = | :? CloneOptions as opts -> RunCloneAndReturnExitCode opts | :? CommandLine.NotParsed -> 1 ``` +
+ +# Release History + +See the [changelog](CHANGELOG.md) # Contributors First off, _Thank you!_ All contributions are welcome. @@ -263,6 +326,7 @@ __And most importantly, please target the ```develop``` branch in your pull requ - Dan Nemec (@nemec) - Eric Newton (@ericnewton76) - Kevin Moore (@gimmemoore) +- Moh-Hassan (@moh-hassan) - Steven Evans - Thomas Démoulins (@Thilas) @@ -284,3 +348,4 @@ __And most importantly, please target the ```develop``` branch in your pull requ - GitHub: [ericnewton76](https://github.com/ericnewton76) - Blog: - Twitter: [enorl76](http://twitter.com/enorl76) +- Moh-Hassan diff --git a/appveyor.yml b/appveyor.yml index a3b73034..d11d6abf 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,5 +1,8 @@ #version should be only changed with RELEASE eminent, see RELEASE.md -version: 2.4.{build} + +version: 2.9.1-ci-{build} + +image: Visual Studio 2019 clone_depth: 1 pull_requests: @@ -32,6 +35,8 @@ after_test: artifacts: - path: 'src/CommandLine/bin/Release/*.nupkg' name: NuGetPackages +- path: 'src/CommandLine/bin/Release/*.snupkg' + name: symbol on_failure: - cmd: | @@ -39,18 +44,9 @@ on_failure: appveyor PushArtifact .\files.lst -DeploymentName "Failed Build File Listing" deploy: -- provider: GitHub - auth_token: - secure: hVyVwHl0JiVq0VxXB4VMRWbUtrGclIzadfnWFcWCQBLvbgMLahLBnWlwGglT63pZ - artifact: 'NuGetPackages' - prerelease: false - force_update: true #fsharp package runs as separate build job, so have to force_update to add fsharp.nuget added - on: - APPVEYOR_REPO_TAG: true - - provider: NuGet api_key: - secure: Ab4T/48EyIJhVrqkfKdUxmHUtseEVuXuyrGACxZ0KN35rb/BzABlBM2YjZojicvT - artifact: 'NuGetPackages' + secure: llMIgYMuLHh9thyKMEAmkWraTaA9Zvcm1F8/yRwm0HCiPIt/ehR/GI4kJKyMTPyf + artifact: /.*(\.|\.s)nupkg/ 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/demo/ReadText.Demo/ReadText.Demo.csproj b/demo/ReadText.Demo/ReadText.Demo.csproj index f07c8801..71f16965 100644 --- a/demo/ReadText.Demo/ReadText.Demo.csproj +++ b/demo/ReadText.Demo/ReadText.Demo.csproj @@ -1,52 +1,10 @@ - - + - Debug - AnyCPU - 12.0.0 - 2.0 - {F9D3B288-1A73-4C91-8ED7-11ED1704B817} Exe - ReadText.Demo - ReadText.Demo + net40;net45;net461;netcoreapp2.1;netcoreapp2.0 +false - - true - full - false - bin\Debug - DEBUG; - prompt - 4 - true - - - full - true - bin\Release - prompt - 4 - true - - - - packages\CommandLineParser.2.1.1-beta\lib\net40\CommandLine.dll - - - - - - - Properties\SharedAssemblyInfo.cs - - - - - - - Designer - + - \ No newline at end of file diff --git a/demo/ReadText.Demo/ReadText.Demo.sln b/demo/ReadText.Demo/ReadText.Demo.sln index 1cac367d..cafe0089 100644 --- a/demo/ReadText.Demo/ReadText.Demo.sln +++ b/demo/ReadText.Demo/ReadText.Demo.sln @@ -1,9 +1,11 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 2013 -VisualStudioVersion = 12.0.31101.0 +# Visual Studio 15 +VisualStudioVersion = 15.0.28307.106 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReadText.Demo", "ReadText.Demo.csproj", "{F9D3B288-1A73-4C91-8ED7-11ED1704B817}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReadText.Demo", "ReadText.Demo.csproj", "{F9D3B288-1A73-4C91-8ED7-11ED1704B817}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommandLine", "..\..\src\CommandLine\CommandLine.csproj", "{A03AADAC-F7E5-44A6-8BCC-492B1697CCC9}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -15,8 +17,15 @@ Global {F9D3B288-1A73-4C91-8ED7-11ED1704B817}.Debug|Any CPU.Build.0 = Debug|Any CPU {F9D3B288-1A73-4C91-8ED7-11ED1704B817}.Release|Any CPU.ActiveCfg = Release|Any CPU {F9D3B288-1A73-4C91-8ED7-11ED1704B817}.Release|Any CPU.Build.0 = Release|Any CPU + {A03AADAC-F7E5-44A6-8BCC-492B1697CCC9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A03AADAC-F7E5-44A6-8BCC-492B1697CCC9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A03AADAC-F7E5-44A6-8BCC-492B1697CCC9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A03AADAC-F7E5-44A6-8BCC-492B1697CCC9}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {FF14CDF0-EF51-448B-918C-47CD369568DF} + EndGlobalSection EndGlobal diff --git a/demo/ReadText.LocalizedDemo/.cr/personal/Navigation/RecentFilesHistory.xml b/demo/ReadText.LocalizedDemo/.cr/personal/Navigation/RecentFilesHistory.xml new file mode 100644 index 00000000..7b2b2ccb --- /dev/null +++ b/demo/ReadText.LocalizedDemo/.cr/personal/Navigation/RecentFilesHistory.xml @@ -0,0 +1,16 @@ + + + + + + LocalizableAttributeProperty.cs + d:\work\arci\commandline\src\commandline\infrastructure\localizableattributeproperty.cs + + Infrastructure + + d:\WORK\ARCI\commandline\src\CommandLine\CommandLine.csproj + CommandLine + + + + \ No newline at end of file diff --git a/demo/ReadText.LocalizedDemo/LocalizableSentenceBuilder.cs b/demo/ReadText.LocalizedDemo/LocalizableSentenceBuilder.cs new file mode 100644 index 00000000..bf2b7c56 --- /dev/null +++ b/demo/ReadText.LocalizedDemo/LocalizableSentenceBuilder.cs @@ -0,0 +1,122 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using CommandLine; +using CommandLine.Text; + +namespace ReadText.LocalizedDemo +{ + public class LocalizableSentenceBuilder : SentenceBuilder + { + public override Func RequiredWord + { + get { return () => Properties.Resources.SentenceRequiredWord; } + } + + public override Func ErrorsHeadingText + { + // Cannot be pluralized + get { return () => Properties.Resources.SentenceErrorsHeadingText; } + } + + public override Func UsageHeadingText + { + get { return () => Properties.Resources.SentenceUsageHeadingText; } + } + + public override Func HelpCommandText + { + get + { + return isOption => isOption + ? Properties.Resources.SentenceHelpCommandTextOption + : Properties.Resources.SentenceHelpCommandTextVerb; + } + } + + public override Func VersionCommandText + { + get { return _ => Properties.Resources.SentenceVersionCommandText; } + } + + public override Func FormatError + { + get + { + return error => + { + switch (error.Tag) + { + case ErrorType.BadFormatTokenError: + return String.Format(Properties.Resources.SentenceBadFormatTokenError, ((BadFormatTokenError)error).Token); + case ErrorType.MissingValueOptionError: + return String.Format(Properties.Resources.SentenceMissingValueOptionError, ((MissingValueOptionError)error).NameInfo.NameText); + case ErrorType.UnknownOptionError: + return String.Format(Properties.Resources.SentenceUnknownOptionError, ((UnknownOptionError)error).Token); + case ErrorType.MissingRequiredOptionError: + var errMisssing = ((MissingRequiredOptionError)error); + return errMisssing.NameInfo.Equals(NameInfo.EmptyName) + ? Properties.Resources.SentenceMissingRequiredOptionError + : String.Format(Properties.Resources.SentenceMissingRequiredOptionError, errMisssing.NameInfo.NameText); + case ErrorType.BadFormatConversionError: + var badFormat = ((BadFormatConversionError)error); + return badFormat.NameInfo.Equals(NameInfo.EmptyName) + ? Properties.Resources.SentenceBadFormatConversionErrorValue + : String.Format(Properties.Resources.SentenceBadFormatConversionErrorOption, badFormat.NameInfo.NameText); + case ErrorType.SequenceOutOfRangeError: + var seqOutRange = ((SequenceOutOfRangeError)error); + return seqOutRange.NameInfo.Equals(NameInfo.EmptyName) + ? Properties.Resources.SentenceSequenceOutOfRangeErrorValue + : String.Format(Properties.Resources.SentenceSequenceOutOfRangeErrorOption, + seqOutRange.NameInfo.NameText); + case ErrorType.BadVerbSelectedError: + return String.Format(Properties.Resources.SentenceBadVerbSelectedError, ((BadVerbSelectedError)error).Token); + case ErrorType.NoVerbSelectedError: + return Properties.Resources.SentenceNoVerbSelectedError; + case ErrorType.RepeatedOptionError: + return String.Format(Properties.Resources.SentenceRepeatedOptionError, ((RepeatedOptionError)error).NameInfo.NameText); + case ErrorType.SetValueExceptionError: + var setValueError = (SetValueExceptionError)error; + return String.Format(Properties.Resources.SentenceSetValueExceptionError, setValueError.NameInfo.NameText, setValueError.Exception.Message); + } + throw new InvalidOperationException(); + }; + } + } + + public override Func, string> FormatMutuallyExclusiveSetErrors + { + get + { + return errors => + { + var bySet = from e in errors + group e by e.SetName into g + select new { SetName = g.Key, Errors = g.ToList() }; + + var msgs = bySet.Select( + set => + { + var names = String.Join( + String.Empty, + (from e in set.Errors select String.Format("'{0}', ", e.NameInfo.NameText)).ToArray()); + var namesCount = set.Errors.Count(); + + var incompat = String.Join( + String.Empty, + (from x in + (from s in bySet where !s.SetName.Equals(set.SetName) from e in s.Errors select e) + .Distinct() + select String.Format("'{0}', ", x.NameInfo.NameText)).ToArray()); + //TODO: Pluralize by namesCount + return + String.Format(Properties.Resources.SentenceMutuallyExclusiveSetErrors, + names.Substring(0, names.Length - 2), incompat.Substring(0, incompat.Length - 2)); + }).ToArray(); + return string.Join(Environment.NewLine, msgs); + }; + } + } + } +} diff --git a/demo/ReadText.LocalizedDemo/Options.cs b/demo/ReadText.LocalizedDemo/Options.cs new file mode 100644 index 00000000..6ab1e3ee --- /dev/null +++ b/demo/ReadText.LocalizedDemo/Options.cs @@ -0,0 +1,69 @@ +using CommandLine; +using CommandLine.Text; +using System.Collections.Generic; + +namespace ReadText.LocalizedDemo +{ + interface IOptions + { + [Option('n', "lines", + Default = 5U, + SetName = "bylines", + HelpText = "HelpTextLines", + ResourceType = typeof(Properties.Resources))] + uint? Lines { get; set; } + + [Option('c', "bytes", + SetName = "bybytes", + HelpText = "HelpTextBytes", + ResourceType = typeof(Properties.Resources))] + uint? Bytes { get; set; } + + [Option('q', "quiet", + HelpText = "HelpTextQuiet", + ResourceType = typeof(Properties.Resources))] + bool Quiet { get; set; } + + [Value(0, MetaName = "input file", + HelpText = "HelpTextFileName", + Required = true, + ResourceType = typeof(Properties.Resources))] + string FileName { get; set; } + } + + [Verb("head", HelpText = "HelpTextVerbHead", ResourceType = typeof(Properties.Resources))] + class HeadOptions : IOptions + { + public uint? Lines { get; set; } + + public uint? Bytes { get; set; } + + public bool Quiet { get; set; } + + public string FileName { get; set; } + + [Usage(ApplicationAlias = "ReadText.LocalizedDemo.exe")] + public static IEnumerable Examples + { + get + { + yield return new Example(Properties.Resources.ExamplesNormalScenario, new HeadOptions { FileName = "file.bin"}); + yield return new Example(Properties.Resources.ExamplesSpecifyBytes, new HeadOptions { FileName = "file.bin", Bytes=100 }); + yield return new Example(Properties.Resources.ExamplesSuppressSummary, UnParserSettings.WithGroupSwitchesOnly(), new HeadOptions { FileName = "file.bin", Quiet = true }); + yield return new Example(Properties.Resources.ExamplesReadMoreLines, new[] { UnParserSettings.WithGroupSwitchesOnly(), UnParserSettings.WithUseEqualTokenOnly() }, new HeadOptions { FileName = "file.bin", Lines = 10 }); + } + } + } + + [Verb("tail", HelpText = "HelpTextVerbTail", ResourceType = typeof(Properties.Resources))] + class TailOptions : IOptions + { + public uint? Lines { get; set; } + + public uint? Bytes { get; set; } + + public bool Quiet { get; set; } + + public string FileName { get; set; } + } +} diff --git a/demo/ReadText.LocalizedDemo/Program.cs b/demo/ReadText.LocalizedDemo/Program.cs new file mode 100644 index 00000000..defa6412 --- /dev/null +++ b/demo/ReadText.LocalizedDemo/Program.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using CommandLine; +using CommandLine.Text; + +namespace ReadText.LocalizedDemo +{ + class Program + { + public static int Main(string[] args) + { + // Set sentence builder to localizable + SentenceBuilder.Factory = () => new LocalizableSentenceBuilder(); + + Func reader = opts => + { + var fromTop = opts.GetType() == typeof(HeadOptions); + return opts.Lines.HasValue + ? ReadLines(opts.FileName, fromTop, (int)opts.Lines) + : ReadBytes(opts.FileName, fromTop, (int)opts.Bytes); + }; + Func header = opts => + { + if (opts.Quiet) + { + return string.Empty; + } + var fromTop = opts.GetType() == typeof(HeadOptions); + var builder = new StringBuilder(Properties.Resources.Reading); + builder = opts.Lines.HasValue + ? builder.Append(opts.Lines).Append(Properties.Resources.Lines) + : builder.Append(opts.Bytes).Append(Properties.Resources.Bytes); + builder = fromTop ? builder.Append(Properties.Resources.FromTop) : builder.Append(Properties.Resources.FromBottom); + return builder.ToString(); + }; + Action printIfNotEmpty = text => + { + if (text.Length == 0) { return; } + Console.WriteLine(text); + }; + + var result = Parser.Default.ParseArguments(args); + var texts = result + .MapResult( + (HeadOptions opts) => Tuple.Create(header(opts), reader(opts)), + (TailOptions opts) => Tuple.Create(header(opts), reader(opts)), + _ => MakeError()); + + printIfNotEmpty(texts.Item1); + printIfNotEmpty(texts.Item2); + + return texts.Equals(MakeError()) ? 1 : 0; + } + + private static string ReadLines(string fileName, bool fromTop, int count) + { + var lines = File.ReadAllLines(fileName); + if (fromTop) + { + return string.Join(Environment.NewLine, lines.Take(count)); + } + return string.Join(Environment.NewLine, lines.Reverse().Take(count)); + } + + private static string ReadBytes(string fileName, bool fromTop, int count) + { + var bytes = File.ReadAllBytes(fileName); + if (fromTop) + { + return Encoding.UTF8.GetString(bytes, 0, count); + } + return Encoding.UTF8.GetString(bytes, bytes.Length - count, count); + } + + private static Tuple MakeError() + { + return Tuple.Create("\0", "\0"); + } + } +} diff --git a/demo/ReadText.LocalizedDemo/Properties/AssemblyInfo.cs b/demo/ReadText.LocalizedDemo/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..eaf1b66f --- /dev/null +++ b/demo/ReadText.LocalizedDemo/Properties/AssemblyInfo.cs @@ -0,0 +1,11 @@ +using System.Reflection; +using System.Runtime.CompilerServices; + +[assembly: AssemblyTitle("ReadText.Demo")] +[assembly: AssemblyDescription("ReadText.Demo for Command Line Parser Library")] +[assembly: AssemblyTrademark("")] +#if DEBUG +[assembly: AssemblyConfiguration("Debug")] +#else +[assembly: AssemblyConfiguration("Release")] +#endif diff --git a/demo/ReadText.LocalizedDemo/Properties/Resources.Designer.cs b/demo/ReadText.LocalizedDemo/Properties/Resources.Designer.cs new file mode 100644 index 00000000..f2ca4b31 --- /dev/null +++ b/demo/ReadText.LocalizedDemo/Properties/Resources.Designer.cs @@ -0,0 +1,378 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace ReadText.LocalizedDemo.Properties { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + public class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + public static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("ReadText.LocalizedDemo.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + public static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to bytes. + /// + public static string Bytes { + get { + return ResourceManager.GetString("Bytes", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to normal scenario. + /// + public static string ExamplesNormalScenario { + get { + return ResourceManager.GetString("ExamplesNormalScenario", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to read more lines. + /// + public static string ExamplesReadMoreLines { + get { + return ResourceManager.GetString("ExamplesReadMoreLines", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to specify bytes. + /// + public static string ExamplesSpecifyBytes { + get { + return ResourceManager.GetString("ExamplesSpecifyBytes", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to suppress summary. + /// + public static string ExamplesSuppressSummary { + get { + return ResourceManager.GetString("ExamplesSuppressSummary", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to from bottom:. + /// + public static string FromBottom { + get { + return ResourceManager.GetString("FromBottom", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to from top:. + /// + public static string FromTop { + get { + return ResourceManager.GetString("FromTop", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Bytes to be printed from the beginning or end of the file.. + /// + public static string HelpTextBytes { + get { + return ResourceManager.GetString("HelpTextBytes", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Input file to be processed.. + /// + public static string HelpTextFileName { + get { + return ResourceManager.GetString("HelpTextFileName", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Lines to be printed from the beginning or end of the file.. + /// + public static string HelpTextLines { + get { + return ResourceManager.GetString("HelpTextLines", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Suppresses summary messages.. + /// + public static string HelpTextQuiet { + get { + return ResourceManager.GetString("HelpTextQuiet", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Displays first lines of a file.. + /// + public static string HelpTextVerbHead { + get { + return ResourceManager.GetString("HelpTextVerbHead", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Displays last lines of a file.. + /// + public static string HelpTextVerbTail { + get { + return ResourceManager.GetString("HelpTextVerbTail", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to lines. + /// + public static string Lines { + get { + return ResourceManager.GetString("Lines", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Reading . + /// + public static string Reading { + get { + return ResourceManager.GetString("Reading", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Možnost '{0}' je definována ve špatném formátu.. + /// + public static string SentenceBadFormatConversionErrorOption { + get { + return ResourceManager.GetString("SentenceBadFormatConversionErrorOption", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A value not bound to option name is defined with a bad format.. + /// + public static string SentenceBadFormatConversionErrorValue { + get { + return ResourceManager.GetString("SentenceBadFormatConversionErrorValue", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Token '{0}' is not recognized.. + /// + public static string SentenceBadFormatTokenError { + get { + return ResourceManager.GetString("SentenceBadFormatTokenError", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Verb '{0}' is not recognized.. + /// + public static string SentenceBadVerbSelectedError { + get { + return ResourceManager.GetString("SentenceBadVerbSelectedError", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to ERROR(S):. + /// + public static string SentenceErrorsHeadingText { + get { + return ResourceManager.GetString("SentenceErrorsHeadingText", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Display this help screen.. + /// + public static string SentenceHelpCommandTextOption { + get { + return ResourceManager.GetString("SentenceHelpCommandTextOption", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Display more information on a specific command.. + /// + public static string SentenceHelpCommandTextVerb { + get { + return ResourceManager.GetString("SentenceHelpCommandTextVerb", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Required option '{0}' is missing.. + /// + public static string SentenceMissingRequiredOptionError { + get { + return ResourceManager.GetString("SentenceMissingRequiredOptionError", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A required value not bound to option name is missing.. + /// + public static string SentenceMissingRequiredValueError { + get { + return ResourceManager.GetString("SentenceMissingRequiredValueError", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Option '{0}' has no value.. + /// + public static string SentenceMissingValueOptionError { + get { + return ResourceManager.GetString("SentenceMissingValueOptionError", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Options: {0} are not compatible with {1}.. + /// + public static string SentenceMutuallyExclusiveSetErrors { + get { + return ResourceManager.GetString("SentenceMutuallyExclusiveSetErrors", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No verb selected.. + /// + public static string SentenceNoVerbSelectedError { + get { + return ResourceManager.GetString("SentenceNoVerbSelectedError", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Option '{0}' is defined multiple times.. + /// + public static string SentenceRepeatedOptionError { + get { + return ResourceManager.GetString("SentenceRepeatedOptionError", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Required.. + /// + public static string SentenceRequiredWord { + get { + return ResourceManager.GetString("SentenceRequiredWord", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A sequence option '{0}' is defined with fewer or more items than required.. + /// + public static string SentenceSequenceOutOfRangeErrorOption { + get { + return ResourceManager.GetString("SentenceSequenceOutOfRangeErrorOption", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A sequence value not bound to option name is defined with fewer items than required.. + /// + public static string SentenceSequenceOutOfRangeErrorValue { + get { + return ResourceManager.GetString("SentenceSequenceOutOfRangeErrorValue", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Error setting value to option '{0}': {1}. + /// + public static string SentenceSetValueExceptionError { + get { + return ResourceManager.GetString("SentenceSetValueExceptionError", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Option '{0}' is unknown.. + /// + public static string SentenceUnknownOptionError { + get { + return ResourceManager.GetString("SentenceUnknownOptionError", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to USAGE:. + /// + public static string SentenceUsageHeadingText { + get { + return ResourceManager.GetString("SentenceUsageHeadingText", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Display version information.. + /// + public static string SentenceVersionCommandText { + get { + return ResourceManager.GetString("SentenceVersionCommandText", resourceCulture); + } + } + } +} diff --git a/demo/ReadText.LocalizedDemo/Properties/Resources.cs.resx b/demo/ReadText.LocalizedDemo/Properties/Resources.cs.resx new file mode 100644 index 00000000..cae11e19 --- /dev/null +++ b/demo/ReadText.LocalizedDemo/Properties/Resources.cs.resx @@ -0,0 +1,225 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Čtení + + + řádků + + + bytů + + + od začátku: + + + ok konce: + + + Počet řádek zobrazených od začátku nebo konce souboru. + + + Počet bytů zobrazených od začátku nebo konce souboru. + + + Potlačit sumář. + + + Jméno vstupního souboru. + + + Zobrazit první řádky souboru. + + + normální scénář + + + specifikace počtu byte + + + potlačit sumář + + + přečíst více řádek + + + Zobrazit poslední řádky souboru. + + + CHYBY: + + + Povinné. + + + POUŽITÍ: + + + Zobrazit tuto nápovědu. + + + Zobrazit podrobnou nápovědu pro příkaz. + + + Zobrazit informaci o verzi. + + + Token '{0}' nebyl rozpoznán. + + + Přepínač '{0}' nemá hodnotu. + + + Neznámý přepínač '{0}' + + + Přepínač '{0}' je definován ve špatném formátu. + + + Hodnota nevázaná na přepínač je definována ve špatném formátu. + + + Příkaz '{0}' nebyl rozpoznán. + + + Chybí povinný přepínač '{0}'. + + + Chybí požadovaný přepínač, který není vázán na název možnosti. + + + Přepínače: {0} nejsou kompatibilní s {1}. + + + Nebyl vybrán příkaz. + + + Přepínač '{0}' je definován vícenásobně. + + + Přepínač sekvence '{0}' je definován méně nebo vícekrát než je povoleno. + + + Hodnota přepínače je definována méněkrát než je povoleno. + + + Chyba při nastavení hodnoty přepínače '{0}': {1} + + \ No newline at end of file diff --git a/demo/ReadText.LocalizedDemo/Properties/Resources.resx b/demo/ReadText.LocalizedDemo/Properties/Resources.resx new file mode 100644 index 00000000..b002fc43 --- /dev/null +++ b/demo/ReadText.LocalizedDemo/Properties/Resources.resx @@ -0,0 +1,225 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Reading + + + lines + + + bytes + + + from top: + + + from bottom: + + + Lines to be printed from the beginning or end of the file. + + + Bytes to be printed from the beginning or end of the file. + + + Suppresses summary messages. + + + Input file to be processed. + + + Displays first lines of a file. + + + normal scenario + + + specify bytes + + + suppress summary + + + read more lines + + + Displays last lines of a file. + + + Možnost '{0}' je definována ve špatném formátu. + + + A value not bound to option name is defined with a bad format. + + + Token '{0}' is not recognized. + + + Verb '{0}' is not recognized. + + + ERROR(S): + + + Display this help screen. + + + Display more information on a specific command. + + + Required option '{0}' is missing. + + + A required value not bound to option name is missing. + + + Option '{0}' has no value. + + + Options: {0} are not compatible with {1}. + + + No verb selected. + + + Option '{0}' is defined multiple times. + + + Required. + + + A sequence option '{0}' is defined with fewer or more items than required. + + + A sequence value not bound to option name is defined with fewer items than required. + + + Error setting value to option '{0}': {1} + + + Option '{0}' is unknown. + + + USAGE: + + + Display version information. + + \ No newline at end of file diff --git a/demo/ReadText.LocalizedDemo/Properties/launchSettings.json b/demo/ReadText.LocalizedDemo/Properties/launchSettings.json new file mode 100644 index 00000000..a5627a1b --- /dev/null +++ b/demo/ReadText.LocalizedDemo/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "ReadText.LocalizedDemo": { + "commandName": "Project", + "commandLineArgs": "head" + } + } +} \ No newline at end of file diff --git a/demo/ReadText.LocalizedDemo/ReadText.LocalizedDemo.csproj b/demo/ReadText.LocalizedDemo/ReadText.LocalizedDemo.csproj new file mode 100644 index 00000000..57251e03 --- /dev/null +++ b/demo/ReadText.LocalizedDemo/ReadText.LocalizedDemo.csproj @@ -0,0 +1,23 @@ + + + Exe + net40;net45;net461;netcoreapp2.1;netcoreapp2.0 +false + + + + + + + True + True + Resources.resx + + + + + PublicResXFileCodeGenerator + Resources.Designer.cs + + + \ No newline at end of file diff --git a/demo/ReadText.LocalizedDemo/ReadText.LocalizedDemo.sln b/demo/ReadText.LocalizedDemo/ReadText.LocalizedDemo.sln new file mode 100644 index 00000000..e769b7da --- /dev/null +++ b/demo/ReadText.LocalizedDemo/ReadText.LocalizedDemo.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.28307.106 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReadText.LocalizedDemo", "ReadText.LocalizedDemo.csproj", "{F9D3B288-1A73-4C91-8ED7-11ED1704B817}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommandLine", "..\..\src\CommandLine\CommandLine.csproj", "{A03AADAC-F7E5-44A6-8BCC-492B1697CCC9}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {F9D3B288-1A73-4C91-8ED7-11ED1704B817}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F9D3B288-1A73-4C91-8ED7-11ED1704B817}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F9D3B288-1A73-4C91-8ED7-11ED1704B817}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F9D3B288-1A73-4C91-8ED7-11ED1704B817}.Release|Any CPU.Build.0 = Release|Any CPU + {A03AADAC-F7E5-44A6-8BCC-492B1697CCC9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A03AADAC-F7E5-44A6-8BCC-492B1697CCC9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A03AADAC-F7E5-44A6-8BCC-492B1697CCC9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A03AADAC-F7E5-44A6-8BCC-492B1697CCC9}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {FF14CDF0-EF51-448B-918C-47CD369568DF} + EndGlobalSection +EndGlobal diff --git a/demo/ReadText.LocalizedDemo/packages.config b/demo/ReadText.LocalizedDemo/packages.config new file mode 100644 index 00000000..d34c1336 --- /dev/null +++ b/demo/ReadText.LocalizedDemo/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/demo/fsharp-demo.fsx b/demo/fsharp-demo.fsx index 81b1640d..8aff84e3 100644 --- a/demo/fsharp-demo.fsx +++ b/demo/fsharp-demo.fsx @@ -22,9 +22,11 @@ let formatLong o = let formatInput (o : options) = sprintf "--stringvalue: %s\n-i: %A\n-x: %b\nvalue: %s\n" o.stringValue o.intSequence o.boolValue (formatLong o.longValue) -let inline (|Success|Fail|) (result : ParserResult<'a>) = +let inline (|Success|Help|Version|Fail|) (result : ParserResult<'a>) = match result with | :? Parsed<'a> as parsed -> Success(parsed.Value) + | :? NotParsed<'a> as notParsed when notParsed.Errors.IsHelp() -> Help + | :? NotParsed<'a> as notParsed when notParsed.Errors.IsVersion() -> Version | :? NotParsed<'a> as notParsed -> Fail(notParsed.Errors) | _ -> failwith "invalid parser result" @@ -34,3 +36,4 @@ let result = Parser.Default.ParseArguments(args) match result with | Success(opts) -> printf "%s" (formatInput opts) | Fail(errs) -> printf "Invalid: %A, Errors: %u\n" args (Seq.length errs) + | Help | Version -> () diff --git a/src/CommandLine/BaseAttribute.cs b/src/CommandLine/BaseAttribute.cs index 255dc0d1..be0a3826 100644 --- a/src/CommandLine/BaseAttribute.cs +++ b/src/CommandLine/BaseAttribute.cs @@ -12,8 +12,9 @@ public abstract class BaseAttribute : Attribute private int min; private int max; private object @default; - private string helpText; + private Infrastructure.LocalizableAttributeProperty helpText; private string metaValue; + private Type resourceType; /// /// Initializes a new instance of the class. @@ -22,8 +23,9 @@ protected internal BaseAttribute() { min = -1; max = -1; - helpText = string.Empty; + helpText = new Infrastructure.LocalizableAttributeProperty(nameof(HelpText)); metaValue = string.Empty; + resourceType = null; } /// @@ -90,11 +92,8 @@ public object Default /// public string HelpText { - get { return helpText; } - set - { - helpText = value ?? throw new ArgumentNullException("value"); - } + get => helpText.Value??string.Empty; + set => helpText.Value = value ?? throw new ArgumentNullException("value"); } /// @@ -105,7 +104,12 @@ public string MetaValue get { return metaValue; } set { - metaValue = value ?? throw new ArgumentNullException("value"); + if (value == null) + { + throw new ArgumentNullException("value"); + } + + metaValue = value; } } @@ -117,5 +121,18 @@ public bool Hidden get; set; } + + /// + /// Gets or sets the that contains the resources for . + /// + public Type ResourceType + { + get { return resourceType; } + set + { + resourceType = + helpText.ResourceType = value; + } + } } } diff --git a/src/CommandLine/CastExtensions.cs b/src/CommandLine/CastExtensions.cs new file mode 100644 index 00000000..fa34928c --- /dev/null +++ b/src/CommandLine/CastExtensions.cs @@ -0,0 +1,100 @@ +using System; +using System.Linq; +using System.Reflection; + +namespace CommandLine +{ + internal static class CastExtensions + { + private const string ImplicitCastMethodName = "op_Implicit"; + private const string ExplicitCastMethodName = "op_Explicit"; + + public static bool CanCast(this Type baseType) + { + return baseType.CanImplicitCast() || baseType.CanExplicitCast(); + } + + public static bool CanCast(this object obj) + { + var objType = obj.GetType(); + return objType.CanCast(); + } + + public static T Cast(this object obj) + { + try + { + return (T) obj; + } + catch (InvalidCastException) + { + if (obj.CanImplicitCast()) + return obj.ImplicitCast(); + if (obj.CanExplicitCast()) + return obj.ExplicitCast(); + else + throw; + } + } + + private static bool CanImplicitCast(this Type baseType) + { + return baseType.CanCast(ImplicitCastMethodName); + } + + private static bool CanImplicitCast(this object obj) + { + var baseType = obj.GetType(); + return baseType.CanImplicitCast(); + } + + private static bool CanExplicitCast(this Type baseType) + { + return baseType.CanCast(ExplicitCastMethodName); + } + + private static bool CanExplicitCast(this object obj) + { + var baseType = obj.GetType(); + return baseType.CanExplicitCast(); + } + + private static bool CanCast(this Type baseType, string castMethodName) + { + var targetType = typeof(T); + return baseType.GetMethods(BindingFlags.Public | BindingFlags.Static) + .Where(mi => mi.Name == castMethodName && mi.ReturnType == targetType) + .Any(mi => + { + ParameterInfo pi = mi.GetParameters().FirstOrDefault(); + return pi != null && pi.ParameterType == baseType; + }); + } + + private static T ImplicitCast(this object obj) + { + return obj.Cast(ImplicitCastMethodName); + } + + private static T ExplicitCast(this object obj) + { + return obj.Cast(ExplicitCastMethodName); + } + + private static T Cast(this object obj, string castMethodName) + { + var objType = obj.GetType(); + MethodInfo conversionMethod = objType.GetMethods(BindingFlags.Public | BindingFlags.Static) + .Where(mi => mi.Name == castMethodName && mi.ReturnType == typeof(T)) + .SingleOrDefault(mi => + { + ParameterInfo pi = mi.GetParameters().FirstOrDefault(); + return pi != null && pi.ParameterType == objType; + }); + if (conversionMethod != null) + return (T) conversionMethod.Invoke(null, new[] {obj}); + else + throw new InvalidCastException($"No method to cast {objType.FullName} to {typeof(T).FullName}"); + } + } +} diff --git a/src/CommandLine/CommandLine.csproj b/src/CommandLine/CommandLine.csproj index 46cd3c1c..04496eb8 100644 --- a/src/CommandLine/CommandLine.csproj +++ b/src/CommandLine/CommandLine.csproj @@ -1,42 +1,56 @@  - - CommandLine - Library - netstandard2.0 - $(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 - $(DefineConstants);SKIP_FSHARP - true - ..\..\CommandLine.snk - true - CommandLineParser - CommandLineParser.FSharp - gsscoder;nemec;ericnewton76 - Command Line Parser Library - $(VersionSuffix) - 2.4.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 - https://raw.githubusercontent.com/gsscoder/commandline/master/doc/LICENSE - https://github.com/gsscoder/commandline - https://raw.githubusercontent.com/commandlineparser/commandline/master/art/CommandLine20.png - command line;commandline;argument;option;parser;parsing;library;syntax;shell - + + CommandLine + Library + netstandard2.0;net40;net45;net461 + $(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;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 - 2020 Giacomo Stelluti Scala & Contributors + License.md + CommandLine20.png + https://github.com/commandlineparser/commandline + command line;commandline;argument;option;parser;parsing;library;syntax;shell + https://github.com/commandlineparser/commandline/blob/master/CHANGELOG.md + true + 8.0 + true + snupkg + - - - + + + - - - true - README.md - - + + + true + README.md + + - - - - - \ No newline at end of file + + + + + + + + + + + + + + diff --git a/src/CommandLine/Core/GetoptTokenizer.cs b/src/CommandLine/Core/GetoptTokenizer.cs new file mode 100644 index 00000000..b8c97fc2 --- /dev/null +++ b/src/CommandLine/Core/GetoptTokenizer.cs @@ -0,0 +1,228 @@ +// Copyright 2005-2015 Giacomo Stelluti Scala & Contributors. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using CommandLine.Infrastructure; +using CSharpx; +using RailwaySharp.ErrorHandling; +using System.Text.RegularExpressions; + +namespace CommandLine.Core +{ + static class GetoptTokenizer + { + public static Result, Error> Tokenize( + IEnumerable arguments, + Func nameLookup) + { + return GetoptTokenizer.Tokenize(arguments, nameLookup, ignoreUnknownArguments:false, allowDashDash:true, posixlyCorrect:false); + } + + public static Result, Error> Tokenize( + IEnumerable arguments, + Func nameLookup, + bool ignoreUnknownArguments, + bool allowDashDash, + bool posixlyCorrect) + { + var errors = new List(); + Action onBadFormatToken = arg => errors.Add(new BadFormatTokenError(arg)); + Action unknownOptionError = name => errors.Add(new UnknownOptionError(name)); + Action doNothing = name => {}; + Action onUnknownOption = ignoreUnknownArguments ? doNothing : unknownOptionError; + + int consumeNext = 0; + Action onConsumeNext = (n => consumeNext = consumeNext + n); + bool forceValues = false; + + var tokens = new List(); + + var enumerator = arguments.GetEnumerator(); + while (enumerator.MoveNext()) + { + switch (enumerator.Current) { + case null: + break; + + case string arg when forceValues: + tokens.Add(Token.ValueForced(arg)); + break; + + case string arg when consumeNext > 0: + tokens.Add(Token.Value(arg)); + consumeNext = consumeNext - 1; + break; + + case "--" when allowDashDash: + forceValues = true; + break; + + case "--": + tokens.Add(Token.Value("--")); + if (posixlyCorrect) forceValues = true; + break; + + case "-": + // A single hyphen is always a value (it usually means "read from stdin" or "write to stdout") + tokens.Add(Token.Value("-")); + if (posixlyCorrect) forceValues = true; + break; + + case string arg when arg.StartsWith("--"): + tokens.AddRange(TokenizeLongName(arg, nameLookup, onBadFormatToken, onUnknownOption, onConsumeNext)); + break; + + case string arg when arg.StartsWith("-"): + tokens.AddRange(TokenizeShortName(arg, nameLookup, onUnknownOption, onConsumeNext)); + break; + + case string arg: + // If we get this far, it's a plain value + tokens.Add(Token.Value(arg)); + if (posixlyCorrect) forceValues = true; + break; + } + } + + return Result.Succeed, Error>(tokens.AsEnumerable(), errors.AsEnumerable()); + } + + public static Result, Error> ExplodeOptionList( + Result, Error> tokenizerResult, + Func> optionSequenceWithSeparatorLookup) + { + var tokens = tokenizerResult.SucceededWith().Memoize(); + + var exploded = new List(tokens is ICollection coll ? coll.Count : tokens.Count()); + var nothing = Maybe.Nothing(); // Re-use same Nothing instance for efficiency + var separator = nothing; + foreach (var token in tokens) { + if (token.IsName()) { + separator = optionSequenceWithSeparatorLookup(token.Text); + exploded.Add(token); + } else { + // Forced values are never considered option values, so they should not be split + if (separator.MatchJust(out char sep) && sep != '\0' && !token.IsValueForced()) { + if (token.Text.Contains(sep)) { + exploded.AddRange(token.Text.Split(sep).Select(Token.ValueFromSeparator)); + } else { + exploded.Add(token); + } + } else { + exploded.Add(token); + } + separator = nothing; // Only first value after a separator can possibly be split + } + } + return Result.Succeed(exploded as IEnumerable, tokenizerResult.SuccessMessages()); + } + + public static Func< + IEnumerable, + IEnumerable, + Result, Error>> + ConfigureTokenizer( + StringComparer nameComparer, + bool ignoreUnknownArguments, + bool enableDashDash, + bool posixlyCorrect) + { + return (arguments, optionSpecs) => + { + var tokens = GetoptTokenizer.Tokenize(arguments, name => NameLookup.Contains(name, optionSpecs, nameComparer), ignoreUnknownArguments, enableDashDash, posixlyCorrect); + var explodedTokens = GetoptTokenizer.ExplodeOptionList(tokens, name => NameLookup.HavingSeparator(name, optionSpecs, nameComparer)); + return explodedTokens; + }; + } + + private static IEnumerable TokenizeShortName( + string arg, + Func nameLookup, + Action onUnknownOption, + Action onConsumeNext) + { + + // First option char that requires a value means we swallow the rest of the string as the value + // But if there is no rest of the string, then instead we swallow the next argument + string chars = arg.Substring(1); + int len = chars.Length; + if (len > 0 && Char.IsDigit(chars[0])) + { + // Assume it's a negative number + yield return Token.Value(arg); + yield break; + } + for (int i = 0; i < len; i++) + { + var s = new String(chars[i], 1); + switch(nameLookup(s)) + { + case NameLookupResult.OtherOptionFound: + yield return Token.Name(s); + + if (i+1 < len) + { + // Rest of this is the value (e.g. "-sfoo" where "-s" is a string-consuming arg) + yield return Token.Value(chars.Substring(i+1)); + yield break; + } + else + { + // Value is in next param (e.g., "-s foo") + onConsumeNext(1); + } + break; + + case NameLookupResult.NoOptionFound: + onUnknownOption(s); + break; + + default: + yield return Token.Name(s); + break; + } + } + } + + private static IEnumerable TokenizeLongName( + string arg, + Func nameLookup, + Action onBadFormatToken, + Action onUnknownOption, + Action onConsumeNext) + { + string[] parts = arg.Substring(2).Split(new char[] { '=' }, 2); + string name = parts[0]; + string value = (parts.Length > 1) ? parts[1] : null; + // A parameter like "--stringvalue=" is acceptable, and makes stringvalue be the empty string + if (String.IsNullOrWhiteSpace(name) || name.Contains(" ")) + { + onBadFormatToken(arg); + yield break; + } + switch(nameLookup(name)) + { + case NameLookupResult.NoOptionFound: + onUnknownOption(name); + yield break; + + case NameLookupResult.OtherOptionFound: + yield return Token.Name(name); + if (value == null) // NOT String.IsNullOrEmpty + { + onConsumeNext(1); + } + else + { + yield return Token.Value(value); + } + break; + + default: + yield return Token.Name(name); + break; + } + } + } +} diff --git a/src/CommandLine/Core/InstanceBuilder.cs b/src/CommandLine/Core/InstanceBuilder.cs index 82c29ea8..f48127b1 100644 --- a/src/CommandLine/Core/InstanceBuilder.cs +++ b/src/CommandLine/Core/InstanceBuilder.cs @@ -24,54 +24,78 @@ public static ParserResult Build( bool autoVersion, IEnumerable nonFatalErrors) { + return Build( + factory, + tokenizer, + arguments, + nameComparer, + ignoreValueCase, + parsingCulture, + autoHelp, + autoVersion, + false, + nonFatalErrors); + } + + public static ParserResult Build( + Maybe> factory, + Func, IEnumerable, Result, Error>> tokenizer, + IEnumerable arguments, + StringComparer nameComparer, + bool ignoreValueCase, + CultureInfo parsingCulture, + bool autoHelp, + bool autoVersion, + bool allowMultiInstance, + IEnumerable nonFatalErrors) { var typeInfo = factory.MapValueOrDefault(f => f().GetType(), typeof(T)); var specProps = typeInfo.GetSpecifications(pi => SpecificationProperty.Create( Specification.FromProperty(pi), pi, Maybe.Nothing())) - .Memorize(); + .Memoize(); var specs = from pt in specProps select pt.Specification; var optionSpecs = specs .ThrowingValidate(SpecificationGuards.Lookup) .OfType() - .Memorize(); + .Memoize(); Func makeDefault = () => typeof(T).IsMutable() - ? factory.MapValueOrDefault(f => f(), Activator.CreateInstance()) + ? factory.MapValueOrDefault(f => f(), () => Activator.CreateInstance()) : ReflectionHelper.CreateDefaultImmutableInstance( (from p in specProps select p.Specification.ConversionType).ToArray()); Func, ParserResult> notParsed = errs => new NotParsed(makeDefault().GetType().ToTypeInfo(), errs); - var argumentsList = arguments.Memorize(); + var argumentsList = arguments.Memoize(); Func> buildUp = () => { var tokenizerResult = tokenizer(argumentsList, optionSpecs); - var tokens = tokenizerResult.SucceededWith().Memorize(); + var tokens = tokenizerResult.SucceededWith().Memoize(); var partitions = TokenPartitioner.Partition( tokens, name => TypeLookup.FindTypeDescriptorAndSibling(name, optionSpecs, nameComparer)); - var optionsPartition = partitions.Item1.Memorize(); - var valuesPartition = partitions.Item2.Memorize(); - var errorsPartition = partitions.Item3.Memorize(); + var optionsPartition = partitions.Item1.Memoize(); + var valuesPartition = partitions.Item2.Memoize(); + var errorsPartition = partitions.Item3.Memoize(); var optionSpecPropsResult = OptionMapper.MapValues( (from pt in specProps where pt.Specification.IsOption() select pt), optionsPartition, - (vals, type, isScalar) => TypeConverter.ChangeType(vals, type, isScalar, parsingCulture, ignoreValueCase), + (vals, type, isScalar, isFlag) => TypeConverter.ChangeType(vals, type, isScalar, isFlag, parsingCulture, ignoreValueCase), nameComparer); var valueSpecPropsResult = ValueMapper.MapValues( (from pt in specProps where pt.Specification.IsValue() orderby ((ValueSpecification)pt.Specification).Index select pt), valuesPartition, - (vals, type, isScalar) => TypeConverter.ChangeType(vals, type, isScalar, parsingCulture, ignoreValueCase)); + (vals, type, isScalar) => TypeConverter.ChangeType(vals, type, isScalar, false, parsingCulture, ignoreValueCase)); var missingValueErrors = from token in errorsPartition select @@ -80,56 +104,31 @@ public static ParserResult Build( .FromOptionSpecification()); var specPropsWithValue = - optionSpecPropsResult.SucceededWith().Concat(valueSpecPropsResult.SucceededWith()).Memorize(); + optionSpecPropsResult.SucceededWith().Concat(valueSpecPropsResult.SucceededWith()).Memoize(); - var setPropertyErrors = new List(); + var setPropertyErrors = new List(); - Func buildMutable = () => + //build the instance, determining if the type is mutable or not. + T instance; + if(typeInfo.IsMutable() == true) { - var mutable = factory.MapValueOrDefault(f => f(), Activator.CreateInstance()); - setPropertyErrors.AddRange(mutable.SetProperties(specPropsWithValue, sp => sp.Value.IsJust(), sp => sp.Value.FromJustOrFail())); - setPropertyErrors.AddRange(mutable.SetProperties( - specPropsWithValue, - sp => sp.Value.IsNothing() && sp.Specification.DefaultValue.IsJust(), - sp => sp.Specification.DefaultValue.FromJustOrFail())); - setPropertyErrors.AddRange(mutable.SetProperties( - specPropsWithValue, - sp => - sp.Value.IsNothing() && sp.Specification.TargetType == TargetType.Sequence - && sp.Specification.DefaultValue.MatchNothing(), - sp => sp.Property.PropertyType.GetTypeInfo().GetGenericArguments().Single().CreateEmptyArray())); - return mutable; - }; - - Func buildImmutable = () => + instance = BuildMutable(factory, specPropsWithValue, setPropertyErrors); + } + else { - var ctor = typeInfo.GetTypeInfo().GetConstructor((from sp in specProps select sp.Property.PropertyType).ToArray()); - var values = (from prms in ctor.GetParameters() - join sp in specPropsWithValue on prms.Name.ToLower() equals sp.Property.Name.ToLower() into spv - from sp in spv.DefaultIfEmpty() - select - sp == null - ? specProps.First(s => String.Equals(s.Property.Name, prms.Name, StringComparison.CurrentCultureIgnoreCase)) - .Property.PropertyType.GetDefaultValue() - : sp.Value.GetValueOrDefault( - sp.Specification.DefaultValue.GetValueOrDefault( - sp.Specification.ConversionType.CreateDefaultForImmutable()))).ToArray(); - var immutable = (T)ctor.Invoke(values); - return immutable; - }; - - var instance = typeInfo.IsMutable() ? buildMutable() : buildImmutable(); - - var validationErrors = specPropsWithValue.Validate(SpecificationPropertyRules.Lookup(tokens)); + instance = BuildImmutable(typeInfo, factory, specProps, specPropsWithValue, setPropertyErrors); + } + + var validationErrors = specPropsWithValue.Validate(SpecificationPropertyRules.Lookup(tokens, allowMultiInstance)); var allErrors = - tokenizerResult.SuccessfulMessages() + tokenizerResult.SuccessMessages() .Concat(missingValueErrors) - .Concat(optionSpecPropsResult.SuccessfulMessages()) - .Concat(valueSpecPropsResult.SuccessfulMessages()) + .Concat(optionSpecPropsResult.SuccessMessages()) + .Concat(valueSpecPropsResult.SuccessMessages()) .Concat(validationErrors) .Concat(setPropertyErrors) - .Memorize(); + .Memoize(); var warnings = from e in allErrors where nonFatalErrors.Contains(e.Tag) select e; @@ -140,7 +139,7 @@ from sp in spv.DefaultIfEmpty() argumentsList.Any() ? arguments.Preprocess(PreprocessorGuards.Lookup(nameComparer, autoHelp, autoVersion)) : Enumerable.Empty() - ).Memorize(); + ).Memoize(); var result = argumentsList.Any() ? preprocessorErrors.Any() @@ -150,5 +149,83 @@ from sp in spv.DefaultIfEmpty() return result; } + + private static T BuildMutable(Maybe> factory, IEnumerable specPropsWithValue, List setPropertyErrors ) + { + var mutable = factory.MapValueOrDefault(f => f(), () => Activator.CreateInstance()); + + setPropertyErrors.AddRange( + mutable.SetProperties( + specPropsWithValue, + sp => sp.Value.IsJust(), + sp => sp.Value.FromJustOrFail() + ) + ); + + setPropertyErrors.AddRange( + mutable.SetProperties( + specPropsWithValue, + sp => sp.Value.IsNothing() && sp.Specification.DefaultValue.IsJust(), + sp => sp.Specification.DefaultValue.FromJustOrFail() + ) + ); + + setPropertyErrors.AddRange( + mutable.SetProperties( + specPropsWithValue, + sp => sp.Value.IsNothing() + && sp.Specification.TargetType == TargetType.Sequence + && sp.Specification.DefaultValue.MatchNothing(), + sp => sp.Property.PropertyType.GetTypeInfo().GetGenericArguments().Single().CreateEmptyArray() + ) + ); + + return mutable; + } + + private static T BuildImmutable(Type typeInfo, Maybe> factory, IEnumerable specProps, IEnumerable specPropsWithValue, List setPropertyErrors) + { + var ctor = typeInfo.GetTypeInfo().GetConstructor( + specProps.Select(sp => sp.Property.PropertyType).ToArray() + ); + + if(ctor == null) + { + throw new InvalidOperationException($"Type {typeInfo.FullName} appears to be immutable, but no constructor found to accept values."); + } + try + { + var values = + (from prms in ctor.GetParameters() + join sp in specPropsWithValue on prms.Name.ToLower() equals sp.Property.Name.ToLower() into spv + from sp in spv.DefaultIfEmpty() + select + sp == null + ? specProps.First(s => String.Equals(s.Property.Name, prms.Name, StringComparison.CurrentCultureIgnoreCase)) + .Property.PropertyType.GetDefaultValue() + : sp.Value.GetValueOrDefault( + sp.Specification.DefaultValue.GetValueOrDefault( + sp.Specification.ConversionType.CreateDefaultForImmutable()))).ToArray(); + + var immutable = (T)ctor.Invoke(values); + + return immutable; + } + catch (Exception) + { + var ctorArgs = specPropsWithValue + .Select(x => x.Property.Name.ToLowerInvariant()).ToArray(); + throw GetException(ctorArgs); + } + Exception GetException(string[] s) + { + var ctorSyntax = s != null ? " Constructor Parameters can be ordered as: " + $"'({string.Join(", ", s)})'" : string.Empty; + var msg = + $"Type {typeInfo.FullName} appears to be Immutable with invalid constructor. Check that constructor arguments have the same name and order of their underlying Type. {ctorSyntax}"; + InvalidOperationException invalidOperationException = new InvalidOperationException(msg); + return invalidOperationException; + } + } + } -} \ No newline at end of file +} diff --git a/src/CommandLine/Core/InstanceChooser.cs b/src/CommandLine/Core/InstanceChooser.cs index 86917233..72307bf2 100644 --- a/src/CommandLine/Core/InstanceChooser.cs +++ b/src/CommandLine/Core/InstanceChooser.cs @@ -23,33 +23,68 @@ public static ParserResult Choose( bool autoVersion, IEnumerable nonFatalErrors) { - Func> choose = () => + return Choose( + tokenizer, + types, + arguments, + nameComparer, + ignoreValueCase, + parsingCulture, + autoHelp, + autoVersion, + false, + nonFatalErrors); + } + + public static ParserResult Choose( + Func, IEnumerable, Result, Error>> tokenizer, + IEnumerable types, + IEnumerable arguments, + StringComparer nameComparer, + bool ignoreValueCase, + CultureInfo parsingCulture, + bool autoHelp, + bool autoVersion, + bool allowMultiInstance, + 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; + + ParserResult choose() { var firstArg = arguments.First(); - Func preprocCompare = command => + bool preprocCompare(string command) => 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, allowMultiInstance, 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 MatchVerb( + private static ParserResult MatchDefaultVerb( Func, IEnumerable, Result, Error>> tokenizer, IEnumerable> verbs, + Tuple defaultVerb, IEnumerable arguments, StringComparer nameComparer, bool ignoreValueCase, @@ -58,13 +93,11 @@ private static ParserResult MatchVerb( bool autoVersion, IEnumerable nonFatalErrors) { - return verbs.Any(a => nameComparer.Equals(a.Item1.Name, arguments.First())) + return !(defaultVerb is null) ? InstanceBuilder.Build( - Maybe.Just>( - () => - verbs.Single(v => nameComparer.Equals(v.Item1.Name, arguments.First())).Item2.AutoDefault()), + Maybe.Just>(() => defaultVerb.Item2.AutoDefault()), tokenizer, - arguments.Skip(1), + arguments, nameComparer, ignoreValueCase, parsingCulture, @@ -74,6 +107,44 @@ private static ParserResult MatchVerb( : 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, + CultureInfo parsingCulture, + bool autoHelp, + bool autoVersion, + bool allowMultiInstance, + IEnumerable nonFatalErrors) + { + string firstArg = arguments.First(); + + var verbUsed = verbs.FirstOrDefault(vt => + nameComparer.Equals(vt.Item1.Name, firstArg) + || vt.Item1.Aliases.Any(alias => nameComparer.Equals(alias, firstArg)) + ); + + if (verbUsed == default) + { + return MatchDefaultVerb(tokenizer, verbs, defaultVerb, arguments, nameComparer, ignoreValueCase, parsingCulture, autoHelp, autoVersion, nonFatalErrors); + } + return InstanceBuilder.Build( + Maybe.Just>( + () => verbUsed.Item2.AutoDefault()), + tokenizer, + arguments.Skip(1), + nameComparer, + ignoreValueCase, + parsingCulture, + autoHelp, + autoVersion, + allowMultiInstance, + nonFatalErrors); + } + private static HelpVerbRequestedError MakeHelpVerbRequestedError( IEnumerable> verbs, string verb, diff --git a/src/CommandLine/Core/NameLookup.cs b/src/CommandLine/Core/NameLookup.cs index 3605d1a3..ccb24ea5 100644 --- a/src/CommandLine/Core/NameLookup.cs +++ b/src/CommandLine/Core/NameLookup.cs @@ -20,7 +20,7 @@ public static NameLookupResult Contains(string name, IEnumerable name.MatchName(a.ShortName, a.LongName, comparer)); if (option == null) return NameLookupResult.NoOptionFound; - return option.ConversionType == typeof(bool) + return option.ConversionType == typeof(bool) || (option.ConversionType == typeof(int) && option.FlagCounter) ? NameLookupResult.BooleanOptionFound : NameLookupResult.OtherOptionFound; } diff --git a/src/CommandLine/Core/OptionMapper.cs b/src/CommandLine/Core/OptionMapper.cs index 102646a9..ded42c4f 100644 --- a/src/CommandLine/Core/OptionMapper.cs +++ b/src/CommandLine/Core/OptionMapper.cs @@ -15,35 +15,42 @@ public static Result< MapValues( IEnumerable propertyTuples, IEnumerable>> options, - Func, Type, bool, Maybe> converter, + Func, Type, bool, bool, Maybe> converter, StringComparer comparer) { var sequencesAndErrors = propertyTuples .Select( pt => { - var matched = options.FirstOrDefault(s => + var matched = options.Where(s => s.Key.MatchName(((OptionSpecification)pt.Specification).ShortName, ((OptionSpecification)pt.Specification).LongName, comparer)).ToMaybe(); - return matched.IsJust() - ? ( - from sequence in matched - from converted in - converter( - sequence.Value, - pt.Property.PropertyType, - pt.Specification.TargetType != TargetType.Sequence) - select Tuple.Create( - pt.WithValue(Maybe.Just(converted)), Maybe.Nothing()) - ) + if (matched.IsJust()) + { + var matches = matched.GetValueOrDefault(Enumerable.Empty>>()); + var values = new List(); + foreach (var kvp in matches) + { + foreach (var value in kvp.Value) + { + values.Add(value); + } + } + + bool isFlag = pt.Specification.Tag == SpecificationType.Option && ((OptionSpecification)pt.Specification).FlagCounter; + + return converter(values, isFlag ? typeof(bool) : pt.Property.PropertyType, pt.Specification.TargetType != TargetType.Sequence, isFlag) + .Select(value => Tuple.Create(pt.WithValue(Maybe.Just(value)), Maybe.Nothing())) .GetValueOrDefault( Tuple.Create>( pt, Maybe.Just( new BadFormatConversionError( - ((OptionSpecification)pt.Specification).FromOptionSpecification())))) - : Tuple.Create(pt, Maybe.Nothing()); + ((OptionSpecification)pt.Specification).FromOptionSpecification())))); + } + + return Tuple.Create(pt, Maybe.Nothing()); } - ).Memorize(); + ).Memoize(); return Result.Succeed( sequencesAndErrors.Select(se => se.Item1), sequencesAndErrors.Select(se => se.Item2).OfType>().Select(se => se.Value)); diff --git a/src/CommandLine/Core/OptionSpecification.cs b/src/CommandLine/Core/OptionSpecification.cs index 0bbbbb06..1c2e4f88 100644 --- a/src/CommandLine/Core/OptionSpecification.cs +++ b/src/CommandLine/Core/OptionSpecification.cs @@ -1,4 +1,4 @@ -// Copyright 2005-2015 Giacomo Stelluti Scala & Contributors. All rights reserved. See License.md in the project root for license information. +// Copyright 2005-2015 Giacomo Stelluti Scala & Contributors. All rights reserved. See License.md in the project root for license information. using System; using System.Collections.Generic; @@ -13,16 +13,21 @@ sealed class OptionSpecification : Specification private readonly string longName; private readonly char separator; private readonly string setName; + private readonly string group; + private readonly bool flagCounter; public OptionSpecification(string shortName, string longName, bool required, string setName, Maybe min, Maybe max, char separator, Maybe defaultValue, string helpText, string metaValue, IEnumerable enumValues, - Type conversionType, TargetType targetType, bool hidden = false) - : base(SpecificationType.Option, required, min, max, defaultValue, helpText, metaValue, enumValues, conversionType, targetType, hidden) + Type conversionType, TargetType targetType, string group, bool flagCounter = false, bool hidden = false) + : base(SpecificationType.Option, + required, min, max, defaultValue, helpText, metaValue, enumValues, conversionType, conversionType == typeof(int) && flagCounter ? TargetType.Switch : targetType, hidden) { this.shortName = shortName; this.longName = longName; this.separator = separator; this.setName = setName; + this.group = group; + this.flagCounter = flagCounter; } public static OptionSpecification FromAttribute(OptionAttribute attribute, Type conversionType, IEnumerable enumValues) @@ -41,13 +46,15 @@ public static OptionSpecification FromAttribute(OptionAttribute attribute, Type enumValues, conversionType, conversionType.ToTargetType(), + attribute.Group, + attribute.FlagCounter, attribute.Hidden); } public static OptionSpecification NewSwitch(string shortName, string longName, bool required, string helpText, string metaValue, bool hidden = false) { return new OptionSpecification(shortName, longName, required, string.Empty, Maybe.Nothing(), Maybe.Nothing(), - '\0', Maybe.Nothing(), helpText, metaValue, Enumerable.Empty(), typeof(bool), TargetType.Switch, hidden); + '\0', Maybe.Nothing(), helpText, metaValue, Enumerable.Empty(), typeof(bool), TargetType.Switch, string.Empty, false, hidden); } public string ShortName @@ -69,5 +76,18 @@ public string SetName { get { return setName; } } + + public string Group + { + get { return group; } + } + + /// + /// Whether this is an int option that counts how many times a flag was set rather than taking a value on the command line + /// + public bool FlagCounter + { + get { return flagCounter; } + } } -} \ No newline at end of file +} diff --git a/src/CommandLine/Core/PartitionExtensions.cs b/src/CommandLine/Core/PartitionExtensions.cs new file mode 100644 index 00000000..47cc397e --- /dev/null +++ b/src/CommandLine/Core/PartitionExtensions.cs @@ -0,0 +1,25 @@ +// Copyright 2005-2015 Giacomo Stelluti Scala & Contributors. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using CSharpx; + +namespace CommandLine.Core +{ + static class PartitionExtensions + { + public static Tuple,IEnumerable> PartitionByPredicate( + this IEnumerable items, + Func pred) + { + List yes = new List(); + List no = new List(); + foreach (T item in items) { + List list = pred(item) ? yes : no; + list.Add(item); + } + return Tuple.Create,IEnumerable>(yes, no); + } + } +} diff --git a/src/CommandLine/Core/ReflectionExtensions.cs b/src/CommandLine/Core/ReflectionExtensions.cs index 87fe97db..622e1e6e 100644 --- a/src/CommandLine/Core/ReflectionExtensions.cs +++ b/src/CommandLine/Core/ReflectionExtensions.cs @@ -4,7 +4,6 @@ using System.Collections; using System.Collections.Generic; using System.Linq; -using System.Linq.Expressions; using System.Reflection; using CommandLine.Infrastructure; using CommandLine.Text; @@ -14,8 +13,6 @@ namespace CommandLine.Core { static class ReflectionExtensions { - public const string CannotSetValueToTargetInstance = "Cannot set value to target instance."; - public static IEnumerable GetSpecifications(this Type type, Func selector) { return from pi in type.FlattenHierarchy().SelectMany(x => x.GetTypeInfo().GetProperties()) @@ -42,8 +39,8 @@ public static Maybe> GetUsageData(this Type { return (from pi in type.FlattenHierarchy().SelectMany(x => x.GetTypeInfo().GetProperties()) - let attrs = pi.GetCustomAttributes(true) - where attrs.OfType().Any() + let attrs = pi.GetCustomAttributes(typeof(UsageAttribute), true) + where attrs.Any() select Tuple.Create(pi, (UsageAttribute)attrs.First())) .SingleOrDefault() .ToMaybe(); @@ -93,10 +90,6 @@ public static IEnumerable SetProperties( private static IEnumerable SetValue(this SpecificationProperty specProp, T instance, object value) { - Action fail = inner => { - throw new InvalidOperationException(CannotSetValueToTargetInstance, inner); - }; - try { specProp.Property.SetValue(instance, value, null); @@ -106,17 +99,18 @@ private static IEnumerable SetValue(this SpecificationProperty specPro { return new[] { new SetValueExceptionError(specProp.Specification.FromSpecification(), e.InnerException, value) }; } - catch (Exception e) + catch (ArgumentException e) { - return new[] { new SetValueExceptionError(specProp.Specification.FromSpecification(), e, value) }; + var argEx = new ArgumentException(InvalidAttributeConfigurationError.ErrorMessage, e); + + return new[] { new SetValueExceptionError(specProp.Specification.FromSpecification(), argEx, value) }; } - catch(ArgumentException e) + + catch (Exception e) { - var argEx = new ArgumentException(InvalidAttributeConfigurationError.ErrorMessage, e); - fail(argEx); + return new[] { new SetValueExceptionError(specProp.Specification.FromSpecification(), e, value) }; } - return instance; } public static object CreateEmptyArray(this Type type) @@ -126,21 +120,29 @@ public static object CreateEmptyArray(this Type type) public static object GetDefaultValue(this Type type) { - var e = Expression.Lambda>( - Expression.Convert( - Expression.Default(type), - typeof(object))); - return e.Compile()(); + return type.IsValueType ? Activator.CreateInstance(type) : null; } public static bool IsMutable(this Type type) { - Func isMutable = () => { - var props = type.GetTypeInfo().GetProperties(BindingFlags.Public | BindingFlags.Instance).Any(p => p.CanWrite); - var fields = type.GetTypeInfo().GetFields(BindingFlags.Public | BindingFlags.Instance).Any(); - return props || fields; - }; - return type != typeof(object) ? isMutable() : true; + if(type == typeof(object)) + return true; + + // Find all inherited defined properties and fields on the type + var inheritedTypes = type.GetTypeInfo().FlattenHierarchy().Select(i => i.GetTypeInfo()); + + foreach (var inheritedType in inheritedTypes) + { + if ( + inheritedType.GetTypeInfo().GetProperties(BindingFlags.Public | BindingFlags.Instance).Any(p => p.CanWrite) || + inheritedType.GetTypeInfo().GetFields(BindingFlags.Public | BindingFlags.Instance).Any() + ) + { + return true; + } + } + + return false; } public static object CreateDefaultForImmutable(this Type type) @@ -213,5 +215,13 @@ public static bool IsPrimitiveEx(this Type type) }.Contains(type) || Convert.GetTypeCode(type) != TypeCode.Object; } + + public static bool IsCustomStruct(this Type type) + { + var isStruct = type.GetTypeInfo().IsValueType && !type.GetTypeInfo().IsPrimitive && !type.GetTypeInfo().IsEnum && type != typeof(Guid); + if (!isStruct) return false; + var ctor = type.GetTypeInfo().GetConstructor(new[] { typeof(string) }); + return ctor != null; + } } -} \ No newline at end of file +} diff --git a/src/CommandLine/Core/Scalar.cs b/src/CommandLine/Core/Scalar.cs deleted file mode 100644 index 215ca2d2..00000000 --- a/src/CommandLine/Core/Scalar.cs +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright 2005-2015 Giacomo Stelluti Scala & Contributors. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Linq; -using CommandLine.Infrastructure; -using CSharpx; - -namespace CommandLine.Core -{ - static class Scalar - { - public static IEnumerable Partition( - IEnumerable tokens, - Func> typeLookup) - { - return from tseq in tokens.Pairwise( - (f, s) => - f.IsName() && s.IsValue() - ? typeLookup(f.Text).MapValueOrDefault(info => - info.TargetType == TargetType.Scalar ? new[] { f, s } : new Token[] { }, new Token[] { }) - : new Token[] { }) - from t in tseq - select t; - } - } -} diff --git a/src/CommandLine/Core/Sequence.cs b/src/CommandLine/Core/Sequence.cs deleted file mode 100644 index 04d1b4ae..00000000 --- a/src/CommandLine/Core/Sequence.cs +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright 2005-2015 Giacomo Stelluti Scala & Contributors. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Linq; -using CommandLine.Infrastructure; -using CSharpx; - -namespace CommandLine.Core -{ - static class Sequence - { - public static IEnumerable Partition( - IEnumerable tokens, - Func> typeLookup) - { - return from tseq in tokens.Pairwise( - (f, s) => - f.IsName() && s.IsValue() - ? typeLookup(f.Text).MapValueOrDefault(info => - info.TargetType == TargetType.Sequence - ? new[] { f }.Concat(tokens.OfSequence(f, info)) - : new Token[] { }, new Token[] { }) - : new Token[] { }) - from t in tseq - select t; - } - - private static IEnumerable OfSequence(this IEnumerable tokens, Token nameToken, TypeDescriptor info) - { - var nameIndex = tokens.IndexOf(t => t.Equals(nameToken)); - if (nameIndex >= 0) - { - return info.NextValue.MapValueOrDefault( - _ => info.MaxItems.MapValueOrDefault( - n => tokens.Skip(nameIndex + 1).Take(n), - tokens.Skip(nameIndex + 1).TakeWhile(v => v.IsValue())), - tokens.Skip(nameIndex + 1).TakeWhile(v => v.IsValue())); - } - return new Token[] { }; - } - } -} diff --git a/src/CommandLine/Core/Specification.cs b/src/CommandLine/Core/Specification.cs index 9b267741..b95b998c 100644 --- a/src/CommandLine/Core/Specification.cs +++ b/src/CommandLine/Core/Specification.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; +using CommandLine.Infrastructure; using CSharpx; namespace CommandLine.Core @@ -115,9 +116,8 @@ public static Specification FromProperty(PropertyInfo property) if (oa.Count() == 1) { var spec = OptionSpecification.FromAttribute(oa.Single(), property.PropertyType, - property.PropertyType.GetTypeInfo().IsEnum - ? Enum.GetNames(property.PropertyType) - : Enumerable.Empty()); + ReflectionHelper.GetNamesOfEnum(property.PropertyType)); + if (spec.ShortName.Length == 0 && spec.LongName.Length == 0) { return spec.WithLongName(property.Name.ToLowerInvariant()); diff --git a/src/CommandLine/Core/SpecificationExtensions.cs b/src/CommandLine/Core/SpecificationExtensions.cs index 5f77a5dd..c080e983 100644 --- a/src/CommandLine/Core/SpecificationExtensions.cs +++ b/src/CommandLine/Core/SpecificationExtensions.cs @@ -34,6 +34,8 @@ public static OptionSpecification WithLongName(this OptionSpecification specific specification.EnumValues, specification.ConversionType, specification.TargetType, + specification.Group, + specification.FlagCounter, specification.Hidden); } diff --git a/src/CommandLine/Core/SpecificationPropertyRules.cs b/src/CommandLine/Core/SpecificationPropertyRules.cs index 71145e8a..4f8b78a9 100644 --- a/src/CommandLine/Core/SpecificationPropertyRules.cs +++ b/src/CommandLine/Core/SpecificationPropertyRules.cs @@ -1,9 +1,10 @@ -// Copyright 2005-2015 Giacomo Stelluti Scala & Contributors. All rights reserved. See License.md in the project root for license information. +// Copyright 2005-2015 Giacomo Stelluti Scala & Contributors. All rights reserved. See License.md in the project root for license information. + +using CSharpx; using System; using System.Collections.Generic; using System.Linq; -using CSharpx; namespace CommandLine.Core { @@ -12,16 +13,79 @@ static class SpecificationPropertyRules public static IEnumerable, IEnumerable>> Lookup( IEnumerable tokens) + { + return Lookup(tokens, false); + } + + public static IEnumerable, IEnumerable>> + Lookup( + IEnumerable tokens, + bool allowMultiInstance) { return new List, IEnumerable>> { EnforceMutuallyExclusiveSet(), + EnforceGroup(), + EnforceMutuallyExclusiveSetAndGroupAreNotUsedTogether(), EnforceRequired(), EnforceRange(), - EnforceSingle(tokens) + EnforceSingle(tokens, allowMultiInstance) }; } + private static Func, 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 => + { + var optionsValues = + from sp in specProps + where sp.Specification.IsOption() + let o = (OptionSpecification)sp.Specification + where o.Group.Length > 0 + select new + { + Option = o, + 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() && g.DefaultValue.IsNothing())); + + if (errorGroups.Any()) + { + return errorGroups.Select(gr => new MissingGroupOptionError(gr.Key, gr.Select(g => new NameInfo(g.Option.ShortName, g.Option.LongName)))); + } + + return Enumerable.Empty(); + }; + } + private static Func, IEnumerable> EnforceMutuallyExclusiveSet() { return specProps => @@ -51,12 +115,12 @@ private static Func, IEnumerable> Enfo return specProps => { var requiredWithValue = from sp in specProps - where sp.Specification.IsOption() - where sp.Specification.Required - where sp.Value.IsJust() - let o = (OptionSpecification)sp.Specification - where o.SetName.Length > 0 - select sp.Specification; + where sp.Specification.IsOption() + where sp.Specification.Required + where sp.Value.IsJust() + let o = (OptionSpecification)sp.Specification + where o.SetName.Length > 0 + select sp.Specification; var setWithRequiredValue = ( from s in requiredWithValue let o = (OptionSpecification)s @@ -64,13 +128,14 @@ where o.SetName.Length > 0 select o.SetName) .Distinct(); var requiredWithoutValue = from sp in specProps - where sp.Specification.IsOption() - where sp.Specification.Required - where sp.Value.IsNothing() - let o = (OptionSpecification)sp.Specification - where o.SetName.Length > 0 - where setWithRequiredValue.ContainsIfNotEmpty(o.SetName) - select sp.Specification; + where sp.Specification.IsOption() + where sp.Specification.Required + where sp.Value.IsNothing() + let o = (OptionSpecification)sp.Specification + where o.SetName.Length > 0 + where o.Group.Length == 0 + where setWithRequiredValue.ContainsIfNotEmpty(o.SetName) + select sp.Specification; var missing = requiredWithoutValue .Except(requiredWithValue) @@ -81,6 +146,7 @@ where sp.Specification.Required where sp.Value.IsNothing() let o = (OptionSpecification)sp.Specification where o.SetName.Length == 0 + where o.Group.Length == 0 select sp.Specification) .Concat( from sp in specProps @@ -115,10 +181,15 @@ from s in options }; } - private static Func, IEnumerable> EnforceSingle(IEnumerable tokens) + private static Func, IEnumerable> EnforceSingle(IEnumerable tokens, bool allowMultiInstance) { return specProps => { + if (allowMultiInstance) + { + return Enumerable.Empty(); + } + var specs = from sp in specProps where sp.Specification.IsOption() where sp.Value.IsJust() @@ -130,11 +201,11 @@ from o in to.DefaultIfEmpty() where o != null select new { o.ShortName, o.LongName }; var longOptions = from t in tokens - where t.IsName() - join o in specs on t.Text equals o.LongName into to - from o in to.DefaultIfEmpty() - where o != null - select new { o.ShortName, o.LongName }; + where t.IsName() + join o in specs on t.Text equals o.LongName into to + from o in to.DefaultIfEmpty() + where o != null + select new { o.ShortName, o.LongName }; var groups = from x in shortOptions.Concat(longOptions) group x by x into g let count = g.Count() @@ -155,4 +226,4 @@ private static bool ContainsIfNotEmpty(this IEnumerable sequence, T value) return true; } } -} \ No newline at end of file +} diff --git a/src/CommandLine/Core/Switch.cs b/src/CommandLine/Core/Switch.cs deleted file mode 100644 index 96e62443..00000000 --- a/src/CommandLine/Core/Switch.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright 2005-2015 Giacomo Stelluti Scala & Contributors. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Linq; -using CSharpx; - -namespace CommandLine.Core -{ - static class Switch - { - public static IEnumerable Partition( - IEnumerable tokens, - Func> typeLookup) - { - return from t in tokens - where typeLookup(t.Text).MapValueOrDefault(info => t.IsName() && info.TargetType == TargetType.Switch, false) - select t; - } - } -} diff --git a/src/CommandLine/Core/Token.cs b/src/CommandLine/Core/Token.cs index 2afee98f..90bbe54d 100644 --- a/src/CommandLine/Core/Token.cs +++ b/src/CommandLine/Core/Token.cs @@ -32,6 +32,16 @@ public static Token Value(string text, bool explicitlyAssigned) return new Value(text, explicitlyAssigned); } + public static Token ValueForced(string text) + { + return new Value(text, false, true, false); + } + + public static Token ValueFromSeparator(string text) + { + return new Value(text, false, false, true); + } + public TokenType Tag { get { return tag; } @@ -80,23 +90,51 @@ public bool Equals(Name other) class Value : Token, IEquatable { private readonly bool explicitlyAssigned; + private readonly bool forced; + private readonly bool fromSeparator; public Value(string text) - : this(text, false) + : this(text, false, false, false) { } public Value(string text, bool explicitlyAssigned) + : this(text, explicitlyAssigned, false, false) + { + } + + public Value(string text, bool explicitlyAssigned, bool forced, bool fromSeparator) : base(TokenType.Value, text) { this.explicitlyAssigned = explicitlyAssigned; + this.forced = forced; + this.fromSeparator = fromSeparator; } + /// + /// Whether this value came from a long option with "=" separating the name from the value + /// public bool ExplicitlyAssigned { get { return explicitlyAssigned; } } + /// + /// Whether this value came from a sequence specified with a separator (e.g., "--files a.txt,b.txt,c.txt") + /// + public bool FromSeparator + { + get { return fromSeparator; } + } + + /// + /// Whether this value came from args after the -- separator (when EnableDashDash = true) + /// + public bool Forced + { + get { return forced; } + } + public override bool Equals(object obj) { var other = obj as Value; @@ -120,7 +158,7 @@ public bool Equals(Value other) return false; } - return Tag.Equals(other.Tag) && Text.Equals(other.Text); + return Tag.Equals(other.Tag) && Text.Equals(other.Text) && this.Forced == other.Forced; } } @@ -135,5 +173,15 @@ public static bool IsValue(this Token token) { return token.Tag == TokenType.Value; } + + public static bool IsValueFromSeparator(this Token token) + { + return token.IsValue() && ((Value)token).FromSeparator; + } + + public static bool IsValueForced(this Token token) + { + return token.IsValue() && ((Value)token).Forced; + } } -} \ No newline at end of file +} diff --git a/src/CommandLine/Core/TokenPartitioner.cs b/src/CommandLine/Core/TokenPartitioner.cs index cc1d8c26..4dc25f7f 100644 --- a/src/CommandLine/Core/TokenPartitioner.cs +++ b/src/CommandLine/Core/TokenPartitioner.cs @@ -17,16 +17,15 @@ Tuple>>, IEnumerable tokenComparer = ReferenceEqualityComparer.Default; - var tokenList = tokens.Memorize(); - var switches = new HashSet(Switch.Partition(tokenList, typeLookup), tokenComparer); - var scalars = new HashSet(Scalar.Partition(tokenList, typeLookup), tokenComparer); - var sequences = new HashSet(Sequence.Partition(tokenList, typeLookup), tokenComparer); - var nonOptions = tokenList - .Where(t => !switches.Contains(t)) - .Where(t => !scalars.Contains(t)) - .Where(t => !sequences.Contains(t)).Memorize(); - var values = nonOptions.Where(v => v.IsValue()).Memorize(); - var errors = nonOptions.Except(values, (IEqualityComparer)ReferenceEqualityComparer.Default).Memorize(); + var tokenList = tokens.Memoize(); + var partitioned = PartitionTokensByType(tokenList, typeLookup); + var switches = partitioned.Item1; + var scalars = partitioned.Item2; + var sequences = partitioned.Item3; + var nonOptions = partitioned.Item4; + var valuesAndErrors = nonOptions.PartitionByPredicate(v => v.IsValue()); + var values = valuesAndErrors.Item1; + var errors = valuesAndErrors.Item2; return Tuple.Create( KeyValuePairHelper.ForSwitch(switches) @@ -35,5 +34,151 @@ Tuple>>, IEnumerable t.Text), errors); } + + public static Tuple, IEnumerable, IEnumerable, IEnumerable> PartitionTokensByType( + IEnumerable tokens, + Func> typeLookup) + { + var switchTokens = new List(); + var scalarTokens = new List(); + var sequenceTokens = new List(); + var nonOptionTokens = new List(); + var sequences = new Dictionary>(); + var count = new Dictionary(); + var max = new Dictionary>(); + var state = SequenceState.TokenSearch; + var separatorSeen = false; + Token nameToken = null; + foreach (var token in tokens) + { + if (token.IsValueForced()) + { + separatorSeen = false; + nonOptionTokens.Add(token); + } + else if (token.IsName()) + { + separatorSeen = false; + if (typeLookup(token.Text).MatchJust(out var info)) + { + switch (info.TargetType) + { + case TargetType.Switch: + nameToken = null; + switchTokens.Add(token); + state = SequenceState.TokenSearch; + break; + case TargetType.Scalar: + nameToken = token; + scalarTokens.Add(nameToken); + state = SequenceState.ScalarTokenFound; + break; + case TargetType.Sequence: + nameToken = token; + if (! sequences.ContainsKey(nameToken)) + { + sequences[nameToken] = new List(); + count[nameToken] = 0; + max[nameToken] = info.MaxItems; + } + state = SequenceState.SequenceTokenFound; + break; + } + } + else + { + nameToken = null; + nonOptionTokens.Add(token); + state = SequenceState.TokenSearch; + } + } + else + { + switch (state) + { + case SequenceState.TokenSearch: + case SequenceState.ScalarTokenFound when nameToken == null: + case SequenceState.SequenceTokenFound when nameToken == null: + separatorSeen = false; + nameToken = null; + nonOptionTokens.Add(token); + state = SequenceState.TokenSearch; + break; + + case SequenceState.ScalarTokenFound: + separatorSeen = false; + nameToken = null; + scalarTokens.Add(token); + state = SequenceState.TokenSearch; + break; + + case SequenceState.SequenceTokenFound: + if (sequences.TryGetValue(nameToken, out var sequence)) { + if (max[nameToken].MatchJust(out int m) && count[nameToken] >= m) + { + // This sequence is completed, so this and any further values are non-option values + nameToken = null; + nonOptionTokens.Add(token); + state = SequenceState.TokenSearch; + } + else if (token.IsValueFromSeparator()) + { + separatorSeen = true; + sequence.Add(token); + count[nameToken]++; + } + else if (separatorSeen) + { + // Previous token came from a separator but this one didn't: sequence is completed + separatorSeen = false; + nameToken = null; + nonOptionTokens.Add(token); + state = SequenceState.TokenSearch; + } + else + { + sequence.Add(token); + count[nameToken]++; + } + } + else + { + // Should never get here, but just in case: + separatorSeen = false; + sequences[nameToken] = new List(new[] { token }); + count[nameToken] = 0; + max[nameToken] = Maybe.Nothing(); + } + break; + } + } + } + + foreach (var kvp in sequences) + { + if (kvp.Value.Empty()) { + nonOptionTokens.Add(kvp.Key); + } + else + { + sequenceTokens.Add(kvp.Key); + sequenceTokens.AddRange(kvp.Value); + } + } + return Tuple.Create( + (IEnumerable)switchTokens, + (IEnumerable)scalarTokens, + (IEnumerable)sequenceTokens, + (IEnumerable)nonOptionTokens + ); + } + + private enum SequenceState + { + TokenSearch, + SequenceTokenFound, + ScalarTokenFound, + } + } -} \ No newline at end of file +} diff --git a/src/CommandLine/Core/Tokenizer.cs b/src/CommandLine/Core/Tokenizer.cs index fb241579..fe94fc61 100644 --- a/src/CommandLine/Core/Tokenizer.cs +++ b/src/CommandLine/Core/Tokenizer.cs @@ -34,11 +34,11 @@ public static Result, Error> Tokenize( ? TokenizeLongName(arg, onError) : TokenizeShortName(arg, nameLookup) select token) - .Memorize(); + .Memoize(); - var normalized = normalize(tokens).Memorize(); + var normalized = normalize(tokens).Memoize(); - var unkTokens = (from t in normalized where t.IsName() && nameLookup(t.Text) == NameLookupResult.NoOptionFound select t).Memorize(); + var unkTokens = (from t in normalized where t.IsName() && nameLookup(t.Text) == NameLookupResult.NoOptionFound select t).Memoize(); return Result.Succeed(normalized.Where(x => !unkTokens.Contains(x)), errors.Concat(from t in unkTokens select new UnknownOptionError(t.Text))); } @@ -50,7 +50,7 @@ public static Result, Error> PreprocessDashDash( if (arguments.Any(arg => arg.EqualsOrdinal("--"))) { var tokenizerResult = tokenizer(arguments.TakeWhile(arg => !arg.EqualsOrdinal("--"))); - var values = arguments.SkipWhile(arg => !arg.EqualsOrdinal("--")).Skip(1).Select(Token.Value); + var values = arguments.SkipWhile(arg => !arg.EqualsOrdinal("--")).Skip(1).Select(Token.ValueForced); return tokenizerResult.Map(tokens => tokens.Concat(values)); } return tokenizer(arguments); @@ -60,46 +60,58 @@ public static Result, Error> ExplodeOptionList( Result, Error> tokenizerResult, Func> optionSequenceWithSeparatorLookup) { - var tokens = tokenizerResult.SucceededWith().Memorize(); - - var replaces = tokens.Select((t, i) => - optionSequenceWithSeparatorLookup(t.Text) - .MapValueOrDefault(sep => Tuple.Create(i + 1, sep), - Tuple.Create(-1, '\0'))).SkipWhile(x => x.Item1 < 0).Memorize(); - - var exploded = tokens.Select((t, i) => - replaces.FirstOrDefault(x => x.Item1 == i).ToMaybe() - .MapValueOrDefault(r => t.Text.Split(r.Item2).Select(Token.Value), - Enumerable.Empty().Concat(new[] { t }))); - - var flattened = exploded.SelectMany(x => x); - - return Result.Succeed(flattened, tokenizerResult.SuccessfulMessages()); + var tokens = tokenizerResult.SucceededWith().Memoize(); + + var exploded = new List(tokens is ICollection coll ? coll.Count : tokens.Count()); + var nothing = Maybe.Nothing(); // Re-use same Nothing instance for efficiency + var separator = nothing; + foreach (var token in tokens) { + if (token.IsName()) { + separator = optionSequenceWithSeparatorLookup(token.Text); + exploded.Add(token); + } else { + // Forced values are never considered option values, so they should not be split + if (separator.MatchJust(out char sep) && sep != '\0' && !token.IsValueForced()) { + if (token.Text.Contains(sep)) { + exploded.AddRange(token.Text.Split(sep).Select(Token.ValueFromSeparator)); + } else { + exploded.Add(token); + } + } else { + exploded.Add(token); + } + separator = nothing; // Only first value after a separator can possibly be split + } + } + return Result.Succeed(exploded as IEnumerable, tokenizerResult.SuccessMessages()); } + /// + /// Normalizes the given . + /// + /// The given minus all names, and their value if one was present, that are not found using . public static IEnumerable Normalize( IEnumerable tokens, Func nameLookup) { - var indexes = + var toExclude = from i in tokens.Select( (t, i) => { - var prev = tokens.ElementAtOrDefault(i - 1).ToMaybe(); - return t.IsValue() && ((Value)t).ExplicitlyAssigned - && prev.MapValueOrDefault(p => p.IsName() && !nameLookup(p.Text), false) - ? Maybe.Just(i) - : Maybe.Nothing(); + if (t.IsName() == false + || nameLookup(t.Text)) + { + return Maybe.Nothing>(); + } + + var next = tokens.ElementAtOrDefault(i + 1).ToMaybe(); + var removeValue = next.MatchJust(out var nextValue) + && next.MapValueOrDefault(p => p.IsValue() && ((Value)p).ExplicitlyAssigned, false); + return Maybe.Just(new Tuple(t, removeValue ? nextValue : null)); }).Where(i => i.IsJust()) select i.FromJustOrFail(); - var toExclude = - from t in - tokens.Select((t, i) => indexes.Contains(i) ? Maybe.Just(t) : Maybe.Nothing()) - .Where(t => t.IsJust()) - select t.FromJustOrFail(); - - var normalized = tokens.Where(t => toExclude.Contains(t) == false); + var normalized = tokens.Where(t => toExclude.Any(e => ReferenceEquals(e.Item1, t) || ReferenceEquals(e.Item2, t)) == false); return normalized; } @@ -135,6 +147,12 @@ private static IEnumerable TokenizeShortName( string value, Func nameLookup) { + //Allow single dash as a value + if (value.Length == 1 && value[0] == '-') + { + yield return Token.Value(value); + yield break; + } if (value.Length > 1 && value[0] == '-' && value[1] != '-') { var text = value.Substring(1); @@ -205,4 +223,4 @@ private static IEnumerable TokenizeLongName( } } } -} \ No newline at end of file +} diff --git a/src/CommandLine/Core/TypeConverter.cs b/src/CommandLine/Core/TypeConverter.cs index fef7945a..2e27af40 100644 --- a/src/CommandLine/Core/TypeConverter.cs +++ b/src/CommandLine/Core/TypeConverter.cs @@ -13,11 +13,13 @@ namespace CommandLine.Core { static class TypeConverter { - public static Maybe ChangeType(IEnumerable values, Type conversionType, bool scalar, CultureInfo conversionCulture, bool ignoreValueCase) + public static Maybe ChangeType(IEnumerable values, Type conversionType, bool scalar, bool isFlag, CultureInfo conversionCulture, bool ignoreValueCase) { - return scalar - ? ChangeTypeScalar(values.Single(), conversionType, conversionCulture, ignoreValueCase) - : ChangeTypeSequence(values, conversionType, conversionCulture, ignoreValueCase); + return isFlag + ? ChangeTypeFlagCounter(values, conversionType, conversionCulture, ignoreValueCase) + : scalar + ? ChangeTypeScalar(values.Last(), conversionType, conversionCulture, ignoreValueCase) + : ChangeTypeSequence(values, conversionType, conversionCulture, ignoreValueCase); } private static Maybe ChangeTypeSequence(IEnumerable values, Type conversionType, CultureInfo conversionCulture, bool ignoreValueCase) @@ -46,6 +48,14 @@ private static Maybe ChangeTypeScalar(string value, Type conversionType, return result.ToMaybe(); } + private static Maybe ChangeTypeFlagCounter(IEnumerable values, Type conversionType, CultureInfo conversionCulture, bool ignoreValueCase) + { + var converted = values.Select(value => ChangeTypeScalar(value, typeof(bool), conversionCulture, ignoreValueCase)); + return converted.Any(maybe => maybe.MatchNothing()) + ? Maybe.Nothing() + : Maybe.Just((object)converted.Count(value => value.IsJust())); + } + private static object ConvertString(string value, Type type, CultureInfo conversionCulture) { try @@ -111,6 +121,7 @@ private static Result ChangeTypeScalarImpl(string value, Type } }; + if (conversionType.IsCustomStruct()) return Result.Try(makeType); return Result.Try( conversionType.IsPrimitiveEx() || ReflectionHelper.IsFSharpOptionType(conversionType) ? changeType @@ -128,11 +139,20 @@ private static object ToEnum(this string value, Type conversionType, bool ignore { throw new FormatException(); } - if (Enum.IsDefined(conversionType, parsedValue)) + if (IsDefinedEx(parsedValue)) { return parsedValue; } throw new FormatException(); } + + private static bool IsDefinedEx(object enumValue) + { + char firstChar = enumValue.ToString()[0]; + if (Char.IsDigit(firstChar) || firstChar == '-') + return false; + + return true; + } } -} \ No newline at end of file +} diff --git a/src/CommandLine/Core/Verb.cs b/src/CommandLine/Core/Verb.cs index 2fb6674d..48e9427c 100644 --- a/src/CommandLine/Core/Verb.cs +++ b/src/CommandLine/Core/Verb.cs @@ -9,38 +9,36 @@ namespace CommandLine.Core { sealed class Verb { - private readonly string name; - private readonly string helpText; - private readonly bool hidden; - - public Verb(string name, string helpText, bool hidden = false) + public Verb(string name, string helpText, bool hidden, bool isDefault, string[] aliases) { - this.name = name ?? throw new ArgumentNullException(nameof(name)); - this.helpText = helpText ?? throw new ArgumentNullException(nameof(helpText)); - this.hidden = hidden; + if (string.IsNullOrWhiteSpace(name)) + throw new ArgumentNullException(nameof(name)); + Name = name; + + HelpText = helpText ?? throw new ArgumentNullException(nameof(helpText)); + Hidden = hidden; + IsDefault = isDefault; + Aliases = aliases ?? new string[0]; } - public string Name - { - get { return name; } - } + public string Name { get; private set; } - public string HelpText - { - get { return helpText; } - } + public string HelpText { get; private set; } - public bool Hidden - { - get { return hidden; } - } + public bool Hidden { get; private set; } + + public bool IsDefault { get; private set; } + + public string[] Aliases { get; private set; } public static Verb FromAttribute(VerbAttribute attribute) { return new Verb( attribute.Name, attribute.HelpText, - attribute.Hidden + attribute.Hidden, + attribute.IsDefault, + attribute.Aliases ); } @@ -54,4 +52,4 @@ select Tuple.Create( type); } } -} \ No newline at end of file +} diff --git a/src/CommandLine/Error.cs b/src/CommandLine/Error.cs index b1bc3e94..21c2fdcd 100644 --- a/src/CommandLine/Error.cs +++ b/src/CommandLine/Error.cs @@ -1,6 +1,8 @@ // Copyright 2005-2015 Giacomo Stelluti Scala & Contributors. All rights reserved. See License.md in the project root for license information. using System; +using System.Collections.Generic; +using System.Linq; namespace CommandLine { @@ -64,12 +66,25 @@ public enum ErrorType /// /// Value of type. /// - SetValueExceptionError - VersionRequestedError, + + SetValueExceptionError, /// /// Value of type. /// - InvalidAttributeConfigurationError + InvalidAttributeConfigurationError, + /// + /// Value of type. + /// + MissingGroupOptionError, + /// + /// Value of type. + /// + GroupOptionAmbiguityError, + /// + /// Value of type. + /// + MultipleDefaultVerbsError + } /// @@ -209,7 +224,7 @@ public override bool Equals(object obj) /// A hash code for the current . public override int GetHashCode() { - return new {Tag, StopsProcessing, Token}.GetHashCode(); + return new { Tag, StopsProcessing, Token }.GetHashCode(); } /// @@ -288,7 +303,7 @@ public override bool Equals(object obj) /// A hash code for the current . public override int GetHashCode() { - return new {Tag, StopsProcessing, NameInfo}.GetHashCode(); + return new { Tag, StopsProcessing, NameInfo }.GetHashCode(); } /// @@ -511,6 +526,87 @@ public object Value { get { return value; } } + } + + /// + /// Models an error generated when an invalid token is detected. + /// + public sealed class InvalidAttributeConfigurationError : Error + { + public const string ErrorMessage = "Check if Option or Value attribute values are set properly for the given type."; + + internal InvalidAttributeConfigurationError() + : base(ErrorType.InvalidAttributeConfigurationError, true) + { + } + } + + public sealed class MissingGroupOptionError : Error, IEquatable, IEquatable + { + public const string ErrorMessage = "At least one option in a group must have value."; + + private readonly string group; + private readonly IEnumerable names; + + internal MissingGroupOptionError(string group, IEnumerable names) + : base(ErrorType.MissingGroupOptionError) + { + this.group = group; + this.names = names; + } + + public string Group + { + get { return group; } + } + + 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) + { } } -} \ No newline at end of file +} diff --git a/src/CommandLine/ErrorExtensions.cs b/src/CommandLine/ErrorExtensions.cs index 2ffe629d..edd03478 100644 --- a/src/CommandLine/ErrorExtensions.cs +++ b/src/CommandLine/ErrorExtensions.cs @@ -23,5 +23,6 @@ public static IEnumerable OnlyMeaningfulOnes(this IEnumerable erro .Where(e => !(e.Tag == ErrorType.UnknownOptionError && ((UnknownOptionError)e).Token.EqualsOrdinalIgnoreCase("help"))); } + } } diff --git a/src/CommandLine/HelpTextExtensions.cs b/src/CommandLine/HelpTextExtensions.cs new file mode 100644 index 00000000..d0106de8 --- /dev/null +++ b/src/CommandLine/HelpTextExtensions.cs @@ -0,0 +1,45 @@ +// Copyright 2005-2015 Giacomo Stelluti Scala & Contributors. All rights reserved. See License.md in the project root for license information. +using System; +using System.IO; +using System.Collections.Generic; +using System.Linq; + +namespace CommandLine +{ + public static class HelpTextExtensions + { + /// + /// return true when errors contain HelpXXXError + /// + public static bool IsHelp(this IEnumerable errs) + { + if (errs.Any(x => x.Tag == ErrorType.HelpRequestedError || + x.Tag == ErrorType.HelpVerbRequestedError)) + return true; + //when AutoHelp=false in parser, help is disabled and Parser raise UnknownOptionError + return errs.Any(x => (x is UnknownOptionError ee ? ee.Token : "") == "help"); + } + + /// + /// return true when errors contain VersionXXXError + /// + public static bool IsVersion(this IEnumerable errs) + { + if (errs.Any(x => x.Tag == ErrorType.VersionRequestedError)) + return true; + //when AutoVersion=false in parser, Version is disabled and Parser raise UnknownOptionError + return errs.Any(x => (x is UnknownOptionError ee ? ee.Token : "") == "version"); + } + /// + /// redirect errs to Console.Error, and to Console.Out for help/version error + /// + public static TextWriter Output(this IEnumerable errs) + { + if (errs.IsHelp() || errs.IsVersion()) + return Console.Out; + return Console.Error; + } + } +} + + diff --git a/src/CommandLine/Infrastructure/Either.cs b/src/CommandLine/Infrastructure/CSharpx/Either.cs similarity index 91% rename from src/CommandLine/Infrastructure/Either.cs rename to src/CommandLine/Infrastructure/CSharpx/Either.cs index 71666dc2..3d985948 100644 --- a/src/CommandLine/Infrastructure/Either.cs +++ b/src/CommandLine/Infrastructure/CSharpx/Either.cs @@ -1,6 +1,5 @@ -//Use project level define(s) when referencing with Paket. -//#define CSX_EITHER_INTERNAL // Uncomment this to set visibility to internal. -//#define CSX_REM_MAYBE_FUNC // Uncomment this to remove dependency to Maybe.cs. +//#define CSX_EITHER_INTERNAL // Uncomment or define at build time to set accessibility to internal. +//#define CSX_REM_MAYBE_FUNC // Uncomment or define at build time to remove dependency to Maybe.cs. using System; @@ -133,8 +132,7 @@ public static Either Fail(string message) public static Either Bind(Either either, Func> func) { TRight right; - if (either.MatchRight(out right)) - { + if (either.MatchRight(out right)) { return func(right); } return Either.Left(either.GetLeft()); @@ -148,8 +146,7 @@ public static Either Bind(Either Map(Either either, Func func) { TRight right; - if (either.MatchRight(out right)) - { + if (either.MatchRight(out right)) { return Either.Right(func(right)); } return Either.Left(either.GetLeft()); @@ -164,8 +161,7 @@ public static Either Map(Either Bimap(Either either, Func mapLeft, Func mapRight) { TRight right; - if (either.MatchRight(out right)) - { + if (either.MatchRight(out right)) { return Either.Right(mapRight(right)); } return Either.Left(mapLeft(either.GetLeft())); @@ -196,9 +192,10 @@ public static Either SelectMany(this Eit public static TRight GetOrFail(Either either) { TRight value; - if (either.MatchRight(out value)) + if (either.MatchRight(out value)) { return value; - throw new ArgumentException("either", string.Format("The either value was Left {0}", either)); + } + throw new ArgumentException(nameof(either), string.Format("The either value was Left {0}", either)); } /// @@ -224,12 +221,10 @@ public static TRight GetRightOrDefault(Either eith /// public static Either Try(Func func) { - try - { + try { return new Right(func()); } - catch (Exception ex) - { + catch (Exception ex) { return new Left(ex); } } @@ -244,10 +239,9 @@ public static Either Cast(object obj) } #if !CSX_REM_MAYBE_FUNC - public static Either OfMaybe(Maybe maybe, TLeft left) + public static Either FromMaybe(Maybe maybe, TLeft left) { - if (maybe.Tag == MaybeType.Just) - { + if (maybe.Tag == MaybeType.Just) { return Either.Right(((Just)maybe).Value); } return Either.Left(left); @@ -269,8 +263,7 @@ static class EitherExtensions public static void Match(this Either either, Action ifLeft, Action ifRight) { TLeft left; - if (either.MatchLeft(out left)) - { + if (either.MatchLeft(out left)) { ifLeft(left); return; } @@ -319,4 +312,4 @@ public static bool IsRight(this Either either) return either.Tag == EitherType.Right; } } -} \ No newline at end of file +} diff --git a/src/CommandLine/Infrastructure/CSharpx/EnumerableExtensions.cs b/src/CommandLine/Infrastructure/CSharpx/EnumerableExtensions.cs new file mode 100644 index 00000000..b668eb46 --- /dev/null +++ b/src/CommandLine/Infrastructure/CSharpx/EnumerableExtensions.cs @@ -0,0 +1,463 @@ +//#define CSX_ENUM_INTERNAL // Uncomment or define at build time to set accessibility to internal. +//#define CSX_REM_MAYBE_FUNC // Uncomment or define at build time to remove dependency to Maybe.cs. +//#define CSX_REM_CRYPTORAND // Uncomment or define at build time to remove dependency to CryptoRandom.cs. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.Linq; +using System.Text; +using LinqEnumerable = System.Linq.Enumerable; + +namespace CSharpx +{ +#if !CSX_ENUM_INTERNAL + public +#endif + static class EnumerableExtensions + { +#if !CSX_REM_MAYBE_FUNC + /// + /// Safe function that returns Just(first element) or None. + /// + public static Maybe TryHead(this IEnumerable source) + { + using (var e = source.GetEnumerator()) { + return e.MoveNext() + ? Maybe.Just(e.Current) + : Maybe.Nothing(); + } + } + + /// + /// Turns an empty sequence to Nothing, otherwise Just(sequence). + /// + public static Maybe> ToMaybe(this IEnumerable source) + { + using (var e = source.GetEnumerator()) { + return e.MoveNext() + ? Maybe.Just(source) + : Maybe.Nothing>(); + } + } +#endif + + private static IEnumerable AssertCountImpl(IEnumerable source, + int count, Func errorSelector) + { + var collection = source as ICollection; // Optimization for collections + if (collection != null) + { + if (collection.Count != count) { + throw errorSelector(collection.Count.CompareTo(count), count); + } + return source; + } + + return ExpectingCountYieldingImpl(source, count, errorSelector); + } + + private static IEnumerable ExpectingCountYieldingImpl(IEnumerable source, + int count, Func errorSelector) + { + var iterations = 0; + foreach (var element in source) + { + iterations++; + if (iterations > count) { + throw errorSelector(1, count); + } + yield return element; + } + if (iterations != count) { + throw errorSelector(-1, count); + } + } + + /// + /// Returns the Cartesian product of two sequences by combining each element of the first set with each in the second + /// and applying the user=define projection to the pair. + /// + public static IEnumerable Cartesian(this IEnumerable first, IEnumerable second, Func resultSelector) + { + if (first == null) throw new ArgumentNullException(nameof(first)); + if (second == null) throw new ArgumentNullException(nameof(second)); + if (resultSelector == null) throw new ArgumentNullException(nameof(resultSelector)); + + return from element1 in first + from element2 in second // TODO buffer to avoid multiple enumerations + select resultSelector(element1, element2); + } + + /// + /// Prepends a single value to a sequence. + /// + public static IEnumerable Prepend(this IEnumerable source, TSource value) + { + if (source == null) throw new ArgumentNullException(nameof(source)); + + return LinqEnumerable.Concat(LinqEnumerable.Repeat(value, 1), source); + } + + /// + /// Returns a sequence consisting of the head element and the given tail elements. + /// + public static IEnumerable Concat(this T head, IEnumerable tail) + { + if (tail == null) throw new ArgumentNullException(nameof(tail)); + + return tail.Prepend(head); + } + + /// + /// Returns a sequence consisting of the head elements and the given tail element. + /// + public static IEnumerable Concat(this IEnumerable head, T tail) + { + if (head == null) throw new ArgumentNullException(nameof(head)); + + return LinqEnumerable.Concat(head, LinqEnumerable.Repeat(tail, 1)); + } + + /// + /// Excludes elements from a sequence starting at a given index + /// + /// The type of the elements of the sequence + public static IEnumerable Exclude(this IEnumerable sequence, int startIndex, int count) + { + if (sequence == null) throw new ArgumentNullException(nameof(sequence)); + if (startIndex < 0) throw new ArgumentOutOfRangeException(nameof(startIndex)); + if (count < 0) throw new ArgumentOutOfRangeException(nameof(count)); + + return ExcludeImpl(sequence, startIndex, count); + } + + private static IEnumerable ExcludeImpl(IEnumerable sequence, int startIndex, int count) + { + var index = -1; + var endIndex = startIndex + count; + using (var iter = sequence.GetEnumerator()) + { + // yield the first part of the sequence + while (iter.MoveNext() && ++index < startIndex) { + yield return iter.Current; + } + // skip the next part (up to count elements) + while (++index < endIndex && iter.MoveNext()) { + continue; + } + // yield the remainder of the sequence + while (iter.MoveNext()) { + yield return iter.Current; + } + } + } + + /// + /// Returns a sequence of + /// where the key is the zero-based index of the value in the source + /// sequence. + /// + public static IEnumerable> Index(this IEnumerable source) + { + return source.Index(0); + } + + /// + /// Returns a sequence of + /// where the key is the index of the value in the source sequence. + /// An additional parameter specifies the starting index. + /// + public static IEnumerable> Index(this IEnumerable source, int startIndex) + { + return source.Select((element, index) => new KeyValuePair(startIndex + index, element)); + } + + /// + /// Returns the result of applying a function to a sequence of + /// 1 element. + /// + public static TResult Fold(this IEnumerable source, Func folder) + { + return FoldImpl(source, 1, folder, null, null, null); + } + + /// + /// Returns the result of applying a function to a sequence of + /// 2 elements. + /// + public static TResult Fold(this IEnumerable source, Func folder) + { + return FoldImpl(source, 2, null, folder, null, null); + } + + /// + /// Returns the result of applying a function to a sequence of + /// 3 elements. + /// + public static TResult Fold(this IEnumerable source, Func folder) + { + return FoldImpl(source, 3, null, null, folder, null); + } + + /// + /// Returns the result of applying a function to a sequence of + /// 4 elements. + /// + public static TResult Fold(this IEnumerable source, Func folder) + { + return FoldImpl(source, 4, null, null, null, folder); + } + + static TResult FoldImpl(IEnumerable source, int count, + Func folder1, + Func folder2, + Func folder3, + Func folder4) + { + if (source == null) throw new ArgumentNullException(nameof(source)); + if (count == 1 && folder1 == null + || count == 2 && folder2 == null + || count == 3 && folder3 == null + || count == 4 && folder4 == null) + { // ReSharper disable NotResolvedInText + throw new ArgumentNullException("folder"); // ReSharper restore NotResolvedInText + } + + var elements = new T[count]; + foreach (var e in AssertCountImpl( + source.Index(), count, OnFolderSourceSizeErrorSelector)) { + elements[e.Key] = e.Value; + } + + switch (count) { + case 1: return folder1(elements[0]); + case 2: return folder2(elements[0], elements[1]); + case 3: return folder3(elements[0], elements[1], elements[2]); + case 4: return folder4(elements[0], elements[1], elements[2], elements[3]); + default: throw new NotSupportedException(); + } + } + + static readonly Func OnFolderSourceSizeErrorSelector = OnFolderSourceSizeError; + + static Exception OnFolderSourceSizeError(int cmp, int count) + { + var message = cmp < 0 + ? "Sequence contains too few elements when exactly {0} {1} expected" + : "Sequence contains too many elements when exactly {0} {1} expected"; + return new Exception(string.Format(message, count.ToString("N0"), count == 1 ? "was" : "were")); + } + + /// + /// Immediately executes the given action on each element in the source sequence. + /// + public static void ForEach(this IEnumerable source, Action action) + { + if (source == null) throw new ArgumentNullException(nameof(source)); + if (action == null) throw new ArgumentNullException(nameof(action)); + + foreach (var element in source) { + action(element); + } + } + + /// + /// Returns a sequence resulting from applying a function to each + /// element in the source sequence and its + /// predecessor, with the exception of the first element which is + /// only returned as the predecessor of the second element. + /// + public static IEnumerable Pairwise(this IEnumerable source, Func resultSelector) + { + if (source == null) throw new ArgumentNullException(nameof(source)); + if (resultSelector == null) throw new ArgumentNullException(nameof(resultSelector)); + + return PairwiseImpl(source, resultSelector); + } + + private static IEnumerable PairwiseImpl(this IEnumerable source, Func resultSelector) + { + Debug.Assert(source != null); + Debug.Assert(resultSelector != null); + + using (var e = source.GetEnumerator()) { + if (!e.MoveNext()) { + yield break; + } + + var previous = e.Current; + while (e.MoveNext()) { + yield return resultSelector(previous, e.Current); + previous = e.Current; + } + } + } + + /// + /// Creates a delimited string from a sequence of values. The + /// delimiter used depends on the current culture of the executing thread. + /// + public static string ToDelimitedString(this IEnumerable source) + { + return ToDelimitedString(source, null); + } + + /// + /// Creates a delimited string from a sequence of values and + /// a given delimiter. + /// + public static string ToDelimitedString(this IEnumerable source, string delimiter) + { + if (source == null) throw new ArgumentNullException(nameof(source)); + + return ToDelimitedStringImpl(source, delimiter, (sb, e) => sb.Append(e)); + } + + static string ToDelimitedStringImpl(IEnumerable source, string delimiter, Func append) + { + Debug.Assert(source != null); + Debug.Assert(append != null); + + delimiter = delimiter ?? CultureInfo.CurrentCulture.TextInfo.ListSeparator; + var sb = new StringBuilder(); + var i = 0; + + foreach (var value in source) { + if (i++ > 0) sb.Append(delimiter); + append(sb, value); + } + + return sb.ToString(); + } + + /// + /// Return everything except first element and throws exception if empty. + /// + public static IEnumerable Tail(this IEnumerable source) + { + using (var e = source.GetEnumerator()) { + if (e.MoveNext()) { + while (e.MoveNext()) { + yield return e.Current; + } + } + else { + throw new ArgumentException("Source sequence cannot be empty", nameof(source)); + } + } + } + + /// + /// Return everything except first element without throwing exception if empty. + /// + public static IEnumerable TailNoFail(this IEnumerable source) + { + using (var e = source.GetEnumerator()) + { + if (e.MoveNext()) { + while (e.MoveNext()) { + yield return e.Current; + } + } + } + } + + /// + /// Captures current state of a sequence. + /// + public static IEnumerable Memoize(this IEnumerable source) + { + return source.GetType().IsArray ? source : source.ToArray(); + } + + /// + /// Creates an immutable copy of a sequence. + /// + public static IEnumerable Materialize(this IEnumerable source) + { + if (source is MaterializedEnumerable || source.GetType().IsArray) { + return source; + } + return new MaterializedEnumerable(source); + } + + private class MaterializedEnumerable : IEnumerable + { + private readonly ICollection inner; + + public MaterializedEnumerable(IEnumerable enumerable) + { + inner = enumerable as ICollection ?? enumerable.ToArray(); + } + + public IEnumerator GetEnumerator() + { + return inner.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } + + /// + /// Selects a random element. + /// + public static T Choice(this IEnumerable source) + { +#if CSX_REM_CRYPTORAND + var index = new Random().Next(source.Count() - 1); +#else + var index = new CryptoRandom().Next(source.Count() - 1); +#endif + return source.ElementAt(index); + } + + /// + /// Takes an element and a sequence and `intersperses' that element between its elements. + /// + public static IEnumerable Intersperse(this IEnumerable source, T element) + { + if (element == null) throw new ArgumentNullException(nameof(element)); + + var count = source.Count(); + var last = count - 1; + for (var i = 0; i < count; i++) { + yield return source.ElementAt(i); + if (i != last) { + yield return element; + } + } + } + + /// + /// Flattens a sequence by one level. + /// + public static IEnumerable FlattenOnce(this IEnumerable> source) + { + foreach (var element in source) { + foreach (var subelement in element) { + yield return subelement; + } + } + } + + /// + /// Reduces a sequence of strings to a sequence of parts, splitted by space, + /// of each original string. + /// + public static IEnumerable FlattenOnce(this IEnumerable source) + { + foreach (var element in source) { + var parts = element.Split(); + foreach (var part in parts) { + yield return part; + } + } + } + } +} \ No newline at end of file diff --git a/src/CommandLine/Infrastructure/Maybe.cs b/src/CommandLine/Infrastructure/CSharpx/Maybe.cs similarity index 89% rename from src/CommandLine/Infrastructure/Maybe.cs rename to src/CommandLine/Infrastructure/CSharpx/Maybe.cs index 63451865..044bb681 100644 --- a/src/CommandLine/Infrastructure/Maybe.cs +++ b/src/CommandLine/Infrastructure/CSharpx/Maybe.cs @@ -1,6 +1,5 @@ -//Use project level define(s) when referencing with Paket. -//#define CSX_MAYBE_INTERNAL // Uncomment this to set visibility to internal. -//#define CSX_REM_EITHER_FUNC // Uncomment this to remove dependency to Either.cs. +//#define CSX_MAYBE_INTERNAL // Uncomment or define at build time set accessibility to internal. +//#define CSX_REM_EITHER_FUNC // Uncomment or define at build time to remove dependency to Either.cs. using System; using System.Collections.Generic; @@ -165,8 +164,7 @@ public static Maybe> Merge(Maybe first, Maybe seco { T1 value1; T2 value2; - if (first.MatchJust(out value1) && second.MatchJust(out value2)) - { + if (first.MatchJust(out value1) && second.MatchJust(out value2)) { return Maybe.Just(Tuple.Create(value1, value2)); } return Maybe.Nothing>(); @@ -176,10 +174,9 @@ public static Maybe> Merge(Maybe first, Maybe seco /// /// Maps Either Right value to Maybe Just, otherwise Maybe Nothing. /// - public static Maybe OfEither(Either either) + public static Maybe FromEither(Either either) { - if (either.Tag == EitherType.Right) - { + if (either.Tag == EitherType.Right) { return Maybe.Just(((Right)either).Value); } return Maybe.Nothing(); @@ -202,8 +199,7 @@ static class MaybeExtensions public static void Match(this Maybe maybe, Action ifJust, Action ifNothing) { T value; - if (maybe.MatchJust(out value)) - { + if (maybe.MatchJust(out value)) { ifJust(value); return; } @@ -217,8 +213,7 @@ public static void Match(this Maybe> maybe, Action { T1 value1; T2 value2; - if (maybe.MatchJust(out value1, out value2)) - { + if (maybe.MatchJust(out value1, out value2)) { ifJust(value1, value2); return; } @@ -231,8 +226,7 @@ public static void Match(this Maybe> maybe, Action public static bool MatchJust(this Maybe> maybe, out T1 value1, out T2 value2) { Tuple value; - if (maybe.MatchJust(out value)) - { + if (maybe.MatchJust(out value)) { value1 = value.Item1; value2 = value.Item2; return true; @@ -296,13 +290,12 @@ public static Maybe SelectMany( #region Do Semantic /// - /// If contans a value executes an delegate over it. + /// If contains a value executes an delegate over it. /// public static void Do(this Maybe maybe, Action action) { T value; - if (maybe.MatchJust(out value)) - { + if (maybe.MatchJust(out value)) { action(value); } } @@ -314,8 +307,7 @@ public static void Do(this Maybe> maybe, Action ac { T1 value1; T2 value2; - if (maybe.MatchJust(out value1, out value2)) - { + if (maybe.MatchJust(out value1, out value2)) { action(value1, value2); } } @@ -343,8 +335,7 @@ public static bool IsNothing(this Maybe maybe) public static T FromJust(this Maybe maybe) { T value; - if (maybe.MatchJust(out value)) - { + if (maybe.MatchJust(out value)) { return value; } return default(T); @@ -356,8 +347,7 @@ public static T FromJust(this Maybe maybe) public static T FromJustOrFail(this Maybe maybe, Exception exceptionToThrow = null) { T value; - if (maybe.MatchJust(out value)) - { + if (maybe.MatchJust(out value)) { return value; } throw exceptionToThrow ?? new ArgumentException("Value empty."); @@ -381,14 +371,21 @@ public static T2 MapValueOrDefault(this Maybe maybe, Func fu return maybe.MatchJust(out value1) ? func(value1) : noneValue; } + /// + /// If contains a values executes a mapping function over it, otherwise returns the value from . + /// + public static T2 MapValueOrDefault(this Maybe maybe, Func func, Func noneValueFactory) { + T1 value1; + return maybe.MatchJust(out value1) ? func(value1) : noneValueFactory(); + } + /// /// Returns an empty list when given or a singleton list when given a . /// public static IEnumerable ToEnumerable(this Maybe maybe) { T value; - if (maybe.MatchJust(out value)) - { + if (maybe.MatchJust(out value)) { return Enumerable.Empty().Concat(new[] { value }); } return Enumerable.Empty(); diff --git a/src/CommandLine/Infrastructure/EnumerableExtensions.cs b/src/CommandLine/Infrastructure/EnumerableExtensions.cs index fa9a7951..bdada5de 100644 --- a/src/CommandLine/Infrastructure/EnumerableExtensions.cs +++ b/src/CommandLine/Infrastructure/EnumerableExtensions.cs @@ -1,413 +1,68 @@ -//Use project level define(s) when referencing with Paket. -//#define CSX_ENUM_INTERNAL // Uncomment this to set visibility to internal. -//#define CSX_ENUM_REM_STD_FUNC // Uncomment this to remove standard functions. -//#define CSX_REM_MAYBE_FUNC // Uncomment this to remove dependency to Maybe.cs. -//#define CSX_REM_EXTRA_FUNC // Uncomment this to extra functions. +// Copyright 2005-2015 Giacomo Stelluti Scala & Contributors. All rights reserved. See License.md in the project root for license information. using System; -using System.Collections; using System.Collections.Generic; -using System.Diagnostics; -using System.Globalization; using System.Linq; -using System.Text; -using LinqEnumerable = System.Linq.Enumerable; -namespace CSharpx +namespace CommandLine.Infrastructure { -#if !CSX_ENUM_INTERNAL - public -#endif static class EnumerableExtensions { -#if !CSX_ENUM_REM_STD_FUNC - private static IEnumerable AssertCountImpl(IEnumerable source, - int count, Func errorSelector) - { - var collection = source as ICollection; // Optimization for collections - if (collection != null) - { - if (collection.Count != count) - throw errorSelector(collection.Count.CompareTo(count), count); - return source; - } - - return ExpectingCountYieldingImpl(source, count, errorSelector); - } - - private static IEnumerable ExpectingCountYieldingImpl(IEnumerable source, - int count, Func errorSelector) - { - var iterations = 0; - foreach (var element in source) - { - iterations++; - if (iterations > count) - { - throw errorSelector(1, count); - } - yield return element; - } - if (iterations != count) - { - throw errorSelector(-1, count); - } - } - - /// - /// Returns the Cartesian product of two sequences by combining each element of the first set with each in the second - /// and applying the user=define projection to the pair. - /// - public static IEnumerable Cartesian(this IEnumerable first, IEnumerable second, Func resultSelector) - { - if (first == null) throw new ArgumentNullException("first"); - if (second == null) throw new ArgumentNullException("second"); - if (resultSelector == null) throw new ArgumentNullException("resultSelector"); - - return from item1 in first - from item2 in second // TODO buffer to avoid multiple enumerations - select resultSelector(item1, item2); - } - - /// - /// Prepends a single value to a sequence. - /// - public static IEnumerable Prepend(this IEnumerable source, TSource value) - { - if (source == null) throw new ArgumentNullException("source"); - - return LinqEnumerable.Concat(LinqEnumerable.Repeat(value, 1), source); - } - - /// - /// Returns a sequence consisting of the head element and the given tail elements. - /// - public static IEnumerable Concat(this T head, IEnumerable tail) - { - if (tail == null) throw new ArgumentNullException("tail"); - - return tail.Prepend(head); - } - - /// - /// Returns a sequence consisting of the head elements and the given tail element. - /// - public static IEnumerable Concat(this IEnumerable head, T tail) - { - if (head == null) throw new ArgumentNullException("head"); - - return LinqEnumerable.Concat(head, LinqEnumerable.Repeat(tail, 1)); - } - - /// - /// Excludes elements from a sequence starting at a given index - /// - /// The type of the elements of the sequence - public static IEnumerable Exclude(this IEnumerable sequence, int startIndex, int count) - { - if (sequence == null) throw new ArgumentNullException("sequence"); - if (startIndex < 0) throw new ArgumentOutOfRangeException("startIndex"); - if (count < 0) throw new ArgumentOutOfRangeException("count"); - - return ExcludeImpl(sequence, startIndex, count); - } - - private static IEnumerable ExcludeImpl(IEnumerable sequence, int startIndex, int count) + public static int IndexOf(this IEnumerable source, Func predicate) { var index = -1; - var endIndex = startIndex + count; - using (var iter = sequence.GetEnumerator()) - { - // yield the first part of the sequence - while (iter.MoveNext() && ++index < startIndex) - yield return iter.Current; - // skip the next part (up to count items) - while (++index < endIndex && iter.MoveNext()) - continue; - // yield the remainder of the sequence - while (iter.MoveNext()) - yield return iter.Current; - } - } - - /// - /// Returns a sequence of - /// where the key is the zero-based index of the value in the source - /// sequence. - /// - public static IEnumerable> Index(this IEnumerable source) - { - return source.Index(0); - } - - /// - /// Returns a sequence of - /// where the key is the index of the value in the source sequence. - /// An additional parameter specifies the starting index. - /// - public static IEnumerable> Index(this IEnumerable source, int startIndex) - { - return source.Select((item, index) => new KeyValuePair(startIndex + index, item)); - } - - /// - /// Returns the result of applying a function to a sequence of - /// 1 element. - /// - public static TResult Fold(this IEnumerable source, Func folder) - { - return FoldImpl(source, 1, folder, null, null, null); - } - - /// - /// Returns the result of applying a function to a sequence of - /// 2 elements. - /// - public static TResult Fold(this IEnumerable source, Func folder) - { - return FoldImpl(source, 2, null, folder, null, null); - } - - /// - /// Returns the result of applying a function to a sequence of - /// 3 elements. - /// - public static TResult Fold(this IEnumerable source, Func folder) - { - return FoldImpl(source, 3, null, null, folder, null); - } - - /// - /// Returns the result of applying a function to a sequence of - /// 4 elements. - /// - public static TResult Fold(this IEnumerable source, Func folder) - { - return FoldImpl(source, 4, null, null, null, folder); - } - - static TResult FoldImpl(IEnumerable source, int count, - Func folder1, - Func folder2, - Func folder3, - Func folder4) - { - if (source == null) throw new ArgumentNullException("source"); - if (count == 1 && folder1 == null - || count == 2 && folder2 == null - || count == 3 && folder3 == null - || count == 4 && folder4 == null) - { // ReSharper disable NotResolvedInText - throw new ArgumentNullException("folder"); // ReSharper restore NotResolvedInText - } - - var elements = new T[count]; - foreach (var e in AssertCountImpl(source.Index(), count, OnFolderSourceSizeErrorSelector)) - elements[e.Key] = e.Value; - - switch (count) + foreach (var item in source) { - case 1: return folder1(elements[0]); - case 2: return folder2(elements[0], elements[1]); - case 3: return folder3(elements[0], elements[1], elements[2]); - case 4: return folder4(elements[0], elements[1], elements[2], elements[3]); - default: throw new NotSupportedException(); - } - } - - static readonly Func OnFolderSourceSizeErrorSelector = OnFolderSourceSizeError; - - static Exception OnFolderSourceSizeError(int cmp, int count) - { - var message = cmp < 0 - ? "Sequence contains too few elements when exactly {0} {1} expected." - : "Sequence contains too many elements when exactly {0} {1} expected."; - return new Exception(string.Format(message, count.ToString("N0"), count == 1 ? "was" : "were")); - } - - /// - /// Immediately executes the given action on each element in the source sequence. - /// - /// The type of the elements in the sequence - public static void ForEach(this IEnumerable source, Action action) - { - if (source == null) throw new ArgumentNullException("source"); - if (action == null) throw new ArgumentNullException("action"); - - foreach (var element in source) - { - action(element); - } - } - - /// - /// Returns a sequence resulting from applying a function to each - /// element in the source sequence and its - /// predecessor, with the exception of the first element which is - /// only returned as the predecessor of the second element. - /// - public static IEnumerable Pairwise(this IEnumerable source, Func resultSelector) - { - if (source == null) throw new ArgumentNullException("source"); - if (resultSelector == null) throw new ArgumentNullException("resultSelector"); - - return PairwiseImpl(source, resultSelector); - } - - private static IEnumerable PairwiseImpl(this IEnumerable source, Func resultSelector) - { - Debug.Assert(source != null); - Debug.Assert(resultSelector != null); - - using (var e = source.GetEnumerator()) - { - if (!e.MoveNext()) - yield break; - - var previous = e.Current; - while (e.MoveNext()) + index++; + if (predicate(item)) { - yield return resultSelector(previous, e.Current); - previous = e.Current; + break; } } + return index; } - /// - /// Creates a delimited string from a sequence of values. The - /// delimiter used depends on the current culture of the executing thread. - /// - public static string ToDelimitedString(this IEnumerable source) - { - return ToDelimitedString(source, null); - } - - /// - /// Creates a delimited string from a sequence of values and - /// a given delimiter. - /// - public static string ToDelimitedString(this IEnumerable source, string delimiter) + public static object ToUntypedArray(this IEnumerable value, Type type) { - if (source == null) throw new ArgumentNullException("source"); - - return ToDelimitedStringImpl(source, delimiter, (sb, e) => sb.Append(e)); + var array = Array.CreateInstance(type, value.Count()); + value.ToArray().CopyTo(array, 0); + return array; } - static string ToDelimitedStringImpl(IEnumerable source, string delimiter, Func append) + public static bool Empty(this IEnumerable source) { - Debug.Assert(source != null); - Debug.Assert(append != null); - - delimiter = delimiter ?? CultureInfo.CurrentCulture.TextInfo.ListSeparator; - var sb = new StringBuilder(); - var i = 0; - - foreach (var value in source) - { - if (i++ > 0) sb.Append(delimiter); - append(sb, value); - } - - return sb.ToString(); + return !source.Any(); } -#endif -#if !CSX_REM_MAYBE_FUNC /// - /// Safe function that returns Just(first element) or None. + /// Breaks a collection into groups of a specified size. /// - public static Maybe TryHead(this IEnumerable source) + /// A collection of . + /// The number of items each group shall contain. + /// An enumeration of T[]. + /// An incomplete group at the end of the source collection will be silently dropped. + public static IEnumerable Group(this IEnumerable source, int groupSize) { - using (var e = source.GetEnumerator()) + if (groupSize < 1) { - return e.MoveNext() - ? Maybe.Just(e.Current) - : Maybe.Nothing(); + throw new ArgumentOutOfRangeException(nameof(groupSize)); } - } - /// - /// Turns an empty sequence to Nothing, otherwise Just(sequence). - /// - public static Maybe> ToMaybe(this IEnumerable source) - { - using (var e = source.GetEnumerator()) - { - return e.MoveNext() - ? Maybe.Just(source) - : Maybe.Nothing>(); - } - } -#endif - -#if !CSX_REM_EXTRA_FUNC - /// - /// Return everything except first element and throws exception if empty. - /// - public static IEnumerable Tail(this IEnumerable source) - { - using (var e = source.GetEnumerator()) - { - if (e.MoveNext()) - while (e.MoveNext()) - yield return e.Current; - else - throw new ArgumentException("Source sequence cannot be empty.", "source"); - } - } - - /// - /// Return everything except first element without throwing exception if empty. - /// - public static IEnumerable TailNoFail(this IEnumerable source) - { - using (var e = source.GetEnumerator()) - { - if (e.MoveNext()) - while (e.MoveNext()) - yield return e.Current; - } - } + T[] group = new T[groupSize]; + int groupIndex = 0; - /// - /// Captures current state of a sequence. - /// - public static IEnumerable Memorize(this IEnumerable source) - { - return source.GetType().IsArray ? source : source.ToArray(); - } - - /// - /// Creates an immutable copy of a sequence. - /// - public static IEnumerable Materialize(this IEnumerable source) - { - if (source is MaterializedEnumerable || source.GetType().IsArray) - { - return source; - } - return new MaterializedEnumerable(source); - } - - private class MaterializedEnumerable : IEnumerable - { - private readonly ICollection inner; - - public MaterializedEnumerable(IEnumerable enumerable) + foreach (var item in source) { - inner = enumerable as ICollection ?? enumerable.ToArray(); - } + group[groupIndex++] = item; - public IEnumerator GetEnumerator() - { - return inner.GetEnumerator(); - } + if (groupIndex == groupSize) + { + yield return group; - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); + group = new T[groupSize]; + groupIndex = 0; + } } } -#endif } } \ No newline at end of file diff --git a/src/CommandLine/Infrastructure/EnumerableExtensions`1.cs b/src/CommandLine/Infrastructure/EnumerableExtensions`1.cs deleted file mode 100644 index 056fa152..00000000 --- a/src/CommandLine/Infrastructure/EnumerableExtensions`1.cs +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright 2005-2015 Giacomo Stelluti Scala & Contributors. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Linq; - -namespace CommandLine.Infrastructure -{ - static class EnumerableExtensions - { - public static int IndexOf(this IEnumerable source, Func predicate) - { - var index = -1; - foreach (var item in source) - { - index++; - if (predicate(item)) - { - break; - } - } - return index; - } - - public static object ToUntypedArray(this IEnumerable value, Type type) - { - var array = Array.CreateInstance(type, value.Count()); - value.ToArray().CopyTo(array, 0); - return array; - } - - public static bool Empty(this IEnumerable source) - { - return !source.Any(); - } - - /// - /// Breaks a collection into groups of a specified size. - /// - /// A collection of . - /// The number of items each group shall contain. - /// An enumeration of T[]. - /// An incomplete group at the end of the source collection will be silently dropped. - public static IEnumerable Group(this IEnumerable source, int groupSize) - { - if (groupSize < 1) - { - throw new ArgumentOutOfRangeException(nameof(groupSize)); - } - - T[] group = new T[groupSize]; - int groupIndex = 0; - - foreach (var item in source) - { - group[groupIndex++] = item; - - if (groupIndex == groupSize) - { - yield return group; - - group = new T[groupSize]; - groupIndex = 0; - } - } - } - } -} \ No newline at end of file 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/LocalizableAttributeProperty.cs b/src/CommandLine/Infrastructure/LocalizableAttributeProperty.cs new file mode 100644 index 00000000..b8bd1398 --- /dev/null +++ b/src/CommandLine/Infrastructure/LocalizableAttributeProperty.cs @@ -0,0 +1,56 @@ +using System; +using System.Reflection; + +namespace CommandLine.Infrastructure +{ + internal class LocalizableAttributeProperty + { + private string _propertyName; + private string _value; + private Type _type; + private PropertyInfo _localizationPropertyInfo; + + public LocalizableAttributeProperty(string propertyName) + { + _propertyName = propertyName; + } + + public string Value + { + get { return GetLocalizedValue(); } + set + { + _localizationPropertyInfo = null; + _value = value; + } + } + + public Type ResourceType + { + set + { + _localizationPropertyInfo = null; + _type = value; + } + } + + private string GetLocalizedValue() + { + if (String.IsNullOrEmpty(_value) || _type == null) + return _value; + if (_localizationPropertyInfo == null) + { + // Static class IsAbstract + if (!_type.IsVisible) + throw new ArgumentException($"Invalid resource type '{_type.FullName}'! {_type.Name} is not visible for the parser! Change resources 'Access Modifier' to 'Public'", _propertyName); + PropertyInfo propertyInfo = _type.GetProperty(_value, BindingFlags.Public | BindingFlags.GetProperty | BindingFlags.Static); + if (propertyInfo == null || !propertyInfo.CanRead || (propertyInfo.PropertyType != typeof(string) && !propertyInfo.PropertyType.CanCast())) + throw new ArgumentException($"Invalid resource property name! Localized value: {_value}", _propertyName); + _localizationPropertyInfo = propertyInfo; + } + + return _localizationPropertyInfo.GetValue(null, null).Cast(); + } + } + +} diff --git a/src/CommandLine/Infrastructure/ReflectionHelper.cs b/src/CommandLine/Infrastructure/ReflectionHelper.cs index 52bf8991..47fe70ea 100644 --- a/src/CommandLine/Infrastructure/ReflectionHelper.cs +++ b/src/CommandLine/Infrastructure/ReflectionHelper.cs @@ -45,14 +45,19 @@ public static Maybe GetAttribute() // Test support if (_overrides != null) { - return + return _overrides.ContainsKey(typeof(TAttribute)) ? Maybe.Just((TAttribute)_overrides[typeof(TAttribute)]) : - Maybe.Nothing< TAttribute>(); + Maybe.Nothing(); } var assembly = GetExecutingOrEntryAssembly(); + +#if NET40 + var attributes = assembly.GetCustomAttributes(typeof(TAttribute), false); +#else var attributes = assembly.GetCustomAttributes().ToArray(); +#endif return attributes.Length > 0 ? Maybe.Just((TAttribute)attributes[0]) @@ -80,15 +85,17 @@ public static bool IsFSharpOptionType(Type type) public static T CreateDefaultImmutableInstance(Type[] constructorTypes) { var t = typeof(T); - var ctor = t.GetTypeInfo().GetConstructor(constructorTypes); - var values = (from prms in ctor.GetParameters() - select prms.ParameterType.CreateDefaultForImmutable()).ToArray(); - return (T)ctor.Invoke(values); + return (T)CreateDefaultImmutableInstance(t, constructorTypes); } public static object CreateDefaultImmutableInstance(Type type, Type[] constructorTypes) { var ctor = type.GetTypeInfo().GetConstructor(constructorTypes); + if (ctor == null) + { + throw new InvalidOperationException($"Type {type.FullName} appears to be immutable, but no constructor found to accept values."); + } + var values = (from prms in ctor.GetParameters() select prms.ParameterType.CreateDefaultForImmutable()).ToArray(); return ctor.Invoke(values); @@ -96,7 +103,19 @@ public static object CreateDefaultImmutableInstance(Type type, Type[] constructo private static Assembly GetExecutingOrEntryAssembly() { - return Assembly.GetEntryAssembly(); + //resolve issues of null EntryAssembly in Xunit Test #392,424,389 + //return Assembly.GetEntryAssembly(); + return Assembly.GetEntryAssembly() ?? Assembly.GetCallingAssembly(); + } + + public static IEnumerable GetNamesOfEnum(Type t) + { + if (t.IsEnum) + return Enum.GetNames(t); + Type u = Nullable.GetUnderlyingType(t); + if (u != null && u.IsEnum) + return Enum.GetNames(u); + return Enumerable.Empty(); } } -} \ 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/Infrastructure/StringBuilderExtensions.cs b/src/CommandLine/Infrastructure/StringBuilderExtensions.cs index 6519b66f..ae4ccbc4 100644 --- a/src/CommandLine/Infrastructure/StringBuilderExtensions.cs +++ b/src/CommandLine/Infrastructure/StringBuilderExtensions.cs @@ -113,5 +113,39 @@ public static int TrailingSpaces(this StringBuilder builder) } return c; } + + /// + /// Indicates whether the string value of a + /// starts with the input parameter. Returns false if either + /// the StringBuilder or input string is null or empty. + /// + /// The to test. + /// The to look for. + /// + public static bool SafeStartsWith(this StringBuilder builder, string s) + { + if (string.IsNullOrEmpty(s)) + return false; + + return builder?.Length >= s.Length + && builder.ToString(0, s.Length) == s; + } + + /// + /// Indicates whether the string value of a + /// ends with the input parameter. Returns false if either + /// the StringBuilder or input string is null or empty. + /// + /// The to test. + /// The to look for. + /// + public static bool SafeEndsWith(this StringBuilder builder, string s) + { + if (string.IsNullOrEmpty(s)) + return false; + + return builder?.Length >= s.Length + && builder.ToString(builder.Length - s.Length, s.Length) == s; + } } -} \ No newline at end of file +} diff --git a/src/CommandLine/Infrastructure/StringExtensions.cs b/src/CommandLine/Infrastructure/StringExtensions.cs index 7bfab66a..db8aa0bd 100644 --- a/src/CommandLine/Infrastructure/StringExtensions.cs +++ b/src/CommandLine/Infrastructure/StringExtensions.cs @@ -73,5 +73,23 @@ public static bool ToBoolean(this string value) { return value.Equals("true", StringComparison.OrdinalIgnoreCase); } + + public static bool ToBooleanLoose(this string value) + { + if ((string.IsNullOrEmpty(value)) || + (value == "0") || + (value.Equals("f", StringComparison.OrdinalIgnoreCase)) || + (value.Equals("n", StringComparison.OrdinalIgnoreCase)) || + (value.Equals("no", StringComparison.OrdinalIgnoreCase)) || + (value.Equals("off", StringComparison.OrdinalIgnoreCase)) || + (value.Equals("false", StringComparison.OrdinalIgnoreCase))) + { + return false; + } + else + { + return true; + } + } } -} \ No newline at end of file +} diff --git a/src/CommandLine/IntrospectionExtensions.cs b/src/CommandLine/IntrospectionExtensions.cs new file mode 100644 index 00000000..8e4c64ea --- /dev/null +++ b/src/CommandLine/IntrospectionExtensions.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CommandLine +{ +#if NET40 + + internal static class IntrospectionExtensions + { + public static Type GetTypeInfo(this Type type) + { + return type; + } + } +#endif +} + diff --git a/src/CommandLine/OptionAttribute.cs b/src/CommandLine/OptionAttribute.cs index 8ef6d63d..6ae51dac 100644 --- a/src/CommandLine/OptionAttribute.cs +++ b/src/CommandLine/OptionAttribute.cs @@ -1,8 +1,9 @@ // Copyright 2005-2015 Giacomo Stelluti Scala & Contributors. All rights reserved. See License.md in the project root for license information. -using System; using CommandLine.Infrastructure; +using System; + namespace CommandLine { /// @@ -14,7 +15,9 @@ public sealed class OptionAttribute : BaseAttribute private readonly string longName; private readonly string shortName; private string setName; + private bool flagCounter; private char separator; + private string group=string.Empty; private OptionAttribute(string shortName, string longName) : base() { @@ -94,14 +97,33 @@ public string SetName } } + /// + /// If true, this is an int option that counts how many times a flag was set (e.g. "-v -v -v" or "-vvv" would return 3). + /// The property must be of type int (signed 32-bit integer). + /// + public bool FlagCounter + { + get { return flagCounter; } + set { flagCounter = value; } + } + /// /// When applying attribute to target properties, /// it allows you to split an argument and consume its content as a sequence. /// public char Separator { - get { return separator ; } + get { return separator; } set { separator = value; } } + + /// + /// Gets or sets the option group name. When one or more options are grouped, at least one of them should have value. Required rules are ignored. + /// + public string Group + { + get { return group; } + set { group = value; } + } } -} \ No newline at end of file +} diff --git a/src/CommandLine/Parser.cs b/src/CommandLine/Parser.cs index 8f4bd049..4301aa52 100644 --- a/src/CommandLine/Parser.cs +++ b/src/CommandLine/Parser.cs @@ -101,6 +101,7 @@ public ParserResult ParseArguments(IEnumerable args) settings.ParsingCulture, settings.AutoHelp, settings.AutoVersion, + settings.AllowMultiInstance, HandleUnknownArguments(settings.IgnoreUnknownArguments)), settings); } @@ -116,7 +117,6 @@ public ParserResult ParseArguments(IEnumerable args) /// and a sequence of . /// Thrown if one or more arguments are null. public ParserResult ParseArguments(Func factory, IEnumerable args) - where T : new() { if (factory == null) throw new ArgumentNullException("factory"); if (!typeof(T).IsMutable()) throw new ArgumentException("factory"); @@ -132,6 +132,7 @@ public ParserResult ParseArguments(Func factory, IEnumerable ar settings.ParsingCulture, settings.AutoHelp, settings.AutoVersion, + settings.AllowMultiInstance, HandleUnknownArguments(settings.IgnoreUnknownArguments)), settings); } @@ -164,6 +165,7 @@ public ParserResult ParseArguments(IEnumerable args, params Type settings.ParsingCulture, settings.AutoHelp, settings.AutoVersion, + settings.AllowMultiInstance, HandleUnknownArguments(settings.IgnoreUnknownArguments)), settings); } @@ -183,8 +185,13 @@ private static Result, Error> Tokenize( IEnumerable optionSpecs, ParserSettings settings) { - return - Tokenizer.ConfigureTokenizer( + return settings.GetoptMode + ? GetoptTokenizer.ConfigureTokenizer( + settings.NameComparer, + settings.IgnoreUnknownArguments, + settings.EnableDashDash, + settings.PosixlyCorrect)(arguments, optionSpecs) + : Tokenizer.ConfigureTokenizer( settings.NameComparer, settings.IgnoreUnknownArguments, settings.EnableDashDash)(arguments, optionSpecs); @@ -229,4 +236,4 @@ private void Dispose(bool disposing) } } } -} \ No newline at end of file +} diff --git a/src/CommandLine/ParserResult.cs b/src/CommandLine/ParserResult.cs index 20761ada..c7c9c833 100644 --- a/src/CommandLine/ParserResult.cs +++ b/src/CommandLine/ParserResult.cs @@ -9,7 +9,7 @@ namespace CommandLine public sealed class TypeInfo { private readonly Type current; - private readonly IEnumerable choices; + private readonly IEnumerable choices; private TypeInfo(Type current, IEnumerable choices) { @@ -64,10 +64,20 @@ public abstract class ParserResult private readonly ParserResultType tag; private readonly TypeInfo typeInfo; - internal ParserResult(ParserResultType tag, TypeInfo typeInfo) + internal ParserResult(IEnumerable errors, TypeInfo typeInfo) { - this.tag = tag; - this.typeInfo = typeInfo; + this.tag = ParserResultType.NotParsed; + this.typeInfo = typeInfo ?? TypeInfo.Create(typeof(T)); + Errors = errors ?? new Error[0]; + Value = default; + } + + internal ParserResult(T value, TypeInfo typeInfo) + { + Value = value ?? throw new ArgumentNullException(nameof(value)); + this.tag = ParserResultType.Parsed; + this.typeInfo = typeInfo ?? TypeInfo.Create(value.GetType()); + Errors = new Error[0]; } /// @@ -82,6 +92,16 @@ public TypeInfo TypeInfo { get { return typeInfo; } } + + /// + /// Gets the instance with parsed values. If one or more errors occures, is returned. + /// + public T Value { get; } + + /// + /// Gets the sequence of parsing errors. If there are no errors, then an empty IEnumerable is returned. + /// + public IEnumerable Errors { get; } } /// @@ -90,12 +110,9 @@ public TypeInfo TypeInfo /// The type with attributes that define the syntax of parsing rules. public sealed class Parsed : ParserResult, IEquatable> { - private readonly T value; - internal Parsed(T value, TypeInfo typeInfo) - : base(ParserResultType.Parsed, typeInfo) + : base(value, typeInfo) { - this.value = value; } internal Parsed(T value) @@ -103,13 +120,6 @@ internal Parsed(T value) { } - /// - /// Gets the instance with parsed values. - /// - public T Value - { - get { return value; } - } /// /// Determines whether the specified is equal to the current . @@ -118,8 +128,7 @@ public T Value /// true if the specified is equal to the current ; otherwise, false. public override bool Equals(object obj) { - var other = obj as Parsed; - if (other != null) + if (obj is Parsed other) { return Equals(other); } @@ -159,21 +168,12 @@ public bool Equals(Parsed other) /// The type with attributes that define the syntax of parsing rules. public sealed class NotParsed : ParserResult, IEquatable> { - private readonly IEnumerable errors; internal NotParsed(TypeInfo typeInfo, IEnumerable errors) - : base(ParserResultType.NotParsed, typeInfo) + : base(errors, typeInfo) { - this.errors = errors; } - /// - /// Gets the sequence of parsing errors. - /// - public IEnumerable Errors - { - get { return errors; } - } /// /// Determines whether the specified is equal to the current . @@ -182,8 +182,7 @@ public IEnumerable Errors /// true if the specified is equal to the current ; otherwise, false. public override bool Equals(object obj) { - var other = obj as NotParsed; - if (other != null) + if (obj is NotParsed other) { return Equals(other); } diff --git a/src/CommandLine/ParserResultExtensionsAsync.cs b/src/CommandLine/ParserResultExtensionsAsync.cs new file mode 100644 index 00000000..d58d769f --- /dev/null +++ b/src/CommandLine/ParserResultExtensionsAsync.cs @@ -0,0 +1,65 @@ +// Copyright 2005-2015 Giacomo Stelluti Scala & Contributors. All rights reserved. See License.md in the project root for license information. +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace CommandLine +{ + public static partial class ParserResultExtensions + { +#if !NET40 + /// + /// Executes asynchronously if contains + /// parsed values. + /// + /// Type of the target instance built with parsed value. + /// An instance. + /// The to execute. + /// The same instance as a instance. + public static async Task> WithParsedAsync(this ParserResult result, Func action) + { + if (result is Parsed parsed) + { + await action(parsed.Value); + } + return result; + } + + /// + /// Executes asynchronously if parsed values are of . + /// + /// Type of the target instance built with parsed value. + /// An verb result instance. + /// The to execute. + /// The same instance as a instance. + public static async Task> WithParsedAsync(this ParserResult result, Func action) + { + if (result is Parsed parsed) + { + if (parsed.Value is T value) + { + await action(value); + } + } + return result; + } + + /// + /// Executes asynchronously if lacks + /// parsed values and contains errors. + /// + /// Type of the target instance built with parsed value. + /// An instance. + /// The delegate to execute. + /// The same instance as a instance. + public static async Task> WithNotParsedAsync(this ParserResult result, Func, Task> action) + { + if (result is NotParsed notParsed) + { + await action(notParsed.Errors); + } + return result; + } +#endif + } +} diff --git a/src/CommandLine/ParserSettings.cs b/src/CommandLine/ParserSettings.cs index 7b182a22..5ed73f30 100644 --- a/src/CommandLine/ParserSettings.cs +++ b/src/CommandLine/ParserSettings.cs @@ -5,6 +5,7 @@ using System.IO; using CommandLine.Infrastructure; +using CSharpx; namespace CommandLine { @@ -23,8 +24,11 @@ public class ParserSettings : IDisposable private bool autoHelp; private bool autoVersion; private CultureInfo parsingCulture; - private bool enableDashDash; + private Maybe enableDashDash; private int maximumDisplayWidth; + private Maybe allowMultiInstance; + private bool getoptMode; + private Maybe posixlyCorrect; /// /// Initializes a new instance of the class. @@ -36,18 +40,33 @@ public ParserSettings() autoHelp = true; autoVersion = true; parsingCulture = CultureInfo.InvariantCulture; + maximumDisplayWidth = GetWindowWidth(); + getoptMode = false; + enableDashDash = Maybe.Nothing(); + allowMultiInstance = Maybe.Nothing(); + posixlyCorrect = Maybe.Nothing(); + } + + private int GetWindowWidth() + { + +#if !NET40 + if (Console.IsOutputRedirected) return DefaultMaximumLength; +#endif + var width = 1; try { - maximumDisplayWidth = Console.WindowWidth; - if (maximumDisplayWidth < 1) + width = Console.WindowWidth; + if (width < 1) { - maximumDisplayWidth = DefaultMaximumLength; + width = DefaultMaximumLength; } - } - catch (IOException) + } + catch (Exception e) when (e is IOException || e is PlatformNotSupportedException || e is ArgumentOutOfRangeException) { - maximumDisplayWidth = DefaultMaximumLength; + width = DefaultMaximumLength; } + return width; } /// @@ -147,11 +166,12 @@ public bool AutoVersion /// /// Gets or sets a value indicating whether enable double dash '--' syntax, /// that forces parsing of all subsequent tokens as values. + /// If GetoptMode is true, this defaults to true, but can be turned off by explicitly specifying EnableDashDash = false. /// public bool EnableDashDash { - get { return enableDashDash; } - set { PopsicleSetter.Set(Consumed, ref enableDashDash, value); } + get => enableDashDash.MatchJust(out bool value) ? value : getoptMode; + set => PopsicleSetter.Set(Consumed, ref enableDashDash, Maybe.Just(value)); } /// @@ -163,6 +183,35 @@ public int MaximumDisplayWidth set { maximumDisplayWidth = value; } } + /// + /// Gets or sets a value indicating whether options are allowed to be specified multiple times. + /// If GetoptMode is true, this defaults to true, but can be turned off by explicitly specifying AllowMultiInstance = false. + /// + public bool AllowMultiInstance + { + get => allowMultiInstance.MatchJust(out bool value) ? value : getoptMode; + set => PopsicleSetter.Set(Consumed, ref allowMultiInstance, Maybe.Just(value)); + } + + /// + /// Whether strict getopt-like processing is applied to option values; if true, AllowMultiInstance and EnableDashDash will default to true as well. + /// + public bool GetoptMode + { + get => getoptMode; + set => PopsicleSetter.Set(Consumed, ref getoptMode, value); + } + + /// + /// Whether getopt-like processing should follow the POSIX rules (the equivalent of using the "+" prefix in the C getopt() call). + /// If not explicitly set, will default to false unless the POSIXLY_CORRECT environment variable is set, in which case it will default to true. + /// + public bool PosixlyCorrect + { + get => posixlyCorrect.MapValueOrDefault(val => val, () => Environment.GetEnvironmentVariable("POSIXLY_CORRECT").ToBooleanLoose()); + set => PopsicleSetter.Set(Consumed, ref posixlyCorrect, Maybe.Just(value)); + } + internal StringComparer NameComparer { get diff --git a/src/CommandLine/Properties/AssemblyInfo.cs b/src/CommandLine/Properties/AssemblyInfo.cs index 4b4532b3..1dc94d20 100644 --- a/src/CommandLine/Properties/AssemblyInfo.cs +++ b/src/CommandLine/Properties/AssemblyInfo.cs @@ -2,4 +2,4 @@ using System.Runtime.CompilerServices; -[assembly: InternalsVisibleTo("CommandLine.Tests, PublicKey=002400000480000094000000060200000024000052534131000400000100010015eb7571d696c075627830f9468969103bc35764467bdbccfc0850f2fbe6913ee233d5d7cf3bbcb870fd42e6a8cc846d706b5cef35389e5b90051991ee8b6ed73ee1e19f108e409be69af6219b2e31862405f4b8ba101662fbbb54ba92a35d97664fe65c90c2bebd07aef530b01b709be5ed01b7e4d67a6b01c8643e42a20fb4")] +[assembly: InternalsVisibleTo("CommandLine.Tests, PublicKey=002400000480000094000000060200000024000052534131000400000100010009ab24ef889cd26bf46f7eaeda28e0fa5c04c50c93c6e121337b154bca0a1fd58ac6cb86195b709c2120f482730ced04a0e167a5758e56d3464bfabafe022b31510c39a61968fde795480dd60f6a396015c5f69a942074a3f4654b6dd66d0c63608bea78bdf96b35b1b48bb75741c2caad1f70579f286f1dbc2c560511c648d2")] diff --git a/src/CommandLine/Text/CopyrightInfo.cs b/src/CommandLine/Text/CopyrightInfo.cs index 3fd2b6a8..c8bc3593 100644 --- a/src/CommandLine/Text/CopyrightInfo.cs +++ b/src/CommandLine/Text/CopyrightInfo.cs @@ -27,11 +27,11 @@ public class CopyrightInfo /// /// An empty object used for initialization. /// - public static CopyrightInfo Empty + public static CopyrightInfo Empty { get { - return new CopyrightInfo("author", 1); + return new CopyrightInfo("author", DateTime.Now.Year); } } @@ -115,12 +115,13 @@ public static CopyrightInfo Default case MaybeType.Just: return new CopyrightInfo(copyrightAttr.FromJustOrFail()); default: - // if no copyright attribute exist but a company attribute does, use it as copyright holder - return new CopyrightInfo( - ReflectionHelper.GetAttribute().FromJustOrFail( - new InvalidOperationException("CopyrightInfo::Default requires that you define AssemblyCopyrightAttribute or AssemblyCompanyAttribute.") - ).Company, - DateTime.Now.Year); + var companyAttr = ReflectionHelper.GetAttribute(); + return companyAttr.IsNothing() + //if both copyrightAttr and companyAttr aren't available in Assembly,don't fire Exception + ? Empty + // if no copyright attribute exist but a company attribute does, use it as copyright holder + : new CopyrightInfo(companyAttr.FromJust().Company, DateTime.Now.Year); + } } } @@ -192,4 +193,4 @@ protected virtual string FormatYears(int[] years) return yearsPart.ToString(); } } -} \ No newline at end of file +} diff --git a/src/CommandLine/Text/HelpText.cs b/src/CommandLine/Text/HelpText.cs index cd11a475..f5e9a7b9 100644 --- a/src/CommandLine/Text/HelpText.cs +++ b/src/CommandLine/Text/HelpText.cs @@ -1,15 +1,17 @@ // Copyright 2005-2015 Giacomo Stelluti Scala & Contributors. All rights reserved. See License.md in the project root for license information. +using CommandLine.Core; +using CommandLine.Infrastructure; + +using CSharpx; + using System; using System.Collections; using System.Collections.Generic; using System.IO; -using System.Text; using System.Linq; using System.Reflection; -using CommandLine.Infrastructure; -using CommandLine.Core; -using CSharpx; +using System.Text; namespace CommandLine.Text { @@ -17,10 +19,86 @@ namespace CommandLine.Text /// Provides means to format an help screen. /// You can assign it in place of a instance. /// + + + + public struct ComparableOption + { + public bool Required; + public bool IsOption; + public bool IsValue; + public string LongName; + public string ShortName; + public int Index; + } + public class HelpText { + + #region ordering + + ComparableOption ToComparableOption(Specification spec, int index) + { + OptionSpecification option = spec as OptionSpecification; + ValueSpecification value = spec as ValueSpecification; + bool required = option?.Required ?? false; + + return new ComparableOption() + { + Required = required, + IsOption = option != null, + IsValue = value != null, + LongName = option?.LongName ?? value?.MetaName, + ShortName = option?.ShortName, + Index = index + }; + } + + + public Comparison OptionComparison { get; set; } = null; + + public static Comparison RequiredThenAlphaComparison = (ComparableOption attr1, ComparableOption attr2) => + { + if (attr1.IsOption && attr2.IsOption) + { + if (attr1.Required && !attr2.Required) + { + return -1; + } + else if (!attr1.Required && attr2.Required) + { + return 1; + } + + return String.Compare(attr1.LongName, attr2.LongName, StringComparison.Ordinal); + + } + else if (attr1.IsOption && attr2.IsValue) + { + return -1; + } + else + { + return 1; + } + }; + + #endregion + private const int BuilderCapacity = 128; private const int DefaultMaximumLength = 80; // default console width + /// + /// The number of spaces between an option and its associated help text + /// + private const int OptionToHelpTextSeparatorWidth = 4; + /// + /// The width of the option prefix (either "--" or " " + /// + private const int OptionPrefixWidth = 2; + /// + /// The total amount of extra space that needs to accounted for when indenting Option help text + /// + private const int TotalOptionPadding = OptionToHelpTextSeparatorWidth + OptionPrefixWidth; private readonly StringBuilder preOptionsHelp; private readonly StringBuilder postOptionsHelp; private readonly SentenceBuilder sentenceBuilder; @@ -33,6 +111,7 @@ public class HelpText private bool addEnumValuesToHelpText; private bool autoHelp; private bool autoVersion; + private bool addNewLineBetweenHelpSections; /// /// Initializes a new instance of the class. @@ -182,6 +261,15 @@ public bool AdditionalNewLineAfterOption set { additionalNewLineAfterOption = value; } } + /// + /// Gets or sets a value indicating whether to add newlines between help sections. + /// + public bool AddNewLineBetweenHelpSections + { + get { return addNewLineBetweenHelpSections; } + set { addNewLineBetweenHelpSections = value; } + } + /// /// Gets or sets a value indicating whether to add the values of an enum after the description of the specification. /// @@ -257,11 +345,11 @@ public static HelpText AutoBuild( var errors = Enumerable.Empty(); + if (onError != null && parserResult.Tag == ParserResultType.NotParsed) { errors = ((NotParsed)parserResult).Errors; - - if (errors.OnlyMeaningfulOnes().Any()) + if (errors.IsHelp() || errors.OnlyMeaningfulOnes().Any()) auto = onError(auto); } @@ -275,7 +363,11 @@ public static HelpText AutoBuild( { var heading = auto.SentenceBuilder.UsageHeadingText(); if (heading.Length > 0) + { + if (auto.AddNewLineBetweenHelpSections) + heading = Environment.NewLine + heading; auto.AddPreOptionsLine(heading); + } } usageAttr.Do( @@ -297,7 +389,7 @@ public static HelpText AutoBuild( } /// - /// Creates a new instance of the class, + /// Creates a default instance of the class, /// automatically handling verbs or options scenario. /// /// The containing the instance that collected command line arguments parsed with class. @@ -308,6 +400,23 @@ public static HelpText AutoBuild( /// This feature is meant to be invoked automatically by the parser, setting the HelpWriter property /// of . public static HelpText AutoBuild(ParserResult parserResult, int maxDisplayWidth = DefaultMaximumLength) + { + return AutoBuild(parserResult, h => h, maxDisplayWidth); + } + + /// + /// Creates a custom instance of the class, + /// automatically handling verbs or options scenario. + /// + /// The containing the instance that collected command line arguments parsed with class. + /// A delegate used to customize the text block of reporting parsing errors text block. + /// The maximum width of the display. + /// + /// An instance of class. + /// + /// This feature is meant to be invoked automatically by the parser, setting the HelpWriter property + /// of . + public static HelpText AutoBuild(ParserResult parserResult, Func onError, int maxDisplayWidth = DefaultMaximumLength) { if (parserResult.Tag != ParserResultType.NotParsed) throw new ArgumentException("Excepting NotParsed type.", "parserResult"); @@ -315,16 +424,28 @@ public static HelpText AutoBuild(ParserResult parserResult, int maxDisplay var errors = ((NotParsed)parserResult).Errors; if (errors.Any(e => e.Tag == ErrorType.VersionRequestedError)) - return new HelpText(HeadingInfo.Default){MaximumDisplayWidth = maxDisplayWidth }.AddPreOptionsLine(Environment.NewLine); + return new HelpText($"{HeadingInfo.Default}{Environment.NewLine}") { MaximumDisplayWidth = maxDisplayWidth }.AddPreOptionsLine(Environment.NewLine); if (!errors.Any(e => e.Tag == ErrorType.HelpVerbRequestedError)) - return AutoBuild(parserResult, current => DefaultParsingErrorsHandler(parserResult, current), e => e, maxDisplayWidth: maxDisplayWidth); + return AutoBuild(parserResult, current => + { + onError?.Invoke(current); + return DefaultParsingErrorsHandler(parserResult, current); + }, e => e, maxDisplayWidth: maxDisplayWidth); var err = errors.OfType().Single(); - var pr = new NotParsed(TypeInfo.Create(err.Type), Enumerable.Empty()); + var pr = new NotParsed(TypeInfo.Create(err.Type), new Error[] { err }); return err.Matched - ? AutoBuild(pr, current => DefaultParsingErrorsHandler(pr, current), e => e, maxDisplayWidth: maxDisplayWidth) - : AutoBuild(parserResult, current => DefaultParsingErrorsHandler(parserResult, current), e => e, true, maxDisplayWidth); + ? AutoBuild(pr, current => + { + onError?.Invoke(current); + return DefaultParsingErrorsHandler(pr, current); + }, e => e, maxDisplayWidth: maxDisplayWidth) + : AutoBuild(parserResult, current => + { + onError?.Invoke(current); + return DefaultParsingErrorsHandler(parserResult, current); + }, e => e, true, maxDisplayWidth); } /// @@ -339,7 +460,6 @@ public static HelpText DefaultParsingErrorsHandler(ParserResult parserResu if (((NotParsed)parserResult).Errors.OnlyMeaningfulOnes().Empty()) return current; - var errors = RenderParsingErrorsTextAsLines(parserResult, current.SentenceBuilder.FormatError, current.SentenceBuilder.FormatMutuallyExclusiveSetErrors, @@ -443,6 +563,7 @@ public HelpText AddOptions(ParserResult result) return AddOptionsImpl( GetSpecificationsFromType(result.TypeInfo.Current), SentenceBuilder.RequiredWord(), + SentenceBuilder.OptionGroupWord(), MaximumDisplayWidth); } @@ -460,6 +581,7 @@ public HelpText AddVerbs(params Type[] types) return AddOptionsImpl( AdaptVerbsToSpecifications(types), SentenceBuilder.RequiredWord(), + SentenceBuilder.OptionGroupWord(), MaximumDisplayWidth); } @@ -476,6 +598,7 @@ public HelpText AddOptions(int maximumLength, ParserResult result) return AddOptionsImpl( GetSpecificationsFromType(result.TypeInfo.Current), SentenceBuilder.RequiredWord(), + SentenceBuilder.OptionGroupWord(), maximumLength); } @@ -494,6 +617,7 @@ public HelpText AddVerbs(int maximumLength, params Type[] types) return AddOptionsImpl( AdaptVerbsToSpecifications(types), SentenceBuilder.RequiredWord(), + SentenceBuilder.OptionGroupWord(), maximumLength); } @@ -537,7 +661,7 @@ public static IEnumerable RenderParsingErrorsTextAsLines( if (meaningfulErrors.Empty()) yield break; - foreach(var error in meaningfulErrors + foreach (var error in meaningfulErrors .Where(e => e.Tag != ErrorType.MutuallyExclusiveSetError)) { var line = new StringBuilder(indent.Spaces()) @@ -608,7 +732,7 @@ public static IEnumerable RenderUsageTextAsLines(ParserResult pars var styles = example.GetFormatStylesOrDefault(); foreach (var s in styles) { - var commandLine = new StringBuilder(2.Spaces()) + var commandLine = new StringBuilder(OptionPrefixWidth.Spaces()) .Append(appAlias) .Append(' ') .Append(Parser.Default.FormatCommandLine(example.Sample, @@ -617,6 +741,7 @@ public static IEnumerable RenderUsageTextAsLines(ParserResult pars config.PreferShortName = s.PreferShortName; config.GroupSwitches = s.GroupSwitches; config.UseEqualToken = s.UseEqualToken; + config.SkipDefault = s.SkipDefault; })); yield return commandLine.ToString(); } @@ -630,19 +755,40 @@ public static IEnumerable RenderUsageTextAsLines(ParserResult pars public override string ToString() { const int ExtraLength = 10; - return - new StringBuilder( - heading.SafeLength() + copyright.SafeLength() + preOptionsHelp.SafeLength() + - optionsHelp.SafeLength() + ExtraLength).Append(heading) - .AppendWhen(!string.IsNullOrEmpty(copyright), Environment.NewLine, copyright) - .AppendWhen(preOptionsHelp.Length > 0, Environment.NewLine, preOptionsHelp.ToString()) - .AppendWhen( - optionsHelp != null && optionsHelp.Length > 0, + + var sbLength = heading.SafeLength() + copyright.SafeLength() + preOptionsHelp.SafeLength() + + optionsHelp.SafeLength() + postOptionsHelp.SafeLength() + ExtraLength; + var result = new StringBuilder(sbLength); + + result.Append(heading) + .AppendWhen(!string.IsNullOrEmpty(copyright), + Environment.NewLine, + copyright) + .AppendWhen(preOptionsHelp.SafeLength() > 0, + NewLineIfNeededBefore(preOptionsHelp), + Environment.NewLine, + preOptionsHelp.ToString()) + .AppendWhen(optionsHelp.SafeLength() > 0, Environment.NewLine, Environment.NewLine, optionsHelp.SafeToString()) - .AppendWhen(postOptionsHelp.Length > 0, Environment.NewLine, postOptionsHelp.ToString()) - .ToString(); + .AppendWhen(postOptionsHelp.SafeLength() > 0, + NewLineIfNeededBefore(postOptionsHelp), + Environment.NewLine, + postOptionsHelp.ToString()); + + string NewLineIfNeededBefore(StringBuilder sb) + { + if (AddNewLineBetweenHelpSections + && result.Length > 0 + && !result.SafeEndsWith(Environment.NewLine) + && !sb.SafeStartsWith(Environment.NewLine)) + return Environment.NewLine; + else + return null; + } + + return result.ToString(); } internal static void AddLine(StringBuilder builder, string value, int maximumLength) @@ -665,37 +811,7 @@ internal static void AddLine(StringBuilder builder, string value, int maximumLen value = value.TrimEnd(); builder.AppendWhen(builder.Length > 0, Environment.NewLine); - do - { - var wordBuffer = 0; - var words = value.Split(' '); - for (var i = 0; i < words.Length; i++) - { - if (words[i].Length < (maximumLength - wordBuffer)) - { - builder.Append(words[i]); - wordBuffer += words[i].Length; - if ((maximumLength - wordBuffer) > 1 && i != words.Length - 1) - { - builder.Append(" "); - wordBuffer++; - } - } - else if (words[i].Length >= maximumLength && wordBuffer == 0) - { - builder.Append(words[i].Substring(0, maximumLength)); - wordBuffer = maximumLength; - break; - } - else - break; - } - value = value.Substring(Math.Min(wordBuffer, value.Length)); - builder.AppendWhen(value.Length > 0, Environment.NewLine); - } - while (value.Length > maximumLength); - - builder.Append(value); + builder.Append(TextWrapper.WrapAndIndentText(value, 0, maximumLength)); } private IEnumerable GetSpecificationsFromType(Type type) @@ -704,9 +820,9 @@ private IEnumerable GetSpecificationsFromType(Type type) var optionSpecs = specs .OfType(); if (autoHelp) - optionSpecs = optionSpecs.Concat(new [] { MakeHelpEntry() }); + optionSpecs = optionSpecs.Concat(new[] { MakeHelpEntry() }); if (autoVersion) - optionSpecs = optionSpecs.Concat(new [] { MakeVersionEntry() }); + optionSpecs = optionSpecs.Concat(new[] { MakeVersionEntry() }); var valueSpecs = specs .OfType() .OrderBy(v => v.Index); @@ -733,35 +849,59 @@ private static Maybe>> GetUsageFromTy private IEnumerable AdaptVerbsToSpecifications(IEnumerable types) { var optionSpecs = from verbTuple in Verb.SelectFromTypes(types) - select - OptionSpecification.NewSwitch( - string.Empty, - verbTuple.Item1.Name, - false, - verbTuple.Item1.HelpText, - string.Empty, - verbTuple.Item1.Hidden); + select + OptionSpecification.NewSwitch( + string.Empty, + verbTuple.Item1.Name.Concat(verbTuple.Item1.Aliases).ToDelimitedString(", "), + false, + verbTuple.Item1.IsDefault ? "(Default Verb) " + verbTuple.Item1.HelpText : verbTuple.Item1.HelpText, //Default verb + string.Empty, + verbTuple.Item1.Hidden); if (autoHelp) - optionSpecs = optionSpecs.Concat(new [] { MakeHelpEntry() }); + optionSpecs = optionSpecs.Concat(new[] { MakeHelpEntry() }); if (autoVersion) - optionSpecs = optionSpecs.Concat(new [] { MakeVersionEntry() }); + optionSpecs = optionSpecs.Concat(new[] { MakeVersionEntry() }); return optionSpecs; } private HelpText AddOptionsImpl( IEnumerable specifications, string requiredWord, + string optionGroupWord, int maximumLength) { var maxLength = GetMaxLength(specifications); + + optionsHelp = new StringBuilder(BuilderCapacity); - var remainingSpace = maximumLength - (maxLength + 6); + var remainingSpace = maximumLength - (maxLength + TotalOptionPadding); - specifications.ForEach( - option => - AddOption(requiredWord, maxLength, option, remainingSpace)); + if (OptionComparison != null) + { + int i = -1; + var comparables = specifications.ToList().Select(s => + { + i++; + return ToComparableOption(s, i); + }).ToList(); + comparables.Sort(OptionComparison); + + + foreach (var comparable in comparables) + { + Specification spec = specifications.ElementAt(comparable.Index); + AddOption(requiredWord, optionGroupWord, maxLength, spec, remainingSpace); + } + } + else + { + specifications.ForEach( + option => + AddOption(requiredWord, optionGroupWord, maxLength, option, remainingSpace)); + + } return this; } @@ -795,8 +935,23 @@ private HelpText AddPreOptionsLine(string value, int maximumLength) return this; } - private HelpText AddOption(string requiredWord, int maxLength, Specification specification, int widthOfHelpText) + private HelpText AddOption(string requiredWord, string optionGroupWord, int maxLength, Specification specification, int widthOfHelpText) { + OptionSpecification GetOptionGroupSpecification() + { + if (specification.Tag == SpecificationType.Option && + specification is OptionSpecification optionSpecification && + optionSpecification.Group.Length > 0 + ) + + + { + return optionSpecification; + } + + return null; + } + if (specification.Hidden) return this; @@ -809,7 +964,7 @@ private HelpText AddOption(string requiredWord, int maxLength, Specification spe optionsHelp .Append(name.Length < maxLength ? name.ToString().PadRight(maxLength) : name.ToString()) - .Append(" "); + .Append(OptionToHelpTextSeparatorWidth.Spaces()); var optionHelpText = specification.HelpText; @@ -819,46 +974,22 @@ private HelpText AddOption(string requiredWord, int maxLength, Specification spe specification.DefaultValue.Do( defaultValue => optionHelpText = "(Default: {0}) ".FormatInvariant(FormatDefaultValue(defaultValue)) + optionHelpText); - if (specification.Required) + var optionGroupSpecification = GetOptionGroupSpecification(); + + if (specification.Required && optionGroupSpecification == null) optionHelpText = "{0} ".FormatInvariant(requiredWord) + optionHelpText; - if (!string.IsNullOrEmpty(optionHelpText)) + if (optionGroupSpecification != null) { - do - { - var wordBuffer = 0; - var words = optionHelpText.Split(' '); - for (var i = 0; i < words.Length; i++) - { - if (words[i].Length < (widthOfHelpText - wordBuffer)) - { - optionsHelp.Append(words[i]); - wordBuffer += words[i].Length; - if ((widthOfHelpText - wordBuffer) > 1 && i != words.Length - 1) - { - optionsHelp.Append(" "); - wordBuffer++; - } - } - else if (words[i].Length >= widthOfHelpText && wordBuffer == 0) - { - optionsHelp.Append(words[i].Substring(0, widthOfHelpText)); - wordBuffer = widthOfHelpText; - break; - } - else - break; - } - - optionHelpText = optionHelpText.Substring(Math.Min(wordBuffer, optionHelpText.Length)).Trim(); - optionsHelp.AppendWhen(optionHelpText.Length > 0, Environment.NewLine, - new string(' ', maxLength + 6)); - } - while (optionHelpText.Length > widthOfHelpText); + optionHelpText = "({0}: {1}) ".FormatInvariant(optionGroupWord, optionGroupSpecification.Group) + optionHelpText; } + //note that we need to indent trim the start of the string because it's going to be + //appended to an existing line that is as long as the indent-level + var indented = TextWrapper.WrapAndIndentText(optionHelpText, maxLength + TotalOptionPadding, widthOfHelpText).TrimStart(); + optionsHelp - .Append(optionHelpText) + .Append(indented) .Append(Environment.NewLine) .AppendWhen(additionalNewLineAfterOption, Environment.NewLine); @@ -944,13 +1075,13 @@ private int GetMaxOptionLength(OptionSpecification spec) { specLength += spec.LongName.Length; if (AddDashesToOption) - specLength += 2; + specLength += OptionPrefixWidth; specLength += metaLength; } if (hasShort && hasLong) - specLength += 2; // ", " + specLength += OptionPrefixWidth; return specLength; } @@ -997,5 +1128,8 @@ private static string FormatDefaultValue(T value) ? builder.ToString(0, builder.Length - 1) : string.Empty; } + + + } -} \ No newline at end of file +} diff --git a/src/CommandLine/Text/SentenceBuilder.cs b/src/CommandLine/Text/SentenceBuilder.cs index 1c150b67..842ae675 100644 --- a/src/CommandLine/Text/SentenceBuilder.cs +++ b/src/CommandLine/Text/SentenceBuilder.cs @@ -1,10 +1,11 @@ // Copyright 2005-2015 Giacomo Stelluti Scala & Contributors. All rights reserved. See License.md in the project root for license information. +using CommandLine.Infrastructure; + using System; using System.Collections.Generic; using System.Linq; using System.Text; -using CommandLine.Infrastructure; namespace CommandLine.Text { @@ -33,6 +34,11 @@ public static SentenceBuilder Create() /// public abstract Func RequiredWord { get; } + /// + /// Gets a delegate that returns the word 'group'. + /// + public abstract Func OptionGroupWord { get; } + /// /// Gets a delegate that returns that errors block heading text. /// @@ -41,7 +47,7 @@ public static SentenceBuilder Create() /// /// Gets a delegate that returns usage text block heading text. /// - public abstract Func UsageHeadingText { get; } + public abstract Func UsageHeadingText { get; } /// /// Get a delegate that returns the help text of help command. @@ -53,7 +59,7 @@ public static SentenceBuilder Create() /// Get a delegate that returns the help text of vesion command. /// The delegates must accept a boolean that is equal true for options; otherwise false for verbs. /// - public abstract Func VersionCommandText { get; } + public abstract Func VersionCommandText { get; } /// /// Gets a delegate that handles singular error formatting. @@ -67,7 +73,7 @@ public static SentenceBuilder Create() /// public abstract Func, string> FormatMutuallyExclusiveSetErrors { get; } - private class DefaultSentenceBuilder : SentenceBuilder + private class DefaultSentenceBuilder : SentenceBuilder { public override Func RequiredWord { @@ -84,6 +90,11 @@ public override Func UsageHeadingText get { return () => "USAGE:"; } } + public override Func OptionGroupWord + { + get { return () => "Group"; } + } + public override Func HelpCommandText { get @@ -127,7 +138,7 @@ public override Func FormatError case ErrorType.SequenceOutOfRangeError: var seqOutRange = ((SequenceOutOfRangeError)error); return seqOutRange.NameInfo.Equals(NameInfo.EmptyName) - ? "A sequence value not bound to option name is defined with few items than required." + ? "A sequence value not bound to option name is defined with fewer items than required." : "A sequence option '".JoinTo(seqOutRange.NameInfo.NameText, "' is defined with fewer or more items than required."); case ErrorType.BadVerbSelectedError: @@ -140,6 +151,19 @@ public override Func FormatError case ErrorType.SetValueExceptionError: var setValueError = (SetValueExceptionError)error; return "Error setting value to option '".JoinTo(setValueError.NameInfo.NameText, "': ", setValueError.Exception.Message); + case ErrorType.MissingGroupOptionError: + var missingGroupOptionError = (MissingGroupOptionError)error; + return "At least one option from group '".JoinTo( + missingGroupOptionError.Group, + "' (", + string.Join(", ", missingGroupOptionError.Names.Select(n => n.NameText)), + ") is required."); + case ErrorType.GroupOptionAmbiguityError: + var groupOptionAmbiguityError = (GroupOptionAmbiguityError)error; + return "Both SetName and Group are not allowed in option: (".JoinTo(groupOptionAmbiguityError.Option.NameText, ")"); + case ErrorType.MultipleDefaultVerbsError: + return MultipleDefaultVerbsError.ErrorMessage; + } throw new InvalidOperationException(); }; @@ -153,8 +177,8 @@ public override Func, string> FormatMutua return errors => { var bySet = from e in errors - group e by e.SetName into g - select new { SetName = g.Key, Errors = g.ToList() }; + group e by e.SetName into g + select new { SetName = g.Key, Errors = g.ToList() }; var msgs = bySet.Select( set => @@ -169,7 +193,7 @@ group e by e.SetName into g (from x in (from s in bySet where !s.SetName.Equals(set.SetName) from e in s.Errors select e) .Distinct() - select "'".JoinTo(x.NameInfo.NameText, "', ")).ToArray()); + select "'".JoinTo(x.NameInfo.NameText, "', ")).ToArray()); return new StringBuilder("Option") diff --git a/src/CommandLine/Text/TextWrapper.cs b/src/CommandLine/Text/TextWrapper.cs new file mode 100644 index 00000000..19a93f15 --- /dev/null +++ b/src/CommandLine/Text/TextWrapper.cs @@ -0,0 +1,179 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using CommandLine.Infrastructure; + +namespace CommandLine.Text +{ + /// + /// A utility class to word-wrap and indent blocks of text + /// + public class TextWrapper + { + private string[] lines; + public TextWrapper(string input) + { + //start by splitting at newlines and then reinserting the newline as a separate word + //Note that on the input side, we can't assume the line-break style at run time so we have to + //be able to handle both. We can't use Environment.NewLine because that changes at + //_runtime_ and may not match the line-break style that was compiled in + lines = input + .Replace("\r","") + .Split(new[] {'\n'}, StringSplitOptions.None); + } + + /// + /// Splits a string into a words and performs wrapping while also preserving line-breaks and sub-indentation + /// + /// The number of characters we can use for text + /// + /// This method attempts to wrap text without breaking words + /// For example, if columnWidth is 10 , the input + /// "a string for wrapping 01234567890123" + /// would return + /// "a string + /// "for + /// "wrapping + /// "0123456789 + /// "0123" + /// + /// this + public TextWrapper WordWrap(int columnWidth) + { + //ensure we always use at least 1 column even if the client has told us there's no space available + columnWidth = Math.Max(1, columnWidth); + lines= lines + .SelectMany(line => WordWrapLine(line, columnWidth)) + .ToArray(); + return this; + } + + /// + /// Indent all lines in the TextWrapper by the desired number of spaces + /// + /// The number of spaces to indent by + /// this + public TextWrapper Indent(int numberOfSpaces) + { + lines = lines + .Select(line => numberOfSpaces.Spaces() + line) + .ToArray(); + return this; + } + + /// + /// Returns the current state of the TextWrapper as a string + /// + /// + public string ToText() + { + //return the whole thing as a single string + return string.Join(Environment.NewLine,lines); + } + + /// + /// Convenience method to wraps and indent a string in a single operation + /// + /// The string to operate on + /// The number of spaces to indent by + /// The width of the column used for wrapping + /// + /// The string is wrapped _then_ indented so the columnWidth is the width of the + /// usable text block, and does NOT include the indentLevel. + /// + /// the processed string + public static string WrapAndIndentText(string input, int indentLevel,int columnWidth) + { + return new TextWrapper(input) + .WordWrap(columnWidth) + .Indent(indentLevel) + .ToText(); + } + + + private string [] WordWrapLine(string line,int columnWidth) + { + //create a list of individual lines generated from the supplied line + + //When handling sub-indentation we must always reserve at least one column for text! + var unindentedLine = line.TrimStart(); + var currentIndentLevel = Math.Min(line.Length - unindentedLine.Length,columnWidth-1) ; + columnWidth -= currentIndentLevel; + + return unindentedLine.Split(' ') + .Aggregate( + new List(), + (lineList, word) => AddWordToLastLineOrCreateNewLineIfNecessary(lineList, word, columnWidth) + ) + .Select(builder => currentIndentLevel.Spaces()+builder.ToString().TrimEnd()) + .ToArray(); + } + + /// + /// When presented with a word, either append to the last line in the list or start a new line + /// + /// A list of StringBuilders containing results so far + /// The individual word to append + /// The usable text space + /// + /// The 'word' can actually be an empty string. It's important to keep these - + /// empty strings allow us to preserve indentation and extra spaces within a line. + /// + /// The same list as is passed in + private static List AddWordToLastLineOrCreateNewLineIfNecessary(List lines, string word,int columnWidth) + { + //The current indentation level is based on the previous line but we need to be careful + var previousLine = lines.LastOrDefault()?.ToString() ??string.Empty; + + var wouldWrap = !lines.Any() || (word.Length>0 && previousLine.Length + word.Length > columnWidth); + + if (!wouldWrap) + { + //The usual case is we just append the 'word' and a space to the current line + //Note that trailing spaces will get removed later when we turn the line list + //into a single string + lines.Last().Append(word + ' '); + } + else + { + //The 'while' here is to take account of the possibility of someone providing a word + //which just can't fit in the current column. In that case we just split it at the + //column end. + //That's a rare case though - most of the time we'll succeed in a single pass without + //having to split + //Note that we always do at least one pass even if the 'word' is empty in order to + //honour sub-indentation and extra spaces within strings + do + { + var availableCharacters = Math.Min(columnWidth, word.Length); + var segmentToAdd = LeftString(word,availableCharacters) + ' '; + lines.Add(new StringBuilder(segmentToAdd)); + word = RightString(word,availableCharacters); + } while (word.Length > 0); + } + return lines; + } + + + /// + /// Return the right part of a string in a way that compensates for Substring's deficiencies + /// + private static string RightString(string str,int n) + { + return (n >= str.Length || str.Length==0) + ? string.Empty + : str.Substring(n); + } + /// + /// Return the left part of a string in a way that compensates for Substring's deficiencies + /// + private static string LeftString(string str,int n) + { + + return (n >= str.Length || str.Length==0) + ? str + : str.Substring(0,n); + } + } +} diff --git a/src/CommandLine/UnParserExtensions.cs b/src/CommandLine/UnParserExtensions.cs index 7db948e7..e823a7fa 100644 --- a/src/CommandLine/UnParserExtensions.cs +++ b/src/CommandLine/UnParserExtensions.cs @@ -2,6 +2,7 @@ using System; using System.Collections; +using System.Collections.Generic; using System.Linq; using System.Text; using CommandLine.Core; @@ -19,6 +20,7 @@ public class UnParserSettings private bool groupSwitches; private bool useEqualToken; private bool showHidden; + private bool skipDefault; /// /// Gets or sets a value indicating whether unparsing process shall prefer short or long names. @@ -56,6 +58,14 @@ public bool ShowHidden set { PopsicleSetter.Set(Consumed, ref showHidden, value); } } /// + /// Gets or sets a value indicating whether unparsing process shall skip options with DefaultValue. + /// + public bool SkipDefault + { + get { return skipDefault; } + set { PopsicleSetter.Set(Consumed, ref skipDefault, value); } + } + /// /// Factory method that creates an instance of with GroupSwitches set to true. /// /// A properly initalized instance. @@ -90,7 +100,19 @@ public static class UnParserExtensions /// A string with command line arguments. public static string FormatCommandLine(this Parser parser, T options) { - return parser.FormatCommandLine(options, config => {}); + return parser.FormatCommandLine(options, config => { }); + } + + /// + /// Format a command line argument string from a parsed instance in the form of string[]. + /// + /// Type of . + /// Parser instance. + /// A parsed (or manually correctly constructed instance). + /// A string[] with command line arguments. + public static string[] FormatCommandLineArgs(this Parser parser, T options) + { + return parser.FormatCommandLine(options, config => { }).SplitArgs(); } /// @@ -119,38 +141,49 @@ public static string FormatCommandLine(this Parser parser, T options, Action< var specs = (from info in type.GetSpecifications( - pi => new { Specification = Specification.FromProperty(pi), - Value = pi.GetValue(options, null).NormalizeValue(), PropertyValue = pi.GetValue(options, null) }) - where !info.PropertyValue.IsEmpty() - select info) - .Memorize(); + pi => new + { + Specification = Specification.FromProperty(pi), + Value = pi.GetValue(options, null).NormalizeValue(), + PropertyValue = pi.GetValue(options, null) + }) + where !info.PropertyValue.IsEmpty(info.Specification, settings.SkipDefault) + select info) + .Memoize(); var allOptSpecs = from info in specs.Where(i => i.Specification.Tag == SpecificationType.Option) - let o = (OptionSpecification)info.Specification - where o.TargetType != TargetType.Switch || (o.TargetType == TargetType.Switch && ((bool)info.Value)) - where !o.Hidden || settings.ShowHidden - orderby o.UniqueName() - select info; + let o = (OptionSpecification)info.Specification + where o.TargetType != TargetType.Switch || + (o.TargetType == TargetType.Switch && o.FlagCounter && ((int)info.Value > 0)) || + (o.TargetType == TargetType.Switch && ((bool)info.Value)) + where !o.Hidden || settings.ShowHidden + orderby o.UniqueName() + select info; var shortSwitches = from info in allOptSpecs - let o = (OptionSpecification)info.Specification - where o.TargetType == TargetType.Switch - where o.ShortName.Length > 0 - orderby o.UniqueName() - select info; + let o = (OptionSpecification)info.Specification + where o.TargetType == TargetType.Switch + where o.ShortName.Length > 0 + orderby o.UniqueName() + select info; var optSpecs = settings.GroupSwitches ? allOptSpecs.Where(info => !shortSwitches.Contains(info)) : allOptSpecs; var valSpecs = from info in specs.Where(i => i.Specification.Tag == SpecificationType.Value) - let v = (ValueSpecification)info.Specification - orderby v.Index - select info; + let v = (ValueSpecification)info.Specification + orderby v.Index + select info; builder = settings.GroupSwitches && shortSwitches.Any() ? builder.Append('-').Append(string.Join(string.Empty, shortSwitches.Select( - info => ((OptionSpecification)info.Specification).ShortName).ToArray())).Append(' ') + info => { + var o = (OptionSpecification)info.Specification; + return o.FlagCounter + ? string.Concat(Enumerable.Repeat(o.ShortName, (int)info.Value)) + : o.ShortName; + }).ToArray())).Append(' ') : builder; optSpecs.ForEach( opt => @@ -167,7 +200,19 @@ orderby v.Index return builder .ToString().TrimEnd(' '); } - + /// + /// Format a command line argument string[] from a parsed instance. + /// + /// Type of . + /// Parser instance. + /// A parsed (or manually correctly constructed instance). + /// The lambda used to configure + /// aspects and behaviors of the unparsersing process. + /// A string[] with command line arguments. + public static string[] FormatCommandLineArgs(this Parser parser, T options, Action configuration) + { + return FormatCommandLine(parser, options, configuration).SplitArgs(); + } private static string FormatValue(Specification spec, object value) { var builder = new StringBuilder(); @@ -191,13 +236,16 @@ private static string FormatValue(Specification spec, object value) private static object FormatWithQuotesIfString(object value) { + string s = value.ToString(); + if (!string.IsNullOrEmpty(s) && !s.Contains("\"") && s.Contains(" ")) + return $"\"{s}\""; + Func doubQt = v => v.Contains("\"") ? v.Replace("\"", "\\\"") : v; - return (value as string) - .ToMaybe() - .MapValueOrDefault(v => v.Contains(' ') || v.Contains("\"") - ? "\"".JoinTo(doubQt(v), "\"") : v, value); + return s.ToMaybe() + .MapValueOrDefault(v => v.Contains(' ') || v.Contains("\"") + ? "\"".JoinTo(doubQt(v), "\"") : v, value); } private static char SeperatorOrSpace(this Specification spec) @@ -209,24 +257,25 @@ private static char SeperatorOrSpace(this Specification spec) private static string FormatOption(OptionSpecification spec, object value, UnParserSettings settings) { return new StringBuilder() - .Append(spec.FormatName(settings)) + .Append(spec.FormatName(value, settings)) .AppendWhen(spec.TargetType != TargetType.Switch, FormatValue(spec, value)) .ToString(); } - private static string FormatName(this OptionSpecification optionSpec, UnParserSettings settings) + private static string FormatName(this OptionSpecification optionSpec, object value, UnParserSettings settings) { // Have a long name and short name not preferred? Go with long! // No short name? Has to be long! - var longName = (optionSpec.LongName.Length > 0 && !settings.PreferShortName) + var longName = (optionSpec.LongName.Length > 0 && !settings.PreferShortName) || optionSpec.ShortName.Length == 0; - return + var formattedName = new StringBuilder(longName ? "--".JoinTo(optionSpec.LongName) : "-".JoinTo(optionSpec.ShortName)) .AppendWhen(optionSpec.TargetType != TargetType.Switch, longName && settings.UseEqualToken ? "=" : " ") .ToString(); + return optionSpec.FlagCounter ? String.Join(" ", Enumerable.Repeat(formattedName, (int)value)) : formattedName; } private static object NormalizeValue(this object value) @@ -242,9 +291,13 @@ private static object NormalizeValue(this object value) return value; } - private static bool IsEmpty(this object value) + private static bool IsEmpty(this object value, Specification specification, bool skipDefault) { if (value == null) return true; + + if (skipDefault && value.Equals(specification.DefaultValue.FromJust())) return true; + if (Nullable.GetUnderlyingType(specification.ConversionType) != null) return false; //nullable + #if !SKIP_FSHARP if (ReflectionHelper.IsFSharpOptionType(value.GetType()) && !FSharpOptionHelper.IsSome(value)) return true; #endif @@ -253,5 +306,35 @@ private static bool IsEmpty(this object value) if (value is IEnumerable && !((IEnumerable)value).GetEnumerator().MoveNext()) return true; return false; } + + + #region splitter + /// + /// Returns a string array that contains the substrings in this instance that are delimited by space considering string between double quote. + /// + /// the commandline string + /// don't remove the quote + /// a string array that contains the substrings in this instance + public static string[] SplitArgs(this string command, bool keepQuote = false) + { + if (string.IsNullOrEmpty(command)) + return new string[0]; + + var inQuote = false; + var chars = command.ToCharArray().Select(v => + { + if (v == '"') + inQuote = !inQuote; + return !inQuote && v == ' ' ? '\n' : v; + }).ToArray(); + + return new string(chars).Split('\n') + .Select(x => keepQuote ? x : x.Trim('"')) + .Where(x => !string.IsNullOrWhiteSpace(x)) + .ToArray(); + } + + #endregion + } } diff --git a/src/CommandLine/VerbAttribute.cs b/src/CommandLine/VerbAttribute.cs index 0078a7a8..6ee6024d 100644 --- a/src/CommandLine/VerbAttribute.cs +++ b/src/CommandLine/VerbAttribute.cs @@ -1,6 +1,7 @@ // Copyright 2005-2015 Giacomo Stelluti Scala & Contributors. All rights reserved. See License.md in the project root for license information. using System; +using System.Collections.Generic; namespace CommandLine { @@ -8,31 +9,34 @@ namespace CommandLine /// Models a verb command specification. /// [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = false, Inherited = true)] - public sealed class VerbAttribute : Attribute + //public sealed class VerbAttribute : Attribute + public class VerbAttribute : Attribute { - private readonly string name; - private string helpText; + private readonly Infrastructure.LocalizableAttributeProperty helpText; + private Type resourceType; /// /// 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. + /// aliases for this verb. i.e. "move" and "mv" + /// Thrown if is null, empty or whitespace and is false. + public VerbAttribute(string name, bool isDefault = false, string[] aliases = null) { if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("name"); - this.name = name; - this.helpText = string.Empty; + Name = name; + IsDefault = isDefault; + helpText = new Infrastructure.LocalizableAttributeProperty(nameof(HelpText)); + resourceType = null; + Aliases = aliases ?? new string[0]; } /// /// Gets the verb name. /// - public string Name - { - get { return name; } - } + public string Name { get; private set; } /// /// Gets or sets a value indicating whether a command line verb is visible in the help text. @@ -48,11 +52,26 @@ public bool Hidden /// public string HelpText { - get { return helpText; } - set - { - helpText = value ?? throw new ArgumentNullException("value"); - } + get => helpText.Value ?? string.Empty; + set => helpText.Value = value ?? throw new ArgumentNullException("value"); + } + /// + /// Gets or sets the that contains the resources for . + /// + public Type ResourceType + { + get => resourceType; + set => resourceType = helpText.ResourceType = value; } + + /// + /// Gets whether this verb is the default verb. + /// + public bool IsDefault { get; private set; } + + /// + /// Gets or sets the aliases + /// + public string[] Aliases { get; private set; } } -} \ No newline at end of file +} diff --git a/tests/CommandLine.Tests/CommandLine.Tests.csproj b/tests/CommandLine.Tests/CommandLine.Tests.csproj index 0c28967a..d4dbcab0 100644 --- a/tests/CommandLine.Tests/CommandLine.Tests.csproj +++ b/tests/CommandLine.Tests/CommandLine.Tests.csproj @@ -2,18 +2,24 @@ Library - netcoreapp2.0 - $(DefineConstants);PLATFORM_DOTNET + net461;netcoreapp3.1 $(DefineConstants);SKIP_FSHARP ..\..\CommandLine.snk true + gsscoder;nemec;ericnewton76 + Command Line Parser Library + $(VersionSuffix) + 2.5.0 + Copyright (c) 2005 - 2018 Giacomo Stelluti Scala & Contributors + true - - + + $(DefineConstants);PLATFORM_DOTNET + @@ -22,8 +28,11 @@ - - + + + + + \ No newline at end of file diff --git a/tests/CommandLine.Tests/Fakes/CustomAttribute.cs b/tests/CommandLine.Tests/Fakes/CustomAttribute.cs new file mode 100644 index 00000000..845fb2dd --- /dev/null +++ b/tests/CommandLine.Tests/Fakes/CustomAttribute.cs @@ -0,0 +1,4 @@ +using System; + +[AttributeUsage(AttributeTargets.All)] +class CustomAttribute: Attribute {} \ No newline at end of file diff --git a/tests/CommandLine.Tests/Fakes/Custom_Struct.cs b/tests/CommandLine.Tests/Fakes/Custom_Struct.cs new file mode 100644 index 00000000..64807f6b --- /dev/null +++ b/tests/CommandLine.Tests/Fakes/Custom_Struct.cs @@ -0,0 +1,56 @@ +// Copyright 2005-2015 Giacomo Stelluti Scala & Contributors. All rights reserved. See License.md in the project root for license information. + +using System; + +namespace CommandLine.Tests.Fakes +{ + public class CustomStructOptions + { + [Option('c', "custom", HelpText = "Custom Type")] + public CustomStruct Custom { get; set; } + } + + public struct CustomStruct + { + public string Input { get; set; } + public string Server { get; set; } + public int Port { get; set; } + public CustomStruct(string url) + { + Input = url; + Server = ""; + Port = 80; + var data = url.Split(':'); + if (data.Length == 2) + { + Server = data[0]; + Port = Convert.ToInt32(data[1]); + } + } + } + + public class CustomClassOptions + { + [Option('c', "custom", HelpText = "Custom Type")] + public CustomClass Custom { get; set; } + } + + public class CustomClass + { + public string Input { get; set; } + public string Server { get; set; } + public int Port { get; set; } + public CustomClass(string url) + { + Input = url; + Server = ""; + Port = 80; + var data = url.Split(':'); + if (data.Length == 2) + { + Server = data[0]; + Port = Convert.ToInt32(data[1]); + } + } + } +} diff --git a/tests/CommandLine.Tests/Fakes/HelpTextWithLineBreaksAndSubIndentation_Options.cs b/tests/CommandLine.Tests/Fakes/HelpTextWithLineBreaksAndSubIndentation_Options.cs new file mode 100644 index 00000000..afa77f3a --- /dev/null +++ b/tests/CommandLine.Tests/Fakes/HelpTextWithLineBreaksAndSubIndentation_Options.cs @@ -0,0 +1,13 @@ +namespace CommandLine.Tests.Fakes +{ + public class HelpTextWithLineBreaksAndSubIndentation_Options + { + + [Option(HelpText = @"This is a help text description where we want: + * The left pad after a linebreak to be honoured and the indentation to be preserved across to the next line + * The ability to return to no indent. +Like this.")] + public string StringValue { get; set; } + + } +} \ No newline at end of file diff --git a/tests/CommandLine.Tests/Fakes/HelpTextWithLineBreaks_Options.cs b/tests/CommandLine.Tests/Fakes/HelpTextWithLineBreaks_Options.cs new file mode 100644 index 00000000..b8974ca7 --- /dev/null +++ b/tests/CommandLine.Tests/Fakes/HelpTextWithLineBreaks_Options.cs @@ -0,0 +1,23 @@ +namespace CommandLine.Tests.Fakes +{ + public class HelpTextWithLineBreaks_Options + { + [Option(HelpText = + @"This is a help text description. +It has multiple lines. +We also want to ensure that indentation is correct.")] + public string StringValue { get; set; } + + + [Option(HelpText = @"This is a help text description where we want + the left pad after a linebreak to be honoured so that + we can sub-indent within a description.")] + public string StringValu2 { get; set; } + + + [Option(HelpText = @"This is a help text description where we want + The left pad after a linebreak to be honoured and the indentation to be preserved across to the next line in a way that looks pleasing")] + public string StringValu3 { get; set; } + + } +} diff --git a/tests/CommandLine.Tests/Fakes/HelpTextWithMixedLineBreaks_Options.cs b/tests/CommandLine.Tests/Fakes/HelpTextWithMixedLineBreaks_Options.cs new file mode 100644 index 00000000..9950dbc7 --- /dev/null +++ b/tests/CommandLine.Tests/Fakes/HelpTextWithMixedLineBreaks_Options.cs @@ -0,0 +1,9 @@ +namespace CommandLine.Tests.Fakes +{ + public class HelpTextWithMixedLineBreaks_Options + { + [Option(HelpText = + "This is a help text description\n It has multiple lines.\r\n Third line")] + public string StringValue { get; set; } + } +} diff --git a/tests/CommandLine.Tests/Fakes/Help_Fakes.cs b/tests/CommandLine.Tests/Fakes/Help_Fakes.cs index cceb5331..4b9fe83e 100644 --- a/tests/CommandLine.Tests/Fakes/Help_Fakes.cs +++ b/tests/CommandLine.Tests/Fakes/Help_Fakes.cs @@ -79,7 +79,9 @@ class Options_With_Usage_Attribute [Option("secert-option", Hidden = true, HelpText = "This is a description for a secert hidden option that should never be visibile to the user via help text.")] public string SecertOption { get; set; } - [Usage(ApplicationAlias = "mono testapp.exe")] + [ Custom + , Usage(ApplicationAlias = "mono testapp.exe") + ] public static IEnumerable Examples { get diff --git a/tests/CommandLine.Tests/Fakes/Hidden_Option.cs b/tests/CommandLine.Tests/Fakes/Hidden_Option.cs index 1f18f216..b9a87cd3 100644 --- a/tests/CommandLine.Tests/Fakes/Hidden_Option.cs +++ b/tests/CommandLine.Tests/Fakes/Hidden_Option.cs @@ -1,14 +1,8 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace CommandLine.Tests.Fakes +namespace CommandLine.Tests.Fakes { public class Hidden_Option { - [Option('h', "hiddenOption", Default="hidden", Hidden = true)] + [Option('h', "hiddenOption", Hidden = true)] public string HiddenOption { get; set; } } } diff --git a/tests/CommandLine.Tests/Fakes/Immutable_Simple_Options.cs b/tests/CommandLine.Tests/Fakes/Immutable_Simple_Options.cs index d305dcde..ae829e7d 100644 --- a/tests/CommandLine.Tests/Fakes/Immutable_Simple_Options.cs +++ b/tests/CommandLine.Tests/Fakes/Immutable_Simple_Options.cs @@ -31,4 +31,32 @@ public Immutable_Simple_Options(string stringValue, IEnumerable intSequence [Value(0)] public long LongValue { get { return longValue; } } } + + public class Immutable_Simple_Options_Invalid_Ctor_Args + { + private readonly string stringValue; + private readonly IEnumerable intSequence; + private readonly bool boolValue; + private readonly long longValue; + + public Immutable_Simple_Options_Invalid_Ctor_Args(string stringValue1, IEnumerable intSequence2, bool boolValue, long longValue) + { + this.stringValue = stringValue1; + this.intSequence = intSequence2; + this.boolValue = boolValue; + this.longValue = longValue; + } + + [Option(HelpText = "Define a string value here.")] + public string StringValue { get { return stringValue; } } + + [Option('i', Min = 3, Max = 4, HelpText = "Define a int sequence here.")] + public IEnumerable IntSequence { get { return intSequence; } } + + [Option('x', HelpText = "Define a boolean or switch value here.")] + public bool BoolValue { get { return boolValue; } } + + [Value(0)] + public long LongValue { get { return longValue; } } + } } diff --git a/tests/CommandLine.Tests/Fakes/Mutable_Without_Empty_Constructor.cs b/tests/CommandLine.Tests/Fakes/Mutable_Without_Empty_Constructor.cs new file mode 100644 index 00000000..6f311bb1 --- /dev/null +++ b/tests/CommandLine.Tests/Fakes/Mutable_Without_Empty_Constructor.cs @@ -0,0 +1,19 @@ +// Copyright 2005-2015 Giacomo Stelluti Scala & Contributors. All rights reserved. See License.md in the project root for license information. + +namespace CommandLine.Tests.Fakes +{ + class Mutable_Without_Empty_Constructor + { + [Option("amend", HelpText = "Used to amend the tip of the current branch.")] + public bool Amend { get; set; } + + private Mutable_Without_Empty_Constructor() + { + } + + public static Mutable_Without_Empty_Constructor Create() + { + return new Mutable_Without_Empty_Constructor(); + } + } +} diff --git a/tests/CommandLine.Tests/Fakes/Options_HelpText_Ordering.cs b/tests/CommandLine.Tests/Fakes/Options_HelpText_Ordering.cs new file mode 100644 index 00000000..27c7fa6b --- /dev/null +++ b/tests/CommandLine.Tests/Fakes/Options_HelpText_Ordering.cs @@ -0,0 +1,45 @@ +// Copyright 2005-2015 Giacomo Stelluti Scala & Contributors. All rights reserved. See License.md in the project root for license information. + +namespace CommandLine.Tests.Fakes +{ + + [Verb("verb1")] + class Options_HelpText_Ordering_Verb1 + { + [Option('a', "alpha", Required = true)] + public string alphaOption { get; set; } + + [Option('b', "alpha2", Required = true)] + public string alphaTwoOption { get; set; } + + [Option('d', "charlie", Required = false)] + public string deltaOption { get; set; } + + [Option('c', "bravo", Required = false)] + public string charlieOption { get; set; } + + [Option('f', "foxtrot", Required = false)] + public string foxOption { get; set; } + + [Option('e', "echo", Required = false)] + public string echoOption { get; set; } + + [Value(0)] public string someExtraOption { get; set; } + } + + [Verb("verb2")] + class Options_HelpText_Ordering_Verb2 + { + [Option('a', "alpha", Required = true)] + public string alphaOption { get; set; } + + [Option('b', "alpha2", Required = true)] + public string alphaTwoOption { get; set; } + + [Option('c', "bravo", Required = false)] + public string charlieOption { get; set; } + + [Option('d', "charlie", Required = false)] + public string deltaOption { get; set; } + } +} diff --git a/tests/CommandLine.Tests/Fakes/Options_With_Defaults.cs b/tests/CommandLine.Tests/Fakes/Options_With_Defaults.cs new file mode 100644 index 00000000..eca68790 --- /dev/null +++ b/tests/CommandLine.Tests/Fakes/Options_With_Defaults.cs @@ -0,0 +1,26 @@ +namespace CommandLine.Tests.Fakes +{ + class Options_With_Defaults + { + [Option(Default = 99)] + public int P1 { get; set; } + [Option()] + public string P2 { get; set; } + [Option(Default = 88)] + public int P3 { get; set; } + [Option(Default = Shapes.Square)] + public Shapes P4 { get; set; } + } + class Nuulable_Options_With_Defaults + { + [Option(Default = 99)] + public int? P1 { get; set; } + [Option()] + public string P2 { get; set; } + [Option(Default = 88)] + public int? P3 { get; set; } + [Option(Default = Shapes.Square)] + public Shapes? P4 { get; set; } + } +} + diff --git a/tests/CommandLine.Tests/Fakes/Options_With_Enum_Having_HelpText.cs b/tests/CommandLine.Tests/Fakes/Options_With_Enum_Having_HelpText.cs index 4e1560b1..e3ede175 100644 --- a/tests/CommandLine.Tests/Fakes/Options_With_Enum_Having_HelpText.cs +++ b/tests/CommandLine.Tests/Fakes/Options_With_Enum_Having_HelpText.cs @@ -2,7 +2,7 @@ namespace CommandLine.Tests.Fakes { - enum Shapes + public enum Shapes { Circle, Square, diff --git a/tests/CommandLine.Tests/Fakes/Options_With_FileDirectoryInfo.cs b/tests/CommandLine.Tests/Fakes/Options_With_FileDirectoryInfo.cs new file mode 100644 index 00000000..0d05afbf --- /dev/null +++ b/tests/CommandLine.Tests/Fakes/Options_With_FileDirectoryInfo.cs @@ -0,0 +1,18 @@ +// Copyright 2005-2015 Giacomo Stelluti Scala & Contributors. All rights reserved. See License.md in the project root for license information. + +using System.IO; + +namespace CommandLine.Tests.Fakes +{ + public class Options_With_FileDirectoryInfo + { + [Option('s', "stringPath")] + public string StringPath { get; set; } + + [Option('f', "filePath")] + public FileInfo FilePath { get; set; } + + [Option('d', "directoryPath")] + public DirectoryInfo DirectoryPath { get; set; } + } +} diff --git a/tests/CommandLine.Tests/Fakes/Options_With_FlagCounter_Switches.cs b/tests/CommandLine.Tests/Fakes/Options_With_FlagCounter_Switches.cs new file mode 100644 index 00000000..06787b31 --- /dev/null +++ b/tests/CommandLine.Tests/Fakes/Options_With_FlagCounter_Switches.cs @@ -0,0 +1,13 @@ +// Copyright 2005-2015 Giacomo Stelluti Scala & Contributors. All rights reserved. See License.md in the project root for license information. + +namespace CommandLine.Tests.Fakes +{ + public class Options_With_FlagCounter_Switches + { + [Option('v', FlagCounter=true)] + public int Verbose { get; set; } + + [Option('s', FlagCounter=true)] + public int Silent { get; set; } + } +} diff --git a/tests/CommandLine.Tests/Fakes/Options_With_Group.cs b/tests/CommandLine.Tests/Fakes/Options_With_Group.cs new file mode 100644 index 00000000..849171bd --- /dev/null +++ b/tests/CommandLine.Tests/Fakes/Options_With_Group.cs @@ -0,0 +1,14 @@ +namespace CommandLine.Tests.Fakes +{ + public class Options_With_Group + { + [Option('v', "version")] + public string Version { get; set; } + + [Option("option1", Group = "err-group")] + public string Option1 { get; set; } + + [Option("option2", Group = "err-group")] + public string Option2 { get; set; } + } +} 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/Options_With_Nullable_Enum_Having_HelpText.cs b/tests/CommandLine.Tests/Fakes/Options_With_Nullable_Enum_Having_HelpText.cs new file mode 100644 index 00000000..a316124e --- /dev/null +++ b/tests/CommandLine.Tests/Fakes/Options_With_Nullable_Enum_Having_HelpText.cs @@ -0,0 +1,13 @@ +// Copyright 2005-2015 Giacomo Stelluti Scala & Contributors. All rights reserved. See License.md in the project root for license information. + +namespace CommandLine.Tests.Fakes +{ + class Options_With_Nullable_Enum_Having_HelpText + { + [Option(HelpText = "Define a string value here.")] + public string StringValue { get; set; } + + [Option(HelpText="Define a enum value here.")] + public Shapes? Shape { get; set; } + } +} diff --git a/tests/CommandLine.Tests/Fakes/Options_With_Only_Explicit_Interface.cs b/tests/CommandLine.Tests/Fakes/Options_With_Only_Explicit_Interface.cs new file mode 100644 index 00000000..d367bfc0 --- /dev/null +++ b/tests/CommandLine.Tests/Fakes/Options_With_Only_Explicit_Interface.cs @@ -0,0 +1,9 @@ +namespace CommandLine.Tests.Fakes +{ + class Options_With_Only_Explicit_Interface : IInterface_With_Two_Scalar_Options + { + bool IInterface_With_Two_Scalar_Options.Verbose { get; set; } + + string IInterface_With_Two_Scalar_Options.InputFile { get; set; } + } +} diff --git a/tests/CommandLine.Tests/Fakes/Options_With_Option_Sequence_And_Value_Sequence.cs b/tests/CommandLine.Tests/Fakes/Options_With_Option_Sequence_And_Value_Sequence.cs new file mode 100644 index 00000000..c0ce7cdf --- /dev/null +++ b/tests/CommandLine.Tests/Fakes/Options_With_Option_Sequence_And_Value_Sequence.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; + +namespace CommandLine.Tests.Fakes +{ + public class Options_With_Option_Sequence_And_Value_Sequence + { + [Option('o', "option-seq")] + public IEnumerable OptionSequence { get; set; } + + [Value(0)] + public IEnumerable ValueSequence { get; set; } + } +} diff --git a/tests/CommandLine.Tests/Fakes/Options_With_Sequence_Having_Both_Min_And_Max_Equal.cs b/tests/CommandLine.Tests/Fakes/Options_With_Sequence_Having_Both_Min_And_Max_Equal.cs index d24a8036..21671d76 100644 --- a/tests/CommandLine.Tests/Fakes/Options_With_Sequence_Having_Both_Min_And_Max_Equal.cs +++ b/tests/CommandLine.Tests/Fakes/Options_With_Sequence_Having_Both_Min_And_Max_Equal.cs @@ -1,9 +1,6 @@ // Copyright 2005-2015 Giacomo Stelluti Scala & Contributors. All rights reserved. See License.md in the project root for license information. -using System; using System.Collections.Generic; -using System.Linq; -using System.Text; namespace CommandLine.Tests.Fakes { diff --git a/tests/CommandLine.Tests/Fakes/Options_With_Sequence_Having_Separator_And_Values.cs b/tests/CommandLine.Tests/Fakes/Options_With_Sequence_Having_Separator_And_Values.cs new file mode 100644 index 00000000..6099b5b5 --- /dev/null +++ b/tests/CommandLine.Tests/Fakes/Options_With_Sequence_Having_Separator_And_Values.cs @@ -0,0 +1,74 @@ +// Copyright 2005-2015 Giacomo Stelluti Scala & Contributors. All rights reserved. See License.md in the project root for license information. + +using System.Collections.Generic; + +namespace CommandLine.Tests.Fakes +{ + public class Options_For_Issue_91 + { + [Value(0, Required = true)] + public string InputFileName { get; set; } + + [Option('o', "output")] + public string OutputFileName { get; set; } + + [Option('i', "include", Separator = ',')] + public IEnumerable Included { get; set; } + + [Option('e', "exclude", Separator = ',')] + public IEnumerable Excluded { get; set; } + } + + public class Options_For_Issue_454 + { + [Option('c', "channels", Required = true, Separator = ':', HelpText = "Channel names")] + public IEnumerable Channels { get; set; } + + [Value(0, Required = true, MetaName = "file_path", HelpText = "Path of archive to be processed")] + public string ArchivePath { get; set; } + } + + public class Options_For_Issue_510 + { + [Option('a', "aa", Required = false, Separator = ',')] + public IEnumerable A { get; set; } + + [Option('b', "bb", Required = false)] + public string B { get; set; } + + [Value(0, Required = true)] + public string C { get; set; } + } + + public enum FMode { C, D, S }; + + public class Options_For_Issue_617 + { + [Option("fm", Separator=',', Default = new[] { FMode.S })] + public IEnumerable Mode { get; set; } + + [Option('q')] + public bool q { get;set; } + + [Value(0)] + public IList Files { get; set; } + } + + public class Options_For_Issue_619 + { + [Option("verbose", Required = false, Default = false, HelpText = "Generate process tracing information")] + public bool Verbose { get; set; } + + [Option("outdir", Required = false, Default = ".", HelpText = "Directory to look for object file")] + public string OutDir { get; set; } + + [Option("modules", Required = true, Separator = ',', HelpText = "Directories to look for module file")] + public IEnumerable ModuleDirs { get; set; } + + [Option("ignore", Required = false, Separator = ' ', HelpText = "List of additional module name references to ignore")] + public IEnumerable Ignores { get; set; } + + [Value(0, Required = true, HelpText = "List of source files to process")] + public IEnumerable Srcs { get; set; } + } +} diff --git a/tests/CommandLine.Tests/Fakes/Options_With_Similar_Names.cs b/tests/CommandLine.Tests/Fakes/Options_With_Similar_Names.cs new file mode 100644 index 00000000..781d29cb --- /dev/null +++ b/tests/CommandLine.Tests/Fakes/Options_With_Similar_Names.cs @@ -0,0 +1,31 @@ +// Copyright 2005-2015 Giacomo Stelluti Scala & Contributors. All rights reserved. See License.md in the project root for license information. + +using System.Collections.Generic; + +namespace CommandLine.Tests.Fakes +{ + public class Options_With_Similar_Names + { + [Option("deploy", Separator = ',', HelpText= "Projects to deploy")] + public IEnumerable Deploys { get; set; } + + [Option("profile", Required = true, HelpText = "Profile to use when restoring and publishing")] + public string Profile { get; set; } + + [Option("configure-profile", Required = true, HelpText = "Profile to use for Configure")] + public string ConfigureProfile { get; set; } + } + + public class Options_With_Similar_Names_And_Separator + { + [Option('f', "flag", HelpText = "Flag")] + public bool Flag { get; set; } + + [Option('c', "categories", Required = false, Separator = ',', HelpText = "Categories")] + public IEnumerable Categories { get; set; } + + [Option('j', "jobId", Required = true, HelpText = "Texts.ExplainJob")] + public int JobId { get; set; } + } + +} diff --git a/tests/CommandLine.Tests/Fakes/Options_With_Value_Sequence_And_Normal_Option.cs b/tests/CommandLine.Tests/Fakes/Options_With_Value_Sequence_And_Normal_Option.cs new file mode 100644 index 00000000..e8e7bf47 --- /dev/null +++ b/tests/CommandLine.Tests/Fakes/Options_With_Value_Sequence_And_Normal_Option.cs @@ -0,0 +1,28 @@ +// Copyright 2005-2015 Giacomo Stelluti Scala & Contributors. All rights reserved. See License.md in the project root for license information. +using System.Collections.Generic; + +namespace CommandLine.Tests.Fakes +{ + public class Options_With_Value_Sequence_And_Normal_Option + { + [Option('c', "compress", + HelpText = "Compress Match Pattern, Pipe Separated (|) ", + Separator = '|', + Default = new[] + { + "*.txt", "*.log", "*.ini" + })] + public IEnumerable Compress { get; set; } + + [Value(0, + HelpText = "Input Directories.", + Required = true)] + public IEnumerable InputDirs { get; set; } + + + [Option('n', "name", + HelpText = "Metadata Name.", + Default = "WILDCARD")] + public string Name { get; set; } + } +} diff --git a/tests/CommandLine.Tests/Fakes/ResourceFakes.cs b/tests/CommandLine.Tests/Fakes/ResourceFakes.cs new file mode 100644 index 00000000..1b18da6e --- /dev/null +++ b/tests/CommandLine.Tests/Fakes/ResourceFakes.cs @@ -0,0 +1,88 @@ +namespace CommandLine.Tests.Fakes +{ + public static class StaticResource + { + public static string HelpText { get { return "Localized HelpText"; } } + public static TypeWithImplicitCast ImplicitCastHelpText => new TypeWithImplicitCast("Localized HelpText"); + public static TypeWithExplicitCast ExplicitCastHelpText => new TypeWithExplicitCast("Localized HelpText"); + public static TypeWithWrongImplicitCast WrongImplicitCastHelpText => new TypeWithWrongImplicitCast(); + public static TypeWithWrongExplicitCast WrongExplicitCastHelpText => new TypeWithWrongExplicitCast(); + } + + public class NonStaticResource + { + public static string HelpText { get { return "Localized HelpText"; } } + public static string WriteOnlyText { set { value?.ToString(); } } + private static string PrivateHelpText { get { return "Localized HelpText"; } } + public static TypeWithImplicitCast ImplicitCastHelpText => new TypeWithImplicitCast("Localized HelpText"); + public static TypeWithExplicitCast ExplicitCastHelpText => new TypeWithExplicitCast("Localized HelpText"); + public static TypeWithWrongImplicitCast WrongImplicitCastHelpText => new TypeWithWrongImplicitCast(); + public static TypeWithWrongExplicitCast WrongExplicitCastHelpText => new TypeWithWrongExplicitCast(); + } + + public class NonStaticResource_WithNonStaticProperty + { + public string HelpText { get { return "Localized HelpText"; } } + } + + internal class InternalResource + { + public static string HelpText { get { return "Localized HelpText"; } } + } + + public class TypeWithImplicitCast + { + private string value; + + public TypeWithImplicitCast(string value) + { + this.value = value; + } + + public static implicit operator string(TypeWithImplicitCast obj) + { + return obj.value; + } + + public static implicit operator int(TypeWithImplicitCast obj) + { + return 0; + } + } + + public class TypeWithWrongImplicitCast + { + public static implicit operator int(TypeWithWrongImplicitCast obj) + { + return 0; + } + } + + public class TypeWithExplicitCast + { + private string value; + + public TypeWithExplicitCast(string value) + { + this.value = value; + } + + public static explicit operator string(TypeWithExplicitCast obj) + { + return obj.value; + } + + public static explicit operator int(TypeWithExplicitCast obj) + { + return 0; + } + } + + public class TypeWithWrongExplicitCast + { + public static explicit operator int(TypeWithWrongExplicitCast obj) + { + return 0; + } + } +} diff --git a/tests/CommandLine.Tests/Fakes/Scalar_String_Mutable.cs b/tests/CommandLine.Tests/Fakes/Scalar_String_Mutable.cs deleted file mode 100644 index abfc7dfb..00000000 --- a/tests/CommandLine.Tests/Fakes/Scalar_String_Mutable.cs +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright 2005-2015 Giacomo Stelluti Scala & Contributors. All rights reserved. See License.md in the project root for license information. - - -namespace CommandLine.Tests.Properties.Fakes -{ - class Scalar_String_Mutable - { - [Option] - public string StringValue { get; set; } - } -} diff --git a/tests/CommandLine.Tests/Fakes/Simple_Options_With_ExtraArgs.cs b/tests/CommandLine.Tests/Fakes/Simple_Options_With_ExtraArgs.cs new file mode 100644 index 00000000..bb276fa5 --- /dev/null +++ b/tests/CommandLine.Tests/Fakes/Simple_Options_With_ExtraArgs.cs @@ -0,0 +1,27 @@ +// Copyright 2005-2015 Giacomo Stelluti Scala & Contributors. All rights reserved. See License.md in the project root for license information. + +using System.Collections.Generic; + +namespace CommandLine.Tests.Fakes +{ + public class Simple_Options_WithExtraArgs + { + [Option(HelpText = "Define a string value here.")] + public string StringValue { get; set; } + + [Option('s', "shortandlong", HelpText = "Example with both short and long name.")] + public string ShortAndLong { get; set; } + + [Option('i', Min = 3, Max = 4, Separator = ',', HelpText = "Define a int sequence here.")] + public IEnumerable IntSequence { get; set; } + + [Option('x', HelpText = "Define a boolean or switch value here.")] + public bool BoolValue { get; set; } + + [Value(0, HelpText = "Define a long value here.")] + public long LongValue { get; set; } + + [Value(1, HelpText = "Extra args get collected here.")] + public IEnumerable ExtraArgs { get; set; } + } +} diff --git a/tests/CommandLine.Tests/Fakes/Simple_Options_With_Multiple_OptionGroups.cs b/tests/CommandLine.Tests/Fakes/Simple_Options_With_Multiple_OptionGroups.cs new file mode 100644 index 00000000..d4496666 --- /dev/null +++ b/tests/CommandLine.Tests/Fakes/Simple_Options_With_Multiple_OptionGroups.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; + +namespace CommandLine.Tests.Fakes +{ + public class Simple_Options_With_Multiple_OptionGroups + { + [Option(HelpText = "Define a string value here.", Group = "string-group")] + public string StringValue { get; set; } + + [Option('s', "shortandlong", HelpText = "Example with both short and long name.", Group = "string-group")] + public string ShortAndLong { get; set; } + + [Option('x', HelpText = "Define a boolean or switch value here.", Group = "second-group")] + public bool BoolValue { get; set; } + + [Option('i', Min = 3, Max = 4, HelpText = "Define a int sequence here.", Group = "second-group")] + public IEnumerable IntSequence { get; set; } + } +} diff --git a/tests/CommandLine.Tests/Fakes/Simple_Options_With_OptionGroup.cs b/tests/CommandLine.Tests/Fakes/Simple_Options_With_OptionGroup.cs new file mode 100644 index 00000000..f1b2bfdd --- /dev/null +++ b/tests/CommandLine.Tests/Fakes/Simple_Options_With_OptionGroup.cs @@ -0,0 +1,14 @@ +namespace CommandLine.Tests.Fakes +{ + public class Simple_Options_With_OptionGroup + { + [Option(HelpText = "Define a string value here.", Group = "string-group")] + public string StringValue { get; set; } + + [Option('s', "shortandlong", HelpText = "Example with both short and long name.", Group = "string-group")] + 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_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_WithDefaultValue.cs b/tests/CommandLine.Tests/Fakes/Simple_Options_With_OptionGroup_WithDefaultValue.cs new file mode 100644 index 00000000..ccfc643d --- /dev/null +++ b/tests/CommandLine.Tests/Fakes/Simple_Options_With_OptionGroup_WithDefaultValue.cs @@ -0,0 +1,14 @@ +namespace CommandLine.Tests.Fakes +{ + public class Simple_Options_With_OptionGroup_WithDefaultValue + { + [Option(HelpText = "Define a string value here.", Required = true, Group = "")] + public string StringValue { get; set; } + + [Option('s', "shortandlong", HelpText = "Example with both short and long name.", Required = true, Group = "")] + 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/Simple_Options_With_Required_OptionGroup.cs b/tests/CommandLine.Tests/Fakes/Simple_Options_With_Required_OptionGroup.cs new file mode 100644 index 00000000..bbeaaf53 --- /dev/null +++ b/tests/CommandLine.Tests/Fakes/Simple_Options_With_Required_OptionGroup.cs @@ -0,0 +1,14 @@ +namespace CommandLine.Tests.Fakes +{ + public class Simple_Options_With_Required_OptionGroup + { + [Option(HelpText = "Define a string value here.", Required = true, Group = "string-group")] + public string StringValue { get; set; } + + [Option('s', "shortandlong", HelpText = "Example with both short and long name.", Required = true, Group = "string-group")] + 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..9710d0de 100644 --- a/tests/CommandLine.Tests/Fakes/Verb_Fakes.cs +++ b/tests/CommandLine.Tests/Fakes/Verb_Fakes.cs @@ -18,7 +18,20 @@ public class Add_Verb [Value(0)] public string FileName { get; set; } } + [Verb("add", isDefault:true,HelpText = "Add file contents to the index.")] + public class Add_Verb_As_Default + { + [Option('p', "patch", SetName = "mode-p", + HelpText = "Interactively choose hunks of patch between the index and the work tree and add them to the index.")] + public bool Patch { get; set; } + + [Option('f', "force", SetName = "mode-f", + HelpText = "Allow adding otherwise ignored files.")] + public bool Force { get; set; } + [Value(0)] + public string FileName { get; set; } + } [Verb("commit", HelpText = "Record changes to the repository.")] public class Commit_Verb { @@ -85,4 +98,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/ParserProperties.cs b/tests/CommandLine.Tests/ParserProperties.cs deleted file mode 100644 index ae55d18c..00000000 --- a/tests/CommandLine.Tests/ParserProperties.cs +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright 2005-2015 Giacomo Stelluti Scala & Contributors. All rights reserved. See License.md in the project root for license information. - -using CommandLine.Tests.Properties.Fakes; -using FluentAssertions; -using FsCheck; -using Xunit; - -namespace CommandLine.Tests.Properties -{ - public class ParserProperties - { - private static readonly Parser Sut = new Parser(); - - //[Fact] - public void Parsing_a_string_returns_original_string() - { - Prop.ForAll>( - x => - { - var value = x.Get; - var result = Sut.ParseArguments(new[] { "--stringvalue", value }); - ((Parsed)result).Value.StringValue.Should().BeEquivalentTo(value); - }).QuickCheckThrowOnFailure(); - } - } -} diff --git a/tests/CommandLine.Tests/StringExtensions.cs b/tests/CommandLine.Tests/StringExtensions.cs index e3830b0c..1ea18538 100644 --- a/tests/CommandLine.Tests/StringExtensions.cs +++ b/tests/CommandLine.Tests/StringExtensions.cs @@ -13,6 +13,11 @@ public static string[] ToNotEmptyLines(this string value) return value.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries); } + public static string[] ToLines(this string value) + { + return value.Split(new[] { Environment.NewLine }, StringSplitOptions.None); + } + public static string[] TrimStringArray(this IEnumerable array) { return array.Select(item => item.Trim()).ToArray(); diff --git a/tests/CommandLine.Tests/Unit/BaseAttributeTests.cs b/tests/CommandLine.Tests/Unit/BaseAttributeTests.cs index 79d20c7b..ab566255 100644 --- a/tests/CommandLine.Tests/Unit/BaseAttributeTests.cs +++ b/tests/CommandLine.Tests/Unit/BaseAttributeTests.cs @@ -1,9 +1,4 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using System.Text; -using System.Threading.Tasks; using Xunit; namespace CommandLine.Tests.Unit @@ -20,6 +15,45 @@ public static void Default(object defaultValue) Assert.Equal(defaultValue, baseAttribute.Default); } + [Theory] + [InlineData("", null, "")] + [InlineData("", typeof(Fakes.StaticResource), "")] + [InlineData("Help text", null, "Help text")] + [InlineData("HelpText", typeof(Fakes.StaticResource), "Localized HelpText")] + [InlineData("HelpText", typeof(Fakes.NonStaticResource), "Localized HelpText")] + [InlineData("ImplicitCastHelpText", typeof(Fakes.StaticResource), "Localized HelpText")] + [InlineData("ImplicitCastHelpText", typeof(Fakes.NonStaticResource), "Localized HelpText")] + [InlineData("ExplicitCastHelpText", typeof(Fakes.StaticResource), "Localized HelpText")] + [InlineData("ExplicitCastHelpText", typeof(Fakes.NonStaticResource), "Localized HelpText")] + public static void HelpText(string helpText, Type resourceType, string expected) + { + TestBaseAttribute baseAttribute = new TestBaseAttribute(); + baseAttribute.HelpText = helpText; + baseAttribute.ResourceType = resourceType; + + Assert.Equal(expected, baseAttribute.HelpText); + } + + [Theory] + [InlineData("HelpText", typeof(Fakes.NonStaticResource_WithNonStaticProperty))] + [InlineData("WriteOnlyText", typeof(Fakes.NonStaticResource))] + [InlineData("PrivateOnlyText", typeof(Fakes.NonStaticResource))] + [InlineData("HelpText", typeof(Fakes.InternalResource))] + [InlineData("WrongImplicitCastHelpText", typeof(Fakes.StaticResource))] + [InlineData("WrongExplicitCastHelpText", typeof(Fakes.StaticResource))] + [InlineData("WrongImplicitCastHelpText", typeof(Fakes.NonStaticResource))] + [InlineData("WrongExplicitCastHelpText", typeof(Fakes.NonStaticResource))] + public void ThrowsHelpText(string helpText, Type resourceType) + { + TestBaseAttribute baseAttribute = new TestBaseAttribute(); + baseAttribute.HelpText = helpText; + baseAttribute.ResourceType = resourceType; + + // Verify exception + Assert.Throws(() => baseAttribute.HelpText.ToString()); + } + + private class TestBaseAttribute : BaseAttribute { public TestBaseAttribute() @@ -27,5 +61,6 @@ public TestBaseAttribute() // Do nothing } } + } } diff --git a/tests/CommandLine.Tests/Unit/Core/GetoptTokenizerTests.cs b/tests/CommandLine.Tests/Unit/Core/GetoptTokenizerTests.cs new file mode 100644 index 00000000..337a9a3f --- /dev/null +++ b/tests/CommandLine.Tests/Unit/Core/GetoptTokenizerTests.cs @@ -0,0 +1,126 @@ +// Copyright 2005-2015 Giacomo Stelluti Scala & Contributors. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using Xunit; +using FluentAssertions; +using CSharpx; +using RailwaySharp.ErrorHandling; +using CommandLine.Core; + +namespace CommandLine.Tests.Unit.Core +{ + public class GetoptTokenizerTests + { + [Fact] + public void Explode_scalar_with_separator_in_odd_args_input_returns_sequence() + { + // Fixture setup + var expectedTokens = new[] { Token.Name("i"), Token.Value("10"), Token.Name("string-seq"), + Token.Value("aaa"), Token.Value("bb"), Token.Value("cccc"), Token.Name("switch") }; + var specs = new[] { new OptionSpecification(string.Empty, "string-seq", + false, string.Empty, Maybe.Nothing(), Maybe.Nothing(), ',', null, string.Empty, string.Empty, new List(), typeof(IEnumerable), TargetType.Sequence, string.Empty)}; + + // Exercize system + var result = + GetoptTokenizer.ExplodeOptionList( + Result.Succeed( + Enumerable.Empty().Concat(new[] { Token.Name("i"), Token.Value("10"), + Token.Name("string-seq"), Token.Value("aaa,bb,cccc"), Token.Name("switch") }), + Enumerable.Empty()), + optionName => NameLookup.HavingSeparator(optionName, specs, StringComparer.Ordinal)); + // Verify outcome + ((Ok, Error>)result).Success.Should().BeEquivalentTo(expectedTokens); + + // Teardown + } + + [Fact] + public void Explode_scalar_with_separator_in_even_args_input_returns_sequence() + { + // Fixture setup + var expectedTokens = new[] { Token.Name("x"), Token.Name("string-seq"), + Token.Value("aaa"), Token.Value("bb"), Token.Value("cccc"), Token.Name("switch") }; + var specs = new[] { new OptionSpecification(string.Empty, "string-seq", + false, string.Empty, Maybe.Nothing(), Maybe.Nothing(), ',', null, string.Empty, string.Empty, new List(), typeof(IEnumerable), TargetType.Sequence, string.Empty)}; + + // Exercize system + var result = + GetoptTokenizer.ExplodeOptionList( + Result.Succeed( + Enumerable.Empty().Concat(new[] { Token.Name("x"), + Token.Name("string-seq"), Token.Value("aaa,bb,cccc"), Token.Name("switch") }), + Enumerable.Empty()), + optionName => NameLookup.HavingSeparator(optionName, specs, StringComparer.Ordinal)); + + // Verify outcome + ((Ok, Error>)result).Success.Should().BeEquivalentTo(expectedTokens); + + // Teardown + } + + [Fact] + public void Should_properly_parse_option_with_equals_in_value() + { + /** + * This is how the arg. would look in `static void Main(string[] args)` + * if passed from the command-line and the option-value wrapped in quotes. + * Ex.) ./app --connectionString="Server=localhost;Data Source..." + */ + var args = new[] { "--connectionString=Server=localhost;Data Source=(LocalDB)\v12.0;Initial Catalog=temp;" }; + + var result = GetoptTokenizer.Tokenize(args, name => NameLookupResult.OtherOptionFound); + + var tokens = result.SucceededWith(); + + Assert.NotNull(tokens); + Assert.Equal(2, tokens.Count()); + Assert.Equal("connectionString", tokens.First().Text); + Assert.Equal("Server=localhost;Data Source=(LocalDB)\v12.0;Initial Catalog=temp;", tokens.Last().Text); + } + + [Fact] + public void Should_return_error_if_option_format_with_equals_is_not_correct() + { + var args = new[] { "--option1 = fail", "--option2= succeed" }; + + var result = GetoptTokenizer.Tokenize(args, name => NameLookupResult.OtherOptionFound); + + var errors = result.SuccessMessages(); + + Assert.NotNull(errors); + Assert.Equal(1, errors.Count()); + Assert.Equal(ErrorType.BadFormatTokenError, errors.First().Tag); + + var tokens = result.SucceededWith(); + Assert.NotNull(tokens); + Assert.Equal(2, tokens.Count()); + Assert.Equal(TokenType.Name, tokens.First().Tag); + Assert.Equal(TokenType.Value, tokens.Last().Tag); + Assert.Equal("option2", tokens.First().Text); + Assert.Equal(" succeed", tokens.Last().Text); + } + + + [Theory] + [InlineData(new[] { "-a", "-" }, 2,"a","-")] + [InlineData(new[] { "--file", "-" }, 2,"file","-")] + [InlineData(new[] { "-f-" }, 2,"f", "-")] + [InlineData(new[] { "--file=-" }, 2, "file", "-")] + [InlineData(new[] { "-a", "--" }, 2, "a", "--")] + public void Single_dash_as_a_value(string[] args, int countExcepted,string first,string last) + { + //Arrange + //Act + var result = GetoptTokenizer.Tokenize(args, name => NameLookupResult.OtherOptionFound); + var tokens = result.SucceededWith().ToList(); + //Assert + tokens.Should().NotBeNull(); + tokens.Count.Should().Be(countExcepted); + tokens.First().Text.Should().Be(first); + tokens.Last().Text.Should().Be(last); + } + } + +} diff --git a/tests/CommandLine.Tests/Unit/Core/InstanceBuilderTests.cs b/tests/CommandLine.Tests/Unit/Core/InstanceBuilderTests.cs index 8eb36080..2f8d02b7 100644 --- a/tests/CommandLine.Tests/Unit/Core/InstanceBuilderTests.cs +++ b/tests/CommandLine.Tests/Unit/Core/InstanceBuilderTests.cs @@ -1,24 +1,25 @@ -// Copyright 2005-2015 Giacomo Stelluti Scala & Contributors. All rights reserved. See License.md in the project root for license information. +// Copyright 2005-2015 Giacomo Stelluti Scala & Contributors. All rights reserved. See License.md in the project root for license information. -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; using Microsoft.FSharp.Core; using CommandLine.Core; using CommandLine.Infrastructure; +using CommandLine.Tests.Fakes; using CSharpx; -using CommandLine.Tests.Fakes; + using FluentAssertions; + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; using Xunit; -using System.Reflection; namespace CommandLine.Tests.Unit.Core { public class InstanceBuilderTests { - private static ParserResult InvokeBuild(string[] arguments, bool autoHelp = true, bool autoVersion = true) + private static ParserResult InvokeBuild(string[] arguments, bool autoHelp = true, bool autoVersion = true, bool multiInstance = false) where T : new() { return InstanceBuilder.Build( @@ -30,6 +31,7 @@ private static ParserResult InvokeBuild(string[] arguments, bool autoHelp CultureInfo.InvariantCulture, autoHelp, autoVersion, + multiInstance, Enumerable.Empty()); } @@ -75,12 +77,10 @@ public void Explicit_help_request_generates_help_requested_error() // Verify outcome result.Should().BeEquivalentTo(expectedResult); - - // Teardown } [Theory] - [InlineData(new[] {"-123"}, -123L)] + [InlineData(new[] { "-123" }, -123L)] [InlineData(new[] { "-1" }, -1L)] [InlineData(new[] { "-9223372036854775807" }, -9223372036854775807)] // long.MaxValue * -1 public void Parse_negative_long_value(string[] arguments, long expected) @@ -93,8 +93,6 @@ public void Parse_negative_long_value(string[] arguments, long expected) // Verify outcome ((Parsed)result).Value.LongValue.Should().Be(expected); - - // Teardown } [Theory] @@ -113,8 +111,6 @@ public void Parse_double_value(string[] arguments, double expected) // Verify outcome ((Parsed)result).Value.DoubleValue.Should().Be(expected); - - // Teardown } [Theory] @@ -134,8 +130,6 @@ public void Parse_int_sequence(string[] arguments, int[] expected) // Verify outcome ((Parsed)result).Value.IntSequence.Should().BeEquivalentTo(expected); - - // Teardown } [Theory] @@ -153,14 +147,12 @@ public void Parse_int_sequence_with_range(string[] arguments, int[] expected) // Verify outcome ((Parsed)result).Value.IntSequence.Should().BeEquivalentTo(expected); - - // Teardown } [Theory] - [InlineData(new[] {"-s", "just-one"}, new[] {"just-one"})] - [InlineData(new[] {"-sjust-one-samearg"}, new[] {"just-one-samearg"})] - [InlineData(new[] {"-s", "also-two", "are-ok" }, new[] { "also-two", "are-ok" })] + [InlineData(new[] { "-s", "just-one" }, new[] { "just-one" })] + [InlineData(new[] { "-sjust-one-samearg" }, new[] { "just-one-samearg" })] + [InlineData(new[] { "-s", "also-two", "are-ok" }, new[] { "also-two", "are-ok" })] [InlineData(new[] { "--string-seq", "one", "two", "three" }, new[] { "one", "two", "three" })] [InlineData(new[] { "--string-seq=one", "two", "three", "4" }, new[] { "one", "two", "three", "4" })] public void Parse_string_sequence_with_only_min_constraint(string[] arguments, string[] expected) @@ -173,8 +165,6 @@ public void Parse_string_sequence_with_only_min_constraint(string[] arguments, s // Verify outcome ((Parsed)result).Value.StringSequence.Should().BeEquivalentTo(expected); - - // Teardown } [Theory] @@ -192,12 +182,10 @@ public void Parse_string_sequence_with_only_max_constraint(string[] arguments, s // Verify outcome ((Parsed)result).Value.StringSequence.Should().BeEquivalentTo(expected); - - // Teardown } [Fact] - public void Breaking_min_constraint_in_string_sequence_gererates_MissingValueOptionError() + public void Breaking_min_constraint_in_string_sequence_generates_MissingValueOptionError() { // Fixture setup var expectedResult = new[] { new MissingValueOptionError(new NameInfo("s", "string-seq")) }; @@ -208,12 +196,10 @@ public void Breaking_min_constraint_in_string_sequence_gererates_MissingValueOpt // Verify outcome ((NotParsed)result).Errors.Should().BeEquivalentTo(expectedResult); - - // Teardown } [Fact] - public void Breaking_min_constraint_in_string_sequence_as_value_gererates_SequenceOutOfRangeError() + public void Breaking_min_constraint_in_string_sequence_as_value_generates_SequenceOutOfRangeError() { // Fixture setup var expectedResult = new[] { new SequenceOutOfRangeError(NameInfo.EmptyName) }; @@ -224,28 +210,25 @@ public void Breaking_min_constraint_in_string_sequence_as_value_gererates_Sequen // Verify outcome ((NotParsed)result).Errors.Should().BeEquivalentTo(expectedResult); - - // Teardown } [Fact] - public void Breaking_max_constraint_in_string_sequence_gererates_SequenceOutOfRangeError() + public void Breaking_max_constraint_in_string_sequence_does_not_generate_SequenceOutOfRangeError() { // Fixture setup - var expectedResult = new[] { new SequenceOutOfRangeError(new NameInfo("s", "string-seq")) }; + var expectedResult = new[] { "one", "two", "three" }; // Exercize system var result = InvokeBuild( new[] { "--string-seq=one", "two", "three", "this-is-too-much" }); // Verify outcome - ((NotParsed)result).Errors.Should().BeEquivalentTo(expectedResult); - - // Teardown + ((Parsed)result).Value.StringSequence.Should().BeEquivalentTo(expectedResult); + // The "this-is-too-much" arg would end up assigned to a Value; since there is no Value, it is silently dropped } [Fact] - public void Breaking_max_constraint_in_string_sequence_as_value_gererates_SequenceOutOfRangeError() + public void Breaking_max_constraint_in_string_sequence_as_value_generates_SequenceOutOfRangeError() { // Fixture setup var expectedResult = new[] { new SequenceOutOfRangeError(NameInfo.EmptyName) }; @@ -256,8 +239,6 @@ public void Breaking_max_constraint_in_string_sequence_as_value_gererates_Sequen // Verify outcome ((NotParsed)result).Errors.Should().BeEquivalentTo(expectedResult); - - // Teardown } [Theory] @@ -277,8 +258,6 @@ public void Parse_enum_value(string[] arguments, Colors expected) // Verify outcome expected.Should().BeEquivalentTo(((Parsed)result).Value.Colors); - - // Teardown } [Theory] @@ -298,8 +277,6 @@ public void Parse_enum_value_ignore_case(string[] arguments, Colors expected) // Verify outcome expected.Should().BeEquivalentTo(((Parsed)result).Value.Colors); - - // Teardown } [Fact] @@ -314,8 +291,6 @@ public void Parse_enum_value_with_wrong_index_generates_BadFormatConversionError // Verify outcome ((NotParsed)result).Errors.Should().BeEquivalentTo(expectedResult); - - // Teardown } [Fact] @@ -330,8 +305,6 @@ public void Parse_enum_value_with_wrong_item_name_generates_BadFormatConversionE // Verify outcome ((NotParsed)result).Errors.Should().BeEquivalentTo(expectedResult); - - // Teardown } [Fact] @@ -346,8 +319,6 @@ public void Parse_enum_value_with_wrong_item_name_case_generates_BadFormatConver // Verify outcome ((NotParsed)result).Errors.Should().BeEquivalentTo(expectedResult); - - // Teardown } [Fact] @@ -355,12 +326,12 @@ public void Parse_values_partitioned_between_sequence_and_scalar() { // Fixture setup var expectedResult = new Simple_Options_With_Values - { - StringValue = string.Empty, - LongValue = 10L, - StringSequence = new[] { "a", "b", "c" }, - IntValue = 20 - }; + { + StringValue = string.Empty, + LongValue = 10L, + StringSequence = new[] { "a", "b", "c" }, + IntValue = 20 + }; // Exercize system var result = InvokeBuild( @@ -368,8 +339,6 @@ public void Parse_values_partitioned_between_sequence_and_scalar() // Verify outcome expectedResult.Should().BeEquivalentTo(((Parsed)result).Value); - - // Teardown } [Theory] @@ -388,8 +357,6 @@ public void Parse_sequence_value_without_range_constraints(string[] arguments, l // Verify outcome expected.Should().BeEquivalentTo(((Parsed)result).Value.LongSequence); - - // Teardown } [Theory] @@ -407,8 +374,6 @@ public void Parse_long_sequence_with_separator(string[] arguments, long[] expect // Verify outcome expected.Should().BeEquivalentTo(((Parsed)result).Value.LongSequence); - - // Teardown } [Theory] @@ -426,8 +391,6 @@ public void Parse_string_sequence_with_separator(string[] arguments, string[] ex // Verify outcome expected.Should().BeEquivalentTo(((Parsed)result).Value.StringSequence); - - // Teardown } /// @@ -438,12 +401,12 @@ public void Double_dash_force_subsequent_arguments_as_values() { // Fixture setup var expectedResult = new Simple_Options_With_Values - { - StringValue = "str1", - LongValue = 10L, - StringSequence = new[] { "-a", "--bee", "-c" }, - IntValue = 20 - }; + { + StringValue = "str1", + LongValue = 10L, + StringSequence = new[] { "-a", "--bee", "-c" }, + IntValue = 20 + }; var arguments = new[] { "--stringvalue", "str1", "--", "10", "-a", "--bee", "-c", "20" }; // Exercize system @@ -462,12 +425,10 @@ public void Double_dash_force_subsequent_arguments_as_values() // Verify outcome expectedResult.Should().BeEquivalentTo(((Parsed)result).Value); - - // Teardown } [Fact] - public void Parse_option_from_different_sets_gererates_MutuallyExclusiveSetError() + public void Parse_option_from_different_sets_generates_MutuallyExclusiveSetError() { // Fixture setup var expectedResult = new[] @@ -482,14 +443,14 @@ public void Parse_option_from_different_sets_gererates_MutuallyExclusiveSetError // Verify outcome ((NotParsed)result).Errors.Should().BeEquivalentTo(expectedResult); - - // Teardown } [Fact] - public void Two_required_options_at_the_same_set_and_both_are_true() { + public void Two_required_options_at_the_same_set_and_both_are_true() + { // Fixture setup - var expectedResult = new Options_With_Required_Set_To_True_Within_Same_Set { + var expectedResult = new Options_With_Required_Set_To_True_Within_Same_Set + { FtpUrl = "str1", WebUrl = "str2" }; @@ -503,7 +464,8 @@ public void Two_required_options_at_the_same_set_and_both_are_true() { } [Fact] - public void Two_required_options_at_the_same_set_and_none_are_true() { + public void Two_required_options_at_the_same_set_and_none_are_true() + { // Fixture setup var expectedResult = new[] { @@ -516,12 +478,10 @@ public void Two_required_options_at_the_same_set_and_none_are_true() { // Verify outcome ((NotParsed)result).Errors.Should().BeEquivalentTo(expectedResult); - - // Teardown } [Fact] - public void Omitting_required_option_gererates_MissingRequiredOptionError() + public void Omitting_required_option_generates_MissingRequiredOptionError() { // Fixture setup var expectedResult = new[] { new MissingRequiredOptionError(new NameInfo("", "str")) }; @@ -532,12 +492,10 @@ public void Omitting_required_option_gererates_MissingRequiredOptionError() // Verify outcome ((NotParsed)result).Errors.Should().BeEquivalentTo(expectedResult); - - // Teardown } [Fact] - public void Wrong_range_in_sequence_gererates_SequenceOutOfRangeError() + public void Wrong_range_in_sequence_generates_SequenceOutOfRangeError() { // Fixture setup var expectedResult = new[] { new SequenceOutOfRangeError(new NameInfo("i", "")) }; @@ -548,12 +506,10 @@ public void Wrong_range_in_sequence_gererates_SequenceOutOfRangeError() // Verify outcome ((NotParsed)result).Errors.Should().BeEquivalentTo(expectedResult); - - // Teardown } [Fact] - public void Parse_unknown_long_option_gererates_UnknownOptionError() + public void Parse_unknown_long_option_generates_UnknownOptionError() { // Fixture setup var expectedResult = new[] { new UnknownOptionError("xyz") }; @@ -564,12 +520,10 @@ public void Parse_unknown_long_option_gererates_UnknownOptionError() // Verify outcome ((NotParsed)result).Errors.Should().BeEquivalentTo(expectedResult); - - // Teardown } [Fact] - public void Parse_unknown_short_option_gererates_UnknownOptionError() + public void Parse_unknown_short_option_generates_UnknownOptionError() { // Fixture setup var expectedResult = new[] { new UnknownOptionError("z") }; @@ -580,12 +534,10 @@ public void Parse_unknown_short_option_gererates_UnknownOptionError() // Verify outcome ((NotParsed)result).Errors.Should().BeEquivalentTo(expectedResult); - - // Teardown } [Fact] - public void Parse_unknown_short_option_in_option_group_gererates_UnknownOptionError() + public void Parse_unknown_short_option_in_option_group_generates_UnknownOptionError() { // Fixture setup var expectedResult = new[] { new UnknownOptionError("z") }; @@ -596,13 +548,11 @@ public void Parse_unknown_short_option_in_option_group_gererates_UnknownOptionEr // Verify outcome ((NotParsed)result).Errors.Should().BeEquivalentTo(expectedResult); - - // Teardown } [Theory] - [InlineData(new[] {"--stringvalue", "this-value"}, "this-value")] - [InlineData(new[] {"--stringvalue=this-other"}, "this-other")] + [InlineData(new[] { "--stringvalue", "this-value" }, "this-value")] + [InlineData(new[] { "--stringvalue=this-other" }, "this-other")] public void Omitting_names_assumes_identifier_as_long_name(string[] arguments, string expected) { // Fixture setup in attributes @@ -613,8 +563,6 @@ public void Omitting_names_assumes_identifier_as_long_name(string[] arguments, s // Verify outcome ((Parsed)result).Value.StringValue.Should().BeEquivalentTo(expected); - - // Teardown } [Fact] @@ -629,8 +577,6 @@ public void Breaking_required_constraint_in_string_scalar_as_value_generates_Mis // Verify outcome ((NotParsed)result).Errors.Should().BeEquivalentTo(expectedResult); - - // Teardown } [Theory] @@ -648,12 +594,10 @@ public void Parse_utf8_string_correctly(string[] arguments, string expected) // Verify outcome expected.Should().BeEquivalentTo(((Parsed)result).Value.StringValue); - - // Teardown } [Fact] - public void Breaking_equal_min_max_constraint_in_string_sequence_as_value_gererates_SequenceOutOfRangeError() + public void Breaking_equal_min_max_constraint_in_string_sequence_as_value_generates_SequenceOutOfRangeError() { // Fixture setup var expectedResult = new[] { new SequenceOutOfRangeError(NameInfo.EmptyName) }; @@ -664,8 +608,6 @@ public void Breaking_equal_min_max_constraint_in_string_sequence_as_value_gerera // Verify outcome ((NotParsed)result).Errors.Should().BeEquivalentTo(expectedResult); - - // Teardown } [Theory] @@ -683,8 +625,6 @@ public void Parse_nullable_int(string[] arguments, int? expected) // Verify outcome expected.Should().Be(((Parsed)result).Value.NullableInt); - - // Teardown } [Theory] @@ -702,8 +642,6 @@ public void Parse_nullable_long(string[] arguments, long? expected) // Verify outcome expected.Should().Be(((Parsed)result).Value.NullableLong); - - // Teardown } #if !SKIP_FSHARP @@ -724,8 +662,6 @@ public void Parse_fsharp_option_string(string[] arguments, string expectedValue, expectedValue.Should().BeEquivalentTo(((Parsed)result).Value.FileName.Value); } expectedSome.Should().Be(FSharpOption.get_IsSome(((Parsed)result).Value.FileName)); - - // Teardown } [Theory] @@ -745,8 +681,6 @@ public void Parse_fsharp_option_int(string[] arguments, int expectedValue, bool expectedValue.Should().Be(((Parsed)result).Value.Offset.Value); } expectedSome.Should().Be(FSharpOption.get_IsSome(((Parsed)result).Value.Offset)); - - // Teardown } #endif @@ -785,7 +719,7 @@ public void Min_and_max_constraint_set_to_zero_throws_exception() } [Theory] - [InlineData(new[] {"--weburl", "value.com", "--verbose"}, ParserResultType.Parsed, 0)] + [InlineData(new[] { "--weburl", "value.com", "--verbose" }, ParserResultType.Parsed, 0)] [InlineData(new[] { "--ftpurl", "value.org", "--interactive" }, ParserResultType.Parsed, 0)] [InlineData(new[] { "--weburl", "value.com", "--verbose", "--interactive" }, ParserResultType.Parsed, 0)] [InlineData(new[] { "--ftpurl=fvalue", "--weburl=wvalue" }, ParserResultType.NotParsed, 2)] @@ -855,6 +789,20 @@ public void Specifying_options_two_or_more_times_with_mixed_short_long_options_g ((NotParsed)result).Errors.Should().HaveCount(x => x == expected); } + [Theory] + [InlineData(new[] { "--inputfile=file1.bin" }, "file1.bin")] + [InlineData(new[] { "--inputfile", "file2.txt" }, "file2.txt")] + public void Can_define_options_on_explicit_interface_properties(string[] arguments, string expected) + { + // Exercize system + var result = InvokeBuild( + arguments); + + // Verify outcome + expected.Should().BeEquivalentTo(((IInterface_With_Two_Scalar_Options)((Parsed)result).Value).InputFile); + } + + [Theory] [InlineData(new[] { "--inputfile=file1.bin" }, "file1.bin")] [InlineData(new[] { "--inputfile", "file2.txt" }, "file2.txt")] @@ -905,8 +853,6 @@ public void Parse_string_scalar_with_required_constraint_as_value(string[] argum // Verify outcome expected.Should().BeEquivalentTo(((Parsed)result).Value); - - // Teardown } [Theory] @@ -921,15 +867,13 @@ public void Parse_string_scalar_and_sequence_adjacent(string[] arguments, Option // Verify outcome expected.Should().BeEquivalentTo(((Parsed)result).Value); - - // Teardown } [Fact] public void Parse_to_mutable() { // Fixture setup - var expectedResult = new Simple_Options { StringValue="strval0", IntSequence=new[] { 9, 7, 8 }, BoolValue = true, LongValue = 9876543210L }; + var expectedResult = new Simple_Options { StringValue = "strval0", IntSequence = new[] { 9, 7, 8 }, BoolValue = true, LongValue = 9876543210L }; // Exercize system var result = InvokeBuild( @@ -937,17 +881,15 @@ public void Parse_to_mutable() // Verify outcome expectedResult.Should().BeEquivalentTo(((Parsed)result).Value); - - // Teardown } [Theory] [InlineData(new string[] { }, 2)] - [InlineData(new [] { "--str=val0" }, 1)] - [InlineData(new [] { "--long=9" }, 1)] - [InlineData(new [] { "--int=7" }, 2)] - [InlineData(new [] { "--str", "val1", "--int=3" }, 1)] - [InlineData(new [] { "--long", "9", "--int=11" }, 1)] + [InlineData(new[] { "--str=val0" }, 1)] + [InlineData(new[] { "--long=9" }, 1)] + [InlineData(new[] { "--int=7" }, 2)] + [InlineData(new[] { "--str", "val1", "--int=3" }, 1)] + [InlineData(new[] { "--long", "9", "--int=11" }, 1)] public void Breaking_required_constraint_generate_MissingRequiredOptionError(string[] arguments, int expected) { // Exercize system @@ -971,8 +913,23 @@ public void Parse_to_immutable_instance(string[] arguments, Immutable_Simple_Opt // Verify outcome expected.Should().BeEquivalentTo(((Parsed)result).Value); + } - // Teardown + [Theory] + [MemberData(nameof(ImmutableInstanceDataArgs))] + [Trait("Category", "Immutable")] + public void Parse_to_immutable_instance_with_Invalid_Ctor_Args(string[] arguments) + { + // Fixture setup in attributes + + // Exercize system + Action act = () => InvokeBuildImmutable( + arguments); + + // Verify outcome + var expectedMsg = + "Type CommandLine.Tests.Fakes.Immutable_Simple_Options_Invalid_Ctor_Args appears to be Immutable with invalid constructor. Check that constructor arguments have the same name and order of their underlying Type. Constructor Parameters can be ordered as: '(stringvalue, intsequence, boolvalue, longvalue)'"; + act.Should().Throw().WithMessage(expectedMsg); } [Fact] @@ -987,8 +944,6 @@ public void Parse_to_type_with_single_string_ctor_builds_up_correct_instance() // Verify outcome expectedResult.Should().BeEquivalentTo(((Parsed)result).Value); - - // Teardown } [Fact] @@ -1003,8 +958,22 @@ public void Parse_option_with_exception_thrown_from_setter_generates_SetValueExc // Verify outcome ((NotParsed)result).Errors.Should().BeEquivalentTo(expectedResult); + } - // Teardown + [Fact] + public void Parse_default_bool_type_string_SetValueExceptionError() + { + // Fixture setup + string name = nameof(Options_With_InvalidDefaults.FileName).ToLower(); + var expectedResult = new[] { new SetValueExceptionError(new NameInfo("", name), + new ArgumentException(InvalidAttributeConfigurationError.ErrorMessage), "bad") }; + + // Exercize system + var result = InvokeBuild( + new[] { name, "bad" }); + + // Verify outcome + ((NotParsed)result).Errors.Should().BeEquivalentTo(expectedResult); } @@ -1027,8 +996,6 @@ public void Parse_string_with_dashes_except_in_beginning(string[] arguments, str // Verify outcome expected.Should().BeEquivalentTo(((Parsed)result).Value.StringValue); - - // Teardown } [Theory] @@ -1044,8 +1011,6 @@ public void Parse_without_auto_help_should_not_recognize_help_option(string[] ar result.Should().BeOfType>() .Which.Errors.Should().ContainSingle() .Which.Tag.Should().Be(errorType); - - // Teardown } [Theory] @@ -1062,8 +1027,6 @@ public void Parse_with_custom_help_option(string[] arguments, bool isHelp) // Verify outcome result.Should().BeOfType>() .Which.Value.Help.Should().Be(isHelp); - - // Teardown } [Theory] @@ -1079,8 +1042,6 @@ public void Parse_without_auto_version_should_not_recognize_version_option(strin result.Should().BeOfType>() .Which.Errors.Should().ContainSingle() .Which.Tag.Should().Be(errorType); - - // Teardown } [Theory] @@ -1097,8 +1058,6 @@ public void Parse_with_custom_version_option(string[] arguments, bool isVersion) // Verify outcome result.Should().BeOfType>() .Which.Value.MyVersion.Should().Be(isVersion); - - // Teardown } [Theory] @@ -1113,8 +1072,6 @@ public void Parse_Guid(string[] arguments, Options_With_Guid expected) // Verify outcome expected.Should().BeEquivalentTo(((Parsed)result).Value); - - // Teardown } [Fact] @@ -1129,26 +1086,228 @@ public void Parse_TimeSpan() // Verify outcome expectedResult.Should().BeEquivalentTo(((Parsed)result).Value); + } - // Teardown + #region Issue 579 + [Fact] + public void Should_not_parse_quoted_TimeSpan() + { + // Exercize system + var result = InvokeBuild(new[] { "--duration=\"00:42:00\"" }); + + var outcome = result as NotParsed; + + // Verify outcome + outcome.Should().NotBeNull(); + outcome.Errors.Should().NotBeNullOrEmpty() + .And.HaveCount(1) + .And.OnlyContain(e => e.GetType().Equals(typeof(BadFormatConversionError))); + } + #endregion + + [Fact] + public void OptionClass_IsImmutable_HasNoCtor() + { + Action act = () => InvokeBuild(new string[] { "Test" }, false, false); + + act.Should().Throw() + .Which.Message.Should().Be("Type CommandLine.Tests.Unit.Core.InstanceBuilderTests+ValueWithNoSetterOptions appears to be immutable, but no constructor found to accept values."); + } + + [Fact] + public void OptionClass_IsImmutable_HasNoCtor_HelpRequested() + { + Action act = () => InvokeBuild(new string[] { "--help" }); + + act.Should().Throw() + .Which.Message.Should().Be("Type CommandLine.Tests.Unit.Core.InstanceBuilderTests+ValueWithNoSetterOptions appears to be immutable, but no constructor found to accept values."); + } + + [Fact] + public void Options_In_Group_With_No_Values_Generates_MissingGroupOptionError() + { + // Fixture setup + var optionNames = new List + { + new NameInfo("", "option1"), + new NameInfo("", "option2") + }; + var expectedResult = new[] { new MissingGroupOptionError("err-group", optionNames) }; + + // Exercize system + var result = InvokeBuild( + new[] { "-v 10.42" }); + + // Verify outcome + ((NotParsed)result).Errors.Should().BeEquivalentTo(expectedResult); } [Fact] - public void Build_DefaultBoolTypeString_ThrowsInvalidOperationException() + 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 - Action test = () => InvokeBuild( - new string[] { }); + var result = InvokeBuild( + new[] { "-v 10.42" }); // Verify outcome - test.ShouldThrow() - .WithMessage(ReflectionExtensions.CannotSetValueToTargetInstance) - .WithInnerException() - .WithInnerMessage(InvalidAttributeConfigurationError.ErrorMessage); + ((NotParsed)result).Errors.Should().BeEquivalentTo(expectedResult); } + [Theory] + [InlineData("-v", "10.5", "--option1", "test1", "--option2", "test2")] + [InlineData("-v", "10.5", "--option1", "test1")] + [InlineData("-v", "10.5", "--option2", "test2")] + public void Options_In_Group_With_Values_Does_Not_Generate_MissingGroupOptionError(params string[] args) + { + // Exercize system + var result = InvokeBuild(args); + + // Verify outcome + result.Should().BeOfType>(); + } - public static IEnumerable RequiredValueStringData + [Fact] + public void Options_In_Group_WithRequired_Does_Not_Generate_RequiredError() + { + // Fixture setup + var optionNames = new List + { + new NameInfo("", "stingvalue"), + new NameInfo("s", "shortandlong") + }; + var expectedResult = new[] { new MissingGroupOptionError("string-group", optionNames) }; + + // Exercize system + var result = InvokeBuild(new string[] { "-x" }); + + // Verify outcome + result.Should().BeOfType>(); + var errors = ((NotParsed)result).Errors; + + errors.Should().HaveCount(1); + errors.Should().BeEquivalentTo(expectedResult); + } + + [Fact] + public void Options_In_Group_Ignore_Option_Group_If_Option_Group_Name_Empty() + { + var expectedResult = new[] + { + new MissingRequiredOptionError(new NameInfo("", "stringvalue")), + new MissingRequiredOptionError(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); + } + + [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); + } + + [Fact] + public void Parse_int_sequence_with_multi_instance() + { + var expected = new[] { 1, 2, 3 }; + var result = InvokeBuild( + new[] { "--int-seq", "1", "2", "--int-seq", "3" }, + multiInstance: true); + + ((Parsed)result).Value.IntSequence.Should().BeEquivalentTo(expected); + } + + #region custom types + + + [Theory] + [InlineData(new[] { "-c", "localhost:8080" }, "localhost", 8080)] + public void Parse_custom_struct_type(string[] arguments, string expectedServer, int expectedPort) + { + //Arrange + + // Act + var result = InvokeBuild(arguments); + + // Assert + var customValue = ((Parsed)result).Value.Custom; + customValue.Server.Should().Be(expectedServer); + customValue.Port.Should().Be(expectedPort); + customValue.Input.Should().Be(arguments[1]); + } + + [Theory] + [InlineData(new[] { "-c", "localhost:8080" }, "localhost", 8080)] + public void Parse_custom_class_type(string[] arguments, string expectedServer, int expectedPort) + { + //Arrange + + // Act + var result = InvokeBuild(arguments); + + // Assert + var customValue = ((Parsed)result).Value.Custom; + customValue.Server.Should().Be(expectedServer); + customValue.Port.Should().Be(expectedPort); + customValue.Input.Should().Be(arguments[1]); + } + + #endregion + private class ValueWithNoSetterOptions + { + [Value(0, MetaName = "Test", Default = 0)] + public int TestValue { get; } + } + + + public static IEnumerable RequiredValueStringData { get { @@ -1164,7 +1323,7 @@ public static IEnumerable ScalarSequenceStringAdjacentData { get { - yield return new object[] { new[] { "to-value" }, new Options_With_Scalar_Value_And_Adjacent_SequenceString { StringValueWithIndexZero = "to-value", StringOptionSequence = new string[] {} } }; + yield return new object[] { new[] { "to-value" }, new Options_With_Scalar_Value_And_Adjacent_SequenceString { StringValueWithIndexZero = "to-value", StringOptionSequence = new string[] { } } }; yield return new object[] { new[] { "to-value", "-s", "to-seq-0" }, new Options_With_Scalar_Value_And_Adjacent_SequenceString { StringValueWithIndexZero = "to-value", StringOptionSequence = new[] { "to-seq-0" } } }; yield return new object[] { new[] { "to-value", "-s", "to-seq-0", "to-seq-1" }, new Options_With_Scalar_Value_And_Adjacent_SequenceString { StringValueWithIndexZero = "to-value", StringOptionSequence = new[] { "to-seq-0", "to-seq-1" } } }; yield return new object[] { new[] { "-s", "cant-capture", "value-anymore" }, new Options_With_Scalar_Value_And_Adjacent_SequenceString { StringOptionSequence = new[] { "cant-capture", "value-anymore" } } }; @@ -1185,6 +1344,18 @@ public static IEnumerable ImmutableInstanceData yield return new object[] { new[] { "--stringvalue=strval0", "-i", "9", "7", "8", "-x", "9876543210" }, new Immutable_Simple_Options("strval0", new[] { 9, 7, 8 }, true, 9876543210L) }; } } + public static IEnumerable ImmutableInstanceDataArgs + { + get + { + yield return new object[] { new string[] { } } ; + yield return new object[] {new [] {"--stringvalue=strval0"}}; + yield return new object[] { new[] { "-i", "9", "7", "8" } }; + yield return new object[] { new[] { "-x" }}; + yield return new object[] { new[] { "9876543210" }}; + yield return new object[] { new[] { "--stringvalue=strval0", "-i", "9", "7", "8", "-x", "9876543210" }}; + } + } public static IEnumerable GuidData { diff --git a/tests/CommandLine.Tests/Unit/Core/InstanceChooserTests.cs b/tests/CommandLine.Tests/Unit/Core/InstanceChooserTests.cs index 52e12cae..d5cb9a21 100644 --- a/tests/CommandLine.Tests/Unit/Core/InstanceChooserTests.cs +++ b/tests/CommandLine.Tests/Unit/Core/InstanceChooserTests.cs @@ -4,10 +4,10 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; +using Xunit; +using FluentAssertions; using CommandLine.Core; using CommandLine.Tests.Fakes; -using FluentAssertions; -using Xunit; namespace CommandLine.Tests.Unit.Core { @@ -15,7 +15,8 @@ public class InstanceChooserTests { private static ParserResult InvokeChoose( IEnumerable types, - IEnumerable arguments) + IEnumerable arguments, + bool multiInstance = false) { return InstanceChooser.Choose( (args, optionSpecs) => Tokenizer.ConfigureTokenizer(StringComparer.Ordinal, false, false)(args, optionSpecs), @@ -26,6 +27,7 @@ private static ParserResult InvokeChoose( CultureInfo.InvariantCulture, true, true, + multiInstance, Enumerable.Empty()); } @@ -168,5 +170,18 @@ public void Parse_sequence_verb_with_separator_returns_verb_instance(string[] ar expected.Should().BeEquivalentTo(((Parsed)result).Value); // Teardown } + + [Fact] + public void Parse_sequence_verb_with_multi_instance_returns_verb_instance() + { + var expected = new SequenceOptions { LongSequence = new long[] { }, StringSequence = new[] { "s1", "s2" } }; + var result = InvokeChoose( + new[] { typeof(Add_Verb), typeof(Commit_Verb), typeof(Clone_Verb), typeof(SequenceOptions) }, + new[] { "sequence", "-s", "s1", "-s", "s2" }, + true); + + Assert.IsType(((Parsed)result).Value); + expected.Should().BeEquivalentTo(((Parsed)result).Value); + } } } diff --git a/tests/CommandLine.Tests/Unit/Core/KeyValuePairHelperTests.cs b/tests/CommandLine.Tests/Unit/Core/KeyValuePairHelperTests.cs index 77ba39cf..3f1fb293 100644 --- a/tests/CommandLine.Tests/Unit/Core/KeyValuePairHelperTests.cs +++ b/tests/CommandLine.Tests/Unit/Core/KeyValuePairHelperTests.cs @@ -2,8 +2,8 @@ using System.Collections.Generic; using System.Linq; -using CommandLine.Core; using Xunit; +using CommandLine.Core; namespace CommandLine.Tests.Unit.Core { diff --git a/tests/CommandLine.Tests/Unit/Core/NameLookupTests.cs b/tests/CommandLine.Tests/Unit/Core/NameLookupTests.cs index f27e033c..f009c49e 100644 --- a/tests/CommandLine.Tests/Unit/Core/NameLookupTests.cs +++ b/tests/CommandLine.Tests/Unit/Core/NameLookupTests.cs @@ -2,9 +2,9 @@ using System; using System.Collections.Generic; -using CommandLine.Core; -using FluentAssertions; using Xunit; +using FluentAssertions; +using CommandLine.Core; using CSharpx; namespace CommandLine.Tests.Unit.Core @@ -17,7 +17,7 @@ public void Lookup_name_of_sequence_option_with_separator() // Fixture setup var expected = Maybe.Just("."); var specs = new[] { new OptionSpecification(string.Empty, "string-seq", - false, string.Empty, Maybe.Nothing(), Maybe.Nothing(), '.', null, string.Empty, string.Empty, new List(), typeof(IEnumerable), TargetType.Sequence)}; + false, string.Empty, Maybe.Nothing(), Maybe.Nothing(), '.', null, string.Empty, string.Empty, new List(), typeof(IEnumerable), TargetType.Sequence, string.Empty)}; // Exercize system var result = NameLookup.HavingSeparator("string-seq", specs, StringComparer.Ordinal); @@ -35,7 +35,7 @@ public void Get_name_from_option_specification() // Fixture setup var expected = new NameInfo(ShortName, LongName); - var spec = new OptionSpecification(ShortName, LongName, false, string.Empty, Maybe.Nothing(), Maybe.Nothing(), '.', null, string.Empty, string.Empty, new List(), typeof(IEnumerable), TargetType.Sequence); + var spec = new OptionSpecification(ShortName, LongName, false, string.Empty, Maybe.Nothing(), Maybe.Nothing(), '.', null, string.Empty, string.Empty, new List(), typeof(IEnumerable), TargetType.Sequence, string.Empty); // Exercize system var result = spec.FromOptionSpecification(); diff --git a/tests/CommandLine.Tests/Unit/Core/OptionMapperTests.cs b/tests/CommandLine.Tests/Unit/Core/OptionMapperTests.cs index 460dac3b..0a948cea 100644 --- a/tests/CommandLine.Tests/Unit/Core/OptionMapperTests.cs +++ b/tests/CommandLine.Tests/Unit/Core/OptionMapperTests.cs @@ -7,11 +7,11 @@ #if PLATFORM_DOTNET using System.Reflection; #endif -using CommandLine.Core; -using CommandLine.Tests.Fakes; using Xunit; using CSharpx; using RailwaySharp.ErrorHandling; +using CommandLine.Core; +using CommandLine.Tests.Fakes; namespace CommandLine.Tests.Unit.Core { @@ -28,7 +28,7 @@ public void Map_boolean_switch_creates_boolean_value() var specProps = new[] { SpecificationProperty.Create( - new OptionSpecification("x", string.Empty, false, string.Empty, Maybe.Nothing(), Maybe.Nothing(), '\0', Maybe.Nothing(), string.Empty, string.Empty, new List(), typeof(bool), TargetType.Switch), + new OptionSpecification("x", string.Empty, false, string.Empty, Maybe.Nothing(), Maybe.Nothing(), '\0', Maybe.Nothing(), string.Empty, string.Empty, new List(), typeof(bool), TargetType.Switch, string.Empty), typeof(Simple_Options).GetProperties().Single(p => p.Name.Equals("BoolValue", StringComparison.Ordinal)), Maybe.Nothing()) }; @@ -37,7 +37,7 @@ public void Map_boolean_switch_creates_boolean_value() var result = OptionMapper.MapValues( specProps.Where(pt => pt.Specification.IsOption()), tokenPartitions, - (vals, type, isScalar) => TypeConverter.ChangeType(vals, type, isScalar, CultureInfo.InvariantCulture, false), + (vals, type, isScalar, isFlag) => TypeConverter.ChangeType(vals, type, isScalar, isFlag, CultureInfo.InvariantCulture, false), StringComparer.Ordinal ); @@ -49,5 +49,67 @@ public void Map_boolean_switch_creates_boolean_value() // Teardown } + + [Fact] + public void Map_with_multi_instance_scalar() + { + var tokenPartitions = new[] + { + new KeyValuePair>("s", new[] { "string1" }), + new KeyValuePair>("shortandlong", new[] { "string2" }), + new KeyValuePair>("shortandlong", new[] { "string3" }), + new KeyValuePair>("s", new[] { "string4" }), + }; + + var specProps = new[] + { + SpecificationProperty.Create( + new OptionSpecification("s", "shortandlong", false, string.Empty, Maybe.Nothing(), Maybe.Nothing(), '\0', Maybe.Nothing(), string.Empty, string.Empty, new List(), typeof(string), TargetType.Scalar, string.Empty), + typeof(Simple_Options).GetProperties().Single(p => p.Name.Equals(nameof(Simple_Options.ShortAndLong), StringComparison.Ordinal)), + Maybe.Nothing()), + }; + + var result = OptionMapper.MapValues( + specProps.Where(pt => pt.Specification.IsOption()), + tokenPartitions, + (vals, type, isScalar, isFlag) => TypeConverter.ChangeType(vals, type, isScalar, isFlag, CultureInfo.InvariantCulture, false), + StringComparer.Ordinal); + + var property = result.SucceededWith().Single(); + Assert.True(property.Specification.IsOption()); + Assert.True(property.Value.MatchJust(out var stringVal)); + Assert.Equal(tokenPartitions.Last().Value.Last(), stringVal); + } + + [Fact] + public void Map_with_multi_instance_sequence() + { + var tokenPartitions = new[] + { + new KeyValuePair>("i", new [] { "1", "2" }), + new KeyValuePair>("i", new [] { "3" }), + new KeyValuePair>("i", new [] { "4", "5" }), + }; + var specProps = new[] + { + SpecificationProperty.Create( + new OptionSpecification("i", string.Empty, false, string.Empty, Maybe.Nothing(), Maybe.Nothing(), '\0', Maybe.Nothing(), string.Empty, string.Empty, new List(), typeof(IEnumerable), TargetType.Sequence, string.Empty), + typeof(Simple_Options).GetProperties().Single(p => p.Name.Equals(nameof(Simple_Options.IntSequence), StringComparison.Ordinal)), + Maybe.Nothing()) + }; + + var result = OptionMapper.MapValues( + specProps.Where(pt => pt.Specification.IsOption()), + tokenPartitions, + (vals, type, isScalar, isFlag) => TypeConverter.ChangeType(vals, type, isScalar, isFlag, CultureInfo.InvariantCulture, false), + StringComparer.Ordinal); + + var property = result.SucceededWith().Single(); + Assert.True(property.Specification.IsOption()); + Assert.True(property.Value.MatchJust(out var sequence)); + + var expected = tokenPartitions.Aggregate(Enumerable.Empty(), (prev, part) => prev.Concat(part.Value.Select(i => int.Parse(i)))); + Assert.Equal(expected, sequence); + } } } diff --git a/tests/CommandLine.Tests/Unit/Core/ReflectionExtensions.cs b/tests/CommandLine.Tests/Unit/Core/ReflectionExtensions.cs index 2fdb951d..035b9e03 100644 --- a/tests/CommandLine.Tests/Unit/Core/ReflectionExtensions.cs +++ b/tests/CommandLine.Tests/Unit/Core/ReflectionExtensions.cs @@ -1,9 +1,9 @@ // Copyright 2005-2015 Giacomo Stelluti Scala & Contributors. All rights reserved. See License.md in the project root for license information. +using Xunit; +using FluentAssertions; using CommandLine.Core; using CommandLine.Tests.Fakes; -using FluentAssertions; -using Xunit; namespace CommandLine.Tests.Unit.Infrastructure { diff --git a/tests/CommandLine.Tests/Unit/Core/ScalarTests.cs b/tests/CommandLine.Tests/Unit/Core/ScalarTests.cs index 2984b77e..2d08bbcb 100644 --- a/tests/CommandLine.Tests/Unit/Core/ScalarTests.cs +++ b/tests/CommandLine.Tests/Unit/Core/ScalarTests.cs @@ -1,10 +1,10 @@ // Copyright 2005-2015 Giacomo Stelluti Scala & Contributors. All rights reserved. See License.md in the project root for license information. using System.Linq; -using CommandLine.Core; -using CSharpx; using Xunit; using FluentAssertions; +using CSharpx; +using CommandLine.Core; namespace CommandLine.Tests.Unit.Core { @@ -15,12 +15,13 @@ public void Partition_scalar_values_from_empty_token_sequence() { var expected = new Token[] { }; - var result = Scalar.Partition( + var tokens = TokenPartitioner.PartitionTokensByType( new Token[] { }, name => new[] { "str", "int" }.Contains(name) ? Maybe.Just(TypeDescriptor.Create(TargetType.Scalar, Maybe.Nothing())) : Maybe.Nothing()); + var result = tokens.Item2; // Switch, *Scalar*, Sequence, NonOption expected.Should().BeEquivalentTo(result); } @@ -30,7 +31,7 @@ public void Partition_scalar_values() { var expected = new [] { Token.Name("str"), Token.Value("strvalue") }; - var result = Scalar.Partition( + var tokens = TokenPartitioner.PartitionTokensByType( new [] { Token.Name("str"), Token.Value("strvalue"), Token.Value("freevalue"), @@ -40,6 +41,7 @@ public void Partition_scalar_values() new[] { "str", "int" }.Contains(name) ? Maybe.Just(TypeDescriptor.Create(TargetType.Scalar, Maybe.Nothing())) : Maybe.Nothing()); + var result = tokens.Item2; // Switch, *Scalar*, Sequence, NonOption expected.Should().BeEquivalentTo(result); } diff --git a/tests/CommandLine.Tests/Unit/Core/SequenceTests.cs b/tests/CommandLine.Tests/Unit/Core/SequenceTests.cs index 36e3e262..74a3d877 100644 --- a/tests/CommandLine.Tests/Unit/Core/SequenceTests.cs +++ b/tests/CommandLine.Tests/Unit/Core/SequenceTests.cs @@ -1,10 +1,10 @@ // Copyright 2005-2015 Giacomo Stelluti Scala & Contributors. All rights reserved. See License.md in the project root for license information. using System.Linq; -using CommandLine.Core; -using CSharpx; using Xunit; using FluentAssertions; +using CSharpx; +using CommandLine.Core; namespace CommandLine.Tests.Unit.Core { @@ -15,12 +15,13 @@ public void Partition_sequence_values_from_empty_token_sequence() { var expected = new Token[] { }; - var result = Sequence.Partition( + var tokens = TokenPartitioner.PartitionTokensByType( new Token[] { }, name => new[] { "seq" }.Contains(name) ? Maybe.Just(TypeDescriptor.Create(TargetType.Sequence, Maybe.Nothing())) : Maybe.Nothing()); + var result = tokens.Item3; // Switch, Scalar, *Sequence*, NonOption expected.Should().AllBeEquivalentTo(result); } @@ -33,7 +34,7 @@ public void Partition_sequence_values() Token.Name("seq"), Token.Value("seqval0"), Token.Value("seqval1") }; - var result = Sequence.Partition( + var tokens = TokenPartitioner.PartitionTokensByType( new[] { Token.Name("str"), Token.Value("strvalue"), Token.Value("freevalue"), @@ -44,6 +45,7 @@ public void Partition_sequence_values() new[] { "seq" }.Contains(name) ? Maybe.Just(TypeDescriptor.Create(TargetType.Sequence, Maybe.Nothing())) : Maybe.Nothing()); + var result = tokens.Item3; // Switch, Scalar, *Sequence*, NonOption expected.Should().BeEquivalentTo(result); } @@ -57,7 +59,7 @@ public void Partition_sequence_values_from_two_sequneces() Token.Name("seqb"), Token.Value("seqbval0") }; - var result = Sequence.Partition( + var tokens = TokenPartitioner.PartitionTokensByType( new[] { Token.Name("str"), Token.Value("strvalue"), Token.Value("freevalue"), @@ -69,6 +71,7 @@ public void Partition_sequence_values_from_two_sequneces() new[] { "seq", "seqb" }.Contains(name) ? Maybe.Just(TypeDescriptor.Create(TargetType.Sequence, Maybe.Nothing())) : Maybe.Nothing()); + var result = tokens.Item3; // Switch, Scalar, *Sequence*, NonOption expected.Should().BeEquivalentTo(result); } @@ -81,7 +84,7 @@ public void Partition_sequence_values_only() Token.Name("seq"), Token.Value("seqval0"), Token.Value("seqval1") }; - var result = Sequence.Partition( + var tokens = TokenPartitioner.PartitionTokensByType( new[] { Token.Name("seq"), Token.Value("seqval0"), Token.Value("seqval1") @@ -90,8 +93,83 @@ public void Partition_sequence_values_only() new[] { "seq" }.Contains(name) ? Maybe.Just(TypeDescriptor.Create(TargetType.Sequence, Maybe.Nothing())) : Maybe.Nothing()); + var result = tokens.Item3; // Switch, Scalar, *Sequence*, NonOption expected.Should().BeEquivalentTo(result); } + + [Fact] + public void Partition_sequence_multi_instance() + { + var expected = new[] + { + Token.Name("seq"), + Token.Value("seqval0"), + Token.Value("seqval1"), + Token.Value("seqval2"), + Token.Value("seqval3"), + Token.Value("seqval4"), + }; + + var tokens = TokenPartitioner.PartitionTokensByType( + new[] + { + Token.Name("str"), Token.Value("strvalue"), Token.Value("freevalue"), + Token.Name("seq"), Token.Value("seqval0"), Token.Value("seqval1"), + Token.Name("x"), Token.Value("freevalue2"), + Token.Name("seq"), Token.Value("seqval2"), Token.Value("seqval3"), + Token.Name("seq"), Token.Value("seqval4") + }, + name => + new[] { "seq" }.Contains(name) + ? Maybe.Just(TypeDescriptor.Create(TargetType.Sequence, Maybe.Nothing())) + : Maybe.Nothing()); + var result = tokens.Item3; // Switch, Scalar, *Sequence*, NonOption + + var actual = result.ToArray(); + Assert.Equal(expected, actual); + } + + [Fact] + public void Partition_sequence_multi_instance_with_max() + { + var incorrect = new[] + { + Token.Name("seq"), + Token.Value("seqval0"), + Token.Value("seqval1"), + Token.Value("seqval2"), + Token.Value("seqval3"), + Token.Value("seqval4"), + Token.Value("seqval5"), + }; + + var expected = new[] + { + Token.Name("seq"), + Token.Value("seqval0"), + Token.Value("seqval1"), + Token.Value("seqval2"), + }; + + var tokens = TokenPartitioner.PartitionTokensByType( + new[] + { + Token.Name("str"), Token.Value("strvalue"), Token.Value("freevalue"), + Token.Name("seq"), Token.Value("seqval0"), Token.Value("seqval1"), + Token.Name("x"), Token.Value("freevalue2"), + Token.Name("seq"), Token.Value("seqval2"), Token.Value("seqval3"), + Token.Name("seq"), Token.Value("seqval4"), Token.Value("seqval5"), + }, + name => + new[] { "seq" }.Contains(name) + ? Maybe.Just(TypeDescriptor.Create(TargetType.Sequence, Maybe.Just(3))) + : Maybe.Nothing()); + var result = tokens.Item3; // Switch, Scalar, *Sequence*, NonOption + + // Max of 3 will apply to the total values, so there should only be 3 values, not 6 + Assert.NotEqual(incorrect, result); + Assert.Equal(expected, result); + } } } diff --git a/tests/CommandLine.Tests/Unit/Core/SpecificationPropertyRulesTests.cs b/tests/CommandLine.Tests/Unit/Core/SpecificationPropertyRulesTests.cs new file mode 100644 index 00000000..6e055c55 --- /dev/null +++ b/tests/CommandLine.Tests/Unit/Core/SpecificationPropertyRulesTests.cs @@ -0,0 +1,58 @@ +using CommandLine.Core; +using CommandLine.Tests.Fakes; +using CSharpx; +using System.Collections.Generic; +using Xunit; + +namespace CommandLine.Tests.Unit.Core +{ + + public class SpecificationPropertyRulesTests + { + [Fact] + public void Lookup_allows_multi_instance() + { + var tokens = new[] + { + Token.Name("name"), + Token.Value("value"), + Token.Name("name"), + Token.Value("value2"), + }; + + var specProps = new[] + { + SpecificationProperty.Create( + new OptionSpecification(string.Empty, "name", false, string.Empty, Maybe.Nothing(), Maybe.Nothing(), '\0', Maybe.Nothing(), string.Empty, string.Empty, new List(), typeof(IEnumerable), TargetType.Sequence, string.Empty), + typeof(SequenceOptions).GetProperty(nameof(SequenceOptions.StringSequence)), + Maybe.Just(new object())), + }; + + var results = specProps.Validate(SpecificationPropertyRules.Lookup(tokens, true)); + Assert.Empty(results); + } + + [Fact] + public void Lookup_fails_with_repeated_options_false_multi_instance() + { + var tokens = new[] + { + Token.Name("name"), + Token.Value("value"), + Token.Name("name"), + Token.Value("value2"), + }; + + var specProps = new[] + { + SpecificationProperty.Create( + new OptionSpecification(string.Empty, "name", false, string.Empty, Maybe.Nothing(), Maybe.Nothing(), '\0', Maybe.Nothing(), string.Empty, string.Empty, new List(), typeof(IEnumerable), TargetType.Sequence, string.Empty), + typeof(SequenceOptions).GetProperty(nameof(SequenceOptions.StringSequence)), + Maybe.Just(new object())), + }; + + var results = specProps.Validate(SpecificationPropertyRules.Lookup(tokens, false)); + Assert.Contains(results, r => r.GetType() == typeof(RepeatedOptionError)); + } + } +} diff --git a/tests/CommandLine.Tests/Unit/Core/SwitchTests.cs b/tests/CommandLine.Tests/Unit/Core/SwitchTests.cs index a4163990..0fc6db70 100644 --- a/tests/CommandLine.Tests/Unit/Core/SwitchTests.cs +++ b/tests/CommandLine.Tests/Unit/Core/SwitchTests.cs @@ -1,10 +1,10 @@ // Copyright 2005-2015 Giacomo Stelluti Scala & Contributors. All rights reserved. See License.md in the project root for license information. using System.Linq; -using CommandLine.Core; -using CSharpx; using Xunit; using FluentAssertions; +using CSharpx; +using CommandLine.Core; namespace CommandLine.Tests.Unit.Core { @@ -15,12 +15,13 @@ public void Partition_switch_values_from_empty_token_sequence() { var expected = new Token[] { }; - var result = Switch.Partition( + var tokens = TokenPartitioner.PartitionTokensByType( new Token[] { }, name => new[] { "x", "switch" }.Contains(name) ? Maybe.Just(TypeDescriptor.Create(TargetType.Switch, Maybe.Nothing())) : Maybe.Nothing()); + var result = tokens.Item1; // *Switch*, Scalar, Sequence, NonOption expected.Should().BeEquivalentTo(result); } @@ -30,7 +31,7 @@ public void Partition_switch_values() { var expected = new [] { Token.Name("x") }; - var result = Switch.Partition( + var tokens = TokenPartitioner.PartitionTokensByType( new [] { Token.Name("str"), Token.Value("strvalue"), Token.Value("freevalue"), @@ -40,6 +41,7 @@ public void Partition_switch_values() new[] { "x", "switch" }.Contains(name) ? Maybe.Just(TypeDescriptor.Create(TargetType.Switch, Maybe.Nothing())) : Maybe.Nothing()); + var result = tokens.Item1; // *Switch*, Scalar, Sequence, NonOption expected.Should().BeEquivalentTo(result); } diff --git a/tests/CommandLine.Tests/Unit/Core/TextWrapperTests.cs b/tests/CommandLine.Tests/Unit/Core/TextWrapperTests.cs new file mode 100644 index 00000000..8de39610 --- /dev/null +++ b/tests/CommandLine.Tests/Unit/Core/TextWrapperTests.cs @@ -0,0 +1,198 @@ +using System; +using System.Linq; +using Xunit; +using FluentAssertions; +using CSharpx; +using CommandLine.Text; + +namespace CommandLine.Tests.Unit.Core +{ + public class TextWrapperTests + { + private string NormalizeLineBreaks(string str) + { + return str.Replace("\r", ""); + } + + private void EnsureEquivalent(string a, string b) + { + //workaround build system line-end inconsistencies + NormalizeLineBreaks(a).Should().Be(NormalizeLineBreaks(b)); + } + + + [Fact] + public void ExtraSpacesAreTreatedAsNonBreaking() + { + var input = + "here is some text with some extra spacing"; + var expected = @"here is some text +with some extra +spacing"; + var wrapper = new TextWrapper(input); + EnsureEquivalent(wrapper.WordWrap(20).ToText(), expected); + } + + + [Fact] + public void IndentWorksCorrectly() + { + var input = + @"line1 +line2"; + var expected = @" line1 + line2"; + var wrapper = new TextWrapper(input); + EnsureEquivalent(wrapper.Indent(2).ToText(), expected); + } + + [Fact] + public void LongWordsAreBroken() + { + var input = + "here is some text that contains a veryLongWordThatWontFitOnASingleLine"; + var expected = @"here is some text +that contains a +veryLongWordThatWont +FitOnASingleLine"; + var wrapper = new TextWrapper(input); + EnsureEquivalent(wrapper.WordWrap(20).ToText(), expected); + } + + [Fact] + public void NegativeColumnWidthStillProducesOutput() + { + var input = @"test"; + var expected = string.Join(Environment.NewLine, input.Select(c => c.ToString())); + var wrapper = new TextWrapper(input); + EnsureEquivalent(wrapper.WordWrap(-1).ToText(), expected); + } + + [Fact] + public void SimpleWrappingIsAsExpected() + { + var input = + @"here is some text that needs wrapping"; + var expected = @"here is +some text +that needs +wrapping"; + var wrapper = new TextWrapper(input); + EnsureEquivalent(wrapper.WordWrap(10).ToText(), expected); + } + + [Fact] + public void SingleColumnStillProducesOutputForSubIndentation() + { + var input = @"test + ind"; + + var expected = @"t +e +s +t +i +n +d"; + var wrapper = new TextWrapper(input); + EnsureEquivalent(wrapper.WordWrap(-1).ToText(), expected); + } + + [Fact] + public void SpacesWithinStringAreRespected() + { + var input = + "here is some text with some extra spacing"; + var expected = @"here is some +text with some extra +spacing"; + var wrapper = new TextWrapper(input); + EnsureEquivalent(wrapper.WordWrap(20).ToText(), expected); + } + + [Fact] + public void SubIndentationCorrectlyWrapsWhenColumnWidthRequiresIt() + { + var input = @"test + indented"; + var expected = @"test + in + de + nt + ed"; + var wrapper = new TextWrapper(input); + EnsureEquivalent(wrapper.WordWrap(6).ToText(), expected); + } + + [Fact] + public void SubIndentationIsPreservedWhenBreakingWords() + { + var input = + "here is some text that contains \n a veryLongWordThatWontFitOnASingleLine"; + var expected = @"here is some text +that contains + a + veryLongWordThatWo + ntFitOnASingleLine"; + var wrapper = new TextWrapper(input); + EnsureEquivalent(wrapper.WordWrap(20).ToText(), expected); + } + + [Fact] + public void WrappingAvoidsBreakingWords() + { + var input = + @"here hippopotamus is some text that needs wrapping"; + var expected = @"here +hippopotamus is +some text that +needs wrapping"; + var wrapper = new TextWrapper(input); + EnsureEquivalent(wrapper.WordWrap(15).ToText(), expected); + } + + + [Fact] + public void WrappingExtraSpacesObeySubIndent() + { + var input = + "here is some\n text with some extra spacing"; + var expected = @"here is some + text + with some extra + spacing"; + var wrapper = new TextWrapper(input); + EnsureEquivalent(wrapper.WordWrap(20).ToText(), expected); + } + + [Fact] + public void WrappingObeysLineBreaksOfAllStyles() + { + var input = + "here is some text\nthat needs\r\nwrapping"; + var expected = @"here is some text +that needs +wrapping"; + var wrapper = new TextWrapper(input); + EnsureEquivalent(wrapper.WordWrap(20).ToText(), expected); + } + + + [Fact] + public void WrappingPreservesSubIndentation() + { + var input = + "here is some text\n that needs wrapping where we want the wrapped part to preserve indentation\nand this part to not be indented"; + var expected = @"here is some text + that needs + wrapping where we + want the wrapped + part to preserve + indentation +and this part to not +be indented"; + var wrapper = new TextWrapper(input); + EnsureEquivalent(wrapper.WordWrap(20).ToText(), expected); + } + } +} diff --git a/tests/CommandLine.Tests/Unit/Core/TokenPartitionerTests.cs b/tests/CommandLine.Tests/Unit/Core/TokenPartitionerTests.cs index d787645d..20006e59 100644 --- a/tests/CommandLine.Tests/Unit/Core/TokenPartitionerTests.cs +++ b/tests/CommandLine.Tests/Unit/Core/TokenPartitionerTests.cs @@ -3,9 +3,9 @@ using System; using System.Collections.Generic; using System.Linq; -using CommandLine.Core; -using CSharpx; using Xunit; +using CSharpx; +using CommandLine.Core; namespace CommandLine.Tests.Unit.Core { @@ -17,12 +17,12 @@ public void Partition_sequence_returns_sequence() // Fixture setup var expectedSequence = new[] { - new KeyValuePair>("i", new[] {"10", "20", "30", "40"}) + new KeyValuePair>("i", new[] {"10", "20", "30", "40"}) }; - var specs =new[] + var specs = new[] { - new OptionSpecification(string.Empty, "stringvalue", false, string.Empty, Maybe.Nothing(), Maybe.Nothing(), '\0', null, string.Empty, string.Empty, new List(), typeof(string), TargetType.Scalar), - new OptionSpecification("i", string.Empty, false, string.Empty, Maybe.Just(3), Maybe.Just(4), '\0', null, string.Empty, string.Empty, new List(), typeof(IEnumerable), TargetType.Sequence) + new OptionSpecification(string.Empty, "stringvalue", false, string.Empty, Maybe.Nothing(), Maybe.Nothing(), '\0', null, string.Empty, string.Empty, new List(), typeof(string), TargetType.Scalar, string.Empty), + new OptionSpecification("i", string.Empty, false, string.Empty, Maybe.Just(3), Maybe.Just(4), '\0', null, string.Empty, string.Empty, new List(), typeof(IEnumerable), TargetType.Sequence, string.Empty) }; // Exercize system @@ -44,12 +44,12 @@ public void Partition_sequence_returns_sequence_with_duplicates() // Fixture setup var expectedSequence = new[] { - new KeyValuePair>("i", new[] {"10", "10", "30", "40"}) + new KeyValuePair>("i", new[] {"10", "10", "30", "40"}) }; - var specs =new[] + var specs = new[] { - new OptionSpecification(string.Empty, "stringvalue", false, string.Empty, Maybe.Nothing(), Maybe.Nothing(), '\0', null, string.Empty, string.Empty, new List(), typeof(string), TargetType.Scalar), - new OptionSpecification("i", string.Empty, false, string.Empty, Maybe.Just(3), Maybe.Just(4), '\0', null, string.Empty, string.Empty, new List(), typeof(IEnumerable), TargetType.Sequence) + new OptionSpecification(string.Empty, "stringvalue", false, string.Empty, Maybe.Nothing(), Maybe.Nothing(), '\0', null, string.Empty, string.Empty, new List(), typeof(string), TargetType.Scalar, string.Empty), + new OptionSpecification("i", string.Empty, false, string.Empty, Maybe.Just(3), Maybe.Just(4), '\0', null, string.Empty, string.Empty, new List(), typeof(IEnumerable), TargetType.Sequence, string.Empty) }; // Exercize system diff --git a/tests/CommandLine.Tests/Unit/Core/TokenTests.cs b/tests/CommandLine.Tests/Unit/Core/TokenTests.cs index 991171db..1290f9b3 100644 --- a/tests/CommandLine.Tests/Unit/Core/TokenTests.cs +++ b/tests/CommandLine.Tests/Unit/Core/TokenTests.cs @@ -1,7 +1,7 @@ // Copyright 2005-2015 Giacomo Stelluti Scala & Contributors. All rights reserved. See License.md in the project root for license information. -using CommandLine.Core; using Xunit; +using CommandLine.Core; namespace CommandLine.Tests.Unit.Core { diff --git a/tests/CommandLine.Tests/Unit/Core/TokenizerTests.cs b/tests/CommandLine.Tests/Unit/Core/TokenizerTests.cs index ecb21266..ea7268be 100644 --- a/tests/CommandLine.Tests/Unit/Core/TokenizerTests.cs +++ b/tests/CommandLine.Tests/Unit/Core/TokenizerTests.cs @@ -3,15 +3,12 @@ using System; using System.Collections.Generic; using System.Linq; -using CommandLine.Core; -using CommandLine.Infrastructure; - using Xunit; -using CSharpx; - using FluentAssertions; - +using CSharpx; using RailwaySharp.ErrorHandling; +using CommandLine.Core; +using CommandLine.Infrastructure; namespace CommandLine.Tests.Unit.Core { @@ -24,7 +21,7 @@ public void Explode_scalar_with_separator_in_odd_args_input_returns_sequence() var expectedTokens = new[] { Token.Name("i"), Token.Value("10"), Token.Name("string-seq"), Token.Value("aaa"), Token.Value("bb"), Token.Value("cccc"), Token.Name("switch") }; var specs = new[] { new OptionSpecification(string.Empty, "string-seq", - false, string.Empty, Maybe.Nothing(), Maybe.Nothing(), ',', null, string.Empty, string.Empty, new List(), typeof(IEnumerable), TargetType.Sequence)}; + false, string.Empty, Maybe.Nothing(), Maybe.Nothing(), ',', null, string.Empty, string.Empty, new List(), typeof(IEnumerable), TargetType.Sequence, string.Empty)}; // Exercize system var result = @@ -47,7 +44,7 @@ public void Explode_scalar_with_separator_in_even_args_input_returns_sequence() var expectedTokens = new[] { Token.Name("x"), Token.Name("string-seq"), Token.Value("aaa"), Token.Value("bb"), Token.Value("cccc"), Token.Name("switch") }; var specs = new[] { new OptionSpecification(string.Empty, "string-seq", - false, string.Empty, Maybe.Nothing(), Maybe.Nothing(), ',', null, string.Empty, string.Empty, new List(), typeof(IEnumerable), TargetType.Sequence)}; + false, string.Empty, Maybe.Nothing(), Maybe.Nothing(), ',', null, string.Empty, string.Empty, new List(), typeof(IEnumerable), TargetType.Sequence, string.Empty)}; // Exercize system var result = @@ -65,26 +62,54 @@ public void Explode_scalar_with_separator_in_even_args_input_returns_sequence() } [Fact] - public void Normalize_should_remove_all_value_with_explicit_assignment_of_existing_name() + public void Normalize_should_remove_all_names_and_values_with_explicit_assignment_of_non_existing_names() { // Fixture setup var expectedTokens = new[] { - Token.Name("x"), Token.Name("string-seq"), Token.Value("aaa"), Token.Value("bb"), - Token.Name("unknown"), Token.Name("switch") }; + Token.Name("x"), Token.Name("string-seq"), Token.Value("value0", true), Token.Value("bb"), + Token.Name("switch") }; Func nameLookup = name => name.Equals("x") || name.Equals("string-seq") || name.Equals("switch"); // Exercize system var result = Tokenizer.Normalize( - //Result.Succeed( + //Result.Succeed( Enumerable.Empty() .Concat( new[] { - Token.Name("x"), Token.Name("string-seq"), Token.Value("aaa"), Token.Value("bb"), + Token.Name("x"), Token.Name("string-seq"), Token.Value("value0", true), Token.Value("bb"), Token.Name("unknown"), Token.Value("value0", true), Token.Name("switch") }) - //,Enumerable.Empty()), - ,nameLookup); + //,Enumerable.Empty()), + , nameLookup); + + // Verify outcome + result.Should().BeEquivalentTo(expectedTokens); + + // Teardown + } + + [Fact] + public void Normalize_should_remove_all_names_of_non_existing_names() + { + // Fixture setup + var expectedTokens = new[] { + Token.Name("x"), Token.Name("string-seq"), Token.Value("value0", true), Token.Value("bb"), + Token.Name("switch") }; + Func nameLookup = + name => name.Equals("x") || name.Equals("string-seq") || name.Equals("switch"); + + // Exercize system + var result = + Tokenizer.Normalize( + //Result.Succeed( + Enumerable.Empty() + .Concat( + new[] { + Token.Name("x"), Token.Name("string-seq"), Token.Value("value0", true), Token.Value("bb"), + Token.Name("unknown"), Token.Name("switch") }) + //,Enumerable.Empty()), + , nameLookup); // Verify outcome result.Should().BeEquivalentTo(expectedTokens); @@ -119,13 +144,32 @@ 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()); Assert.Equal(ErrorType.BadFormatTokenError, tokens.First().Tag); Assert.Equal(ErrorType.BadFormatTokenError, tokens.Last().Tag); } + + [Theory] + [InlineData(new[] { "-a", "-" }, 2,"a","-")] + [InlineData(new[] { "--file", "-" }, 2,"file","-")] + [InlineData(new[] { "-f-" }, 2,"f", "-")] + [InlineData(new[] { "--file=-" }, 2, "file", "-")] + [InlineData(new[] { "-a", "--" }, 1, "a", "a")] + public void Single_dash_as_a_value(string[] args, int countExcepted,string first,string last) + { + //Arrange + //Act + var result = Tokenizer.Tokenize(args, name => NameLookupResult.OtherOptionFound, token => token); + var tokens = result.SucceededWith().ToList(); + //Assert + tokens.Should().NotBeNull(); + tokens.Count.Should().Be(countExcepted); + tokens.First().Text.Should().Be(first); + tokens.Last().Text.Should().Be(last); + } } - + } diff --git a/tests/CommandLine.Tests/Unit/Core/TypeConverterTests.cs b/tests/CommandLine.Tests/Unit/Core/TypeConverterTests.cs index 90782b4b..c3e93781 100644 --- a/tests/CommandLine.Tests/Unit/Core/TypeConverterTests.cs +++ b/tests/CommandLine.Tests/Unit/Core/TypeConverterTests.cs @@ -1,10 +1,10 @@ using System; using System.Collections.Generic; using System.Globalization; -using CommandLine.Core; -using CSharpx; -using FluentAssertions; using Xunit; +using FluentAssertions; +using CSharpx; +using CommandLine.Core; namespace CommandLine.Tests.Unit.Core { @@ -16,11 +16,18 @@ enum TestEnum ValueB = 2 } + [Flags] + enum TestFlagEnum + { + ValueA = 0x1, + ValueB = 0x2 + } + [Theory] [MemberData(nameof(ChangeType_scalars_source))] public void ChangeType_scalars(string testValue, Type destinationType, bool expectFail, object expectedResult) { - Maybe result = TypeConverter.ChangeType(new[] {testValue}, destinationType, true, CultureInfo.InvariantCulture, true); + Maybe result = TypeConverter.ChangeType(new[] {testValue}, destinationType, true, false, CultureInfo.InvariantCulture, true); if (expectFail) { @@ -50,8 +57,8 @@ public static IEnumerable ChangeType_scalars_source new object[] {((long) int.MinValue - 1).ToString(), typeof (int), true, null}, new object[] {"1", typeof (uint), false, (uint) 1}, - new object[] {"0", typeof (uint), false, (uint) 0}, - new object[] {"-1", typeof (uint), true, null}, + // new object[] {"0", typeof (uint), false, (uint) 0}, //cause warning: Skipping test case with duplicate ID + // new object[] {"-1", typeof (uint), true, null}, //cause warning: Skipping test case with duplicate ID new object[] {uint.MaxValue.ToString(), typeof (uint), false, uint.MaxValue}, new object[] {uint.MinValue.ToString(), typeof (uint), false, uint.MinValue}, new object[] {((long) uint.MaxValue + 1).ToString(), typeof (uint), true, null}, @@ -94,11 +101,66 @@ public static IEnumerable ChangeType_scalars_source new object[] {((int) TestEnum.ValueB + 1).ToString(), typeof (TestEnum), true, null}, new object[] {((int) TestEnum.ValueA - 1).ToString(), typeof (TestEnum), true, null}, + new object[] {"ValueA", typeof (TestFlagEnum), false, TestFlagEnum.ValueA}, + new object[] {"VALUEA", typeof (TestFlagEnum), false, TestFlagEnum.ValueA}, + new object[] {"ValueB", typeof(TestFlagEnum), false, TestFlagEnum.ValueB}, + new object[] {"ValueA,ValueB", typeof (TestFlagEnum), false, TestFlagEnum.ValueA | TestFlagEnum.ValueB}, + new object[] {"ValueA, ValueB", typeof (TestFlagEnum), false, TestFlagEnum.ValueA | TestFlagEnum.ValueB}, + new object[] {"VALUEA,ValueB", typeof (TestFlagEnum), false, TestFlagEnum.ValueA | TestFlagEnum.ValueB}, + new object[] {((int) TestFlagEnum.ValueA).ToString(), typeof (TestFlagEnum), false, TestFlagEnum.ValueA}, + new object[] {((int) TestFlagEnum.ValueB).ToString(), typeof (TestFlagEnum), false, TestFlagEnum.ValueB}, + new object[] {((int) (TestFlagEnum.ValueA | TestFlagEnum.ValueB)).ToString(), typeof (TestFlagEnum), false, TestFlagEnum.ValueA | TestFlagEnum.ValueB}, + new object[] {((int) TestFlagEnum.ValueB + 2).ToString(), typeof (TestFlagEnum), true, null}, + new object[] {((int) TestFlagEnum.ValueA - 1).ToString(), typeof (TestFlagEnum), true, null}, + + // Failed before #339 new object[] {"false", typeof (int), true, 0}, new object[] {"true", typeof (int), true, 0} }; } } + + [Theory] + [MemberData(nameof(ChangeType_flagCounters_source))] + public void ChangeType_flagCounters(string[] testValue, Type destinationType, bool expectFail, object expectedResult) + { + Maybe result = TypeConverter.ChangeType(testValue, destinationType, true, true, CultureInfo.InvariantCulture, true); + + if (expectFail) + { + result.MatchNothing().Should().BeTrue("should fail parsing"); + } + else + { + result.MatchJust(out object matchedValue).Should().BeTrue("should parse successfully"); + Assert.Equal(matchedValue, expectedResult); + } + } + + public static IEnumerable ChangeType_flagCounters_source + { + get + { + return new[] + { + new object[] {new string[0], typeof (int), false, 0}, + new object[] {new[] {"true"}, typeof (int), false, 1}, + new object[] {new[] {"true", "true"}, typeof (int), false, 2}, + new object[] {new[] {"true", "true", "true"}, typeof (int), false, 3}, + new object[] {new[] {"true", "x"}, typeof (int), true, 0}, + }; + } + } + + [Fact] + public void ChangeType_Scalar_LastOneWins() + { + var values = new[] { "100", "200", "300", "400", "500" }; + var result = TypeConverter.ChangeType(values, typeof(int), true, false, CultureInfo.InvariantCulture, true); + result.MatchJust(out var matchedValue).Should().BeTrue("should parse successfully"); + Assert.Equal(500, matchedValue); + + } } } diff --git a/tests/CommandLine.Tests/Unit/GetoptParserTests.cs b/tests/CommandLine.Tests/Unit/GetoptParserTests.cs new file mode 100644 index 00000000..cd2a1577 --- /dev/null +++ b/tests/CommandLine.Tests/Unit/GetoptParserTests.cs @@ -0,0 +1,284 @@ +using System; +using Xunit; +using FluentAssertions; +using CommandLine.Core; +using CommandLine.Tests.Fakes; +using System.IO; +using System.Linq; +using System.Collections.Generic; + +namespace CommandLine.Tests.Unit +{ + public class GetoptParserTests + { + public GetoptParserTests() + { + } + + public class SimpleArgsData : TheoryData + { + public SimpleArgsData() + { + // Options and values can be mixed by default + Add(new string [] { "--stringvalue=foo", "-x", "256" }, + new Simple_Options_WithExtraArgs { + IntSequence = Enumerable.Empty(), + ShortAndLong = null, + StringValue = "foo", + BoolValue = true, + LongValue = 256, + ExtraArgs = Enumerable.Empty(), + }); + Add(new string [] { "256", "--stringvalue=foo", "-x" }, + new Simple_Options_WithExtraArgs { + StringValue = "foo", + ShortAndLong = null, + IntSequence = Enumerable.Empty(), + BoolValue = true, + LongValue = 256, + ExtraArgs = Enumerable.Empty(), + }); + Add(new string [] {"--stringvalue=foo", "256", "-x" }, + new Simple_Options_WithExtraArgs { + StringValue = "foo", + ShortAndLong = null, + IntSequence = Enumerable.Empty(), + BoolValue = true, + LongValue = 256, + ExtraArgs = Enumerable.Empty(), + }); + + // Sequences end at first non-value arg even if they haven't yet consumed their max + Add(new string [] {"--stringvalue=foo", "-i1", "2", "3", "-x", "256" }, + new Simple_Options_WithExtraArgs { + StringValue = "foo", + ShortAndLong = null, + IntSequence = new[] { 1, 2, 3 }, + BoolValue = true, + LongValue = 256, + ExtraArgs = Enumerable.Empty(), + }); + // Sequences also end after consuming their max even if there would be more parameters + Add(new string [] {"--stringvalue=foo", "-i1", "2", "3", "4", "256", "-x" }, + new Simple_Options_WithExtraArgs { + StringValue = "foo", + ShortAndLong = null, + IntSequence = new[] { 1, 2, 3, 4 }, + BoolValue = true, + LongValue = 256, + ExtraArgs = Enumerable.Empty(), + }); + + // The special -- option, if not consumed, turns off further option processing + Add(new string [] {"--stringvalue", "foo", "256", "-x", "-sbar" }, + new Simple_Options_WithExtraArgs { + StringValue = "foo", + ShortAndLong = "bar", + BoolValue = true, + LongValue = 256, + IntSequence = Enumerable.Empty(), + ExtraArgs = Enumerable.Empty(), + }); + Add(new string [] {"--stringvalue", "foo", "--", "256", "-x", "-sbar" }, + new Simple_Options_WithExtraArgs { + StringValue = "foo", + ShortAndLong = null, + BoolValue = false, + LongValue = 256, + IntSequence = Enumerable.Empty(), + ExtraArgs = new [] { "-x", "-sbar" }, + }); + + // But if -- is specified as a value following an equal sign, it has no special meaning + Add(new string [] {"--stringvalue=--", "256", "-x", "-sbar" }, + new Simple_Options_WithExtraArgs { + StringValue = "--", + ShortAndLong = "bar", + BoolValue = true, + LongValue = 256, + IntSequence = Enumerable.Empty(), + ExtraArgs = Enumerable.Empty(), + }); + + // Options that take values will take the next arg whatever it looks like + Add(new string [] {"--stringvalue", "-x", "256" }, + new Simple_Options_WithExtraArgs { + StringValue = "-x", + BoolValue = false, + LongValue = 256, + IntSequence = Enumerable.Empty(), + ExtraArgs = Enumerable.Empty(), + }); + Add(new string [] {"--stringvalue", "-x", "-x", "256" }, + new Simple_Options_WithExtraArgs { + StringValue = "-x", + BoolValue = true, + LongValue = 256, + IntSequence = Enumerable.Empty(), + ExtraArgs = Enumerable.Empty(), + }); + + // That applies even if the next arg is -- which would normally stop option processing: if it's after an option that takes a value, it's consumed as the value + Add(new string [] {"--stringvalue", "--", "256", "-x", "-sbar" }, + new Simple_Options_WithExtraArgs { + StringValue = "--", + ShortAndLong = "bar", + BoolValue = true, + LongValue = 256, + IntSequence = Enumerable.Empty(), + ExtraArgs = Enumerable.Empty(), + }); + + // Options that take values will not swallow the next arg if a value was specified with = + Add(new string [] {"--stringvalue=-x", "256" }, + new Simple_Options_WithExtraArgs { + StringValue = "-x", + BoolValue = false, + LongValue = 256, + IntSequence = Enumerable.Empty(), + ExtraArgs = Enumerable.Empty(), + }); + Add(new string [] {"--stringvalue=-x", "-x", "256" }, + new Simple_Options_WithExtraArgs { + StringValue = "-x", + BoolValue = true, + LongValue = 256, + IntSequence = Enumerable.Empty(), + ExtraArgs = Enumerable.Empty(), + }); + } + } + + [Theory] + [ClassData(typeof(SimpleArgsData))] + public void Getopt_parser_without_posixly_correct_allows_mixed_options_and_nonoptions(string[] args, Simple_Options_WithExtraArgs expected) + { + // Arrange + var sut = new Parser(config => { + config.GetoptMode = true; + config.PosixlyCorrect = false; + }); + + // Act + var result = sut.ParseArguments(args); + + // Assert + if (result is Parsed parsed) { + parsed.Value.Should().BeEquivalentTo(expected); + } else if (result is NotParsed notParsed) { + Console.WriteLine(String.Join(", ", notParsed.Errors.Select(err => err.Tag.ToString()))); + } + result.Should().BeOfType>(); + result.As>().Value.Should().BeEquivalentTo(expected); + } + + public class SimpleArgsDataWithPosixlyCorrect : TheoryData + { + public SimpleArgsDataWithPosixlyCorrect() + { + Add(new string [] { "--stringvalue=foo", "-x", "256" }, + // Parses all options + new Simple_Options_WithExtraArgs { + StringValue = "foo", + ShortAndLong = null, + IntSequence = Enumerable.Empty(), + BoolValue = true, + LongValue = 256, + ExtraArgs = Enumerable.Empty(), + }); + Add(new string [] { "256", "--stringvalue=foo", "-x" }, + // Stops parsing after "256", so StringValue and BoolValue not set + new Simple_Options_WithExtraArgs { + StringValue = null, + ShortAndLong = null, + IntSequence = Enumerable.Empty(), + BoolValue = false, + LongValue = 256, + ExtraArgs = new string[] { "--stringvalue=foo", "-x" }, + }); + Add(new string [] {"--stringvalue=foo", "256", "-x" }, + // Stops parsing after "256", so StringValue is set but BoolValue is not + new Simple_Options_WithExtraArgs { + StringValue = "foo", + ShortAndLong = null, + IntSequence = Enumerable.Empty(), + BoolValue = false, + LongValue = 256, + ExtraArgs = new string[] { "-x" }, + }); + } + } + + [Theory] + [ClassData(typeof(SimpleArgsDataWithPosixlyCorrect))] + public void Getopt_parser_with_posixly_correct_stops_parsing_at_first_nonoption(string[] args, Simple_Options_WithExtraArgs expected) + { + // Arrange + var sut = new Parser(config => { + config.GetoptMode = true; + config.PosixlyCorrect = true; + config.EnableDashDash = true; + }); + + // Act + var result = sut.ParseArguments(args); + + // Assert + result.Should().BeOfType>(); + result.As>().Value.Should().BeEquivalentTo(expected); + } + + [Fact] + public void Getopt_mode_defaults_to_EnableDashDash_being_true() + { + // Arrange + var sut = new Parser(config => { + config.GetoptMode = true; + config.PosixlyCorrect = false; + }); + var args = new string [] {"--stringvalue", "foo", "256", "--", "-x", "-sbar" }; + var expected = new Simple_Options_WithExtraArgs { + StringValue = "foo", + ShortAndLong = null, + BoolValue = false, + LongValue = 256, + IntSequence = Enumerable.Empty(), + ExtraArgs = new [] { "-x", "-sbar" }, + }; + + // Act + var result = sut.ParseArguments(args); + + // Assert + result.Should().BeOfType>(); + result.As>().Value.Should().BeEquivalentTo(expected); + } + + [Fact] + public void Getopt_mode_can_have_EnableDashDash_expicitly_disabled() + { + // Arrange + var sut = new Parser(config => { + config.GetoptMode = true; + config.PosixlyCorrect = false; + config.EnableDashDash = false; + }); + var args = new string [] {"--stringvalue", "foo", "256", "--", "-x", "-sbar" }; + var expected = new Simple_Options_WithExtraArgs { + StringValue = "foo", + ShortAndLong = "bar", + BoolValue = true, + LongValue = 256, + IntSequence = Enumerable.Empty(), + ExtraArgs = new [] { "--" }, + }; + + // Act + var result = sut.ParseArguments(args); + + // Assert + result.Should().BeOfType>(); + result.As>().Value.Should().BeEquivalentTo(expected); + } + } +} diff --git a/tests/CommandLine.Tests/Unit/Issue104Tests.cs b/tests/CommandLine.Tests/Unit/Issue104Tests.cs new file mode 100644 index 00000000..ca35689e --- /dev/null +++ b/tests/CommandLine.Tests/Unit/Issue104Tests.cs @@ -0,0 +1,67 @@ +using System.Linq; +using Xunit; +using FluentAssertions; +using CommandLine.Tests.Fakes; +using CommandLine.Text; + +//Issue #104 +//When outputting HelpText, the code will correctly list valid values for enum type options. However, if the option is a nullable enum type, then it will not list the valid values. + +namespace CommandLine.Tests.Unit +{ + public class Issue104Tests + { + + [Fact] + public void Create_instance_with_enum_options_enabled_and_nullable_enum() + { + // Fixture setup + // Exercize system + var sut = new HelpText { AddDashesToOption = true, AddEnumValuesToHelpText = true, MaximumDisplayWidth = 80 } + .AddPreOptionsLine("pre-options") + .AddOptions(new NotParsed(TypeInfo.Create(typeof(Options_With_Enum_Having_HelpText)), Enumerable.Empty())) + .AddPostOptionsLine("post-options"); + + // Verify outcome + + var lines = sut.ToString().ToNotEmptyLines().TrimStringArray(); + lines[0].Should().BeEquivalentTo("pre-options"); + lines[1].Should().BeEquivalentTo("--stringvalue Define a string value here."); + lines[2].Should().BeEquivalentTo("--shape Define a enum value here. Valid values: Circle, Square,"); + lines[3].Should().BeEquivalentTo("Triangle"); + lines[4].Should().BeEquivalentTo("--help Display this help screen."); + lines[5].Should().BeEquivalentTo("--version Display version information."); + lines[6].Should().BeEquivalentTo("post-options"); + // Teardown + } + + [Fact] + public void Help_with_enum_options_enabled_and_nullable_enum() + { + // Fixture setup + // Exercize system + var args = "--help".Split(); + var sut = new Parser(config => config.HelpWriter = null); + var parserResult = sut.ParseArguments(args); + HelpText helpText = null; + parserResult.WithNotParsed(errors => + { + // Use custom help text to ensure valid enum values are displayed + helpText = HelpText.AutoBuild(parserResult); + helpText.AddEnumValuesToHelpText = true; + helpText.AddOptions(parserResult); + }); + + // Verify outcome + + var lines = helpText.ToString().ToNotEmptyLines().TrimStringArray(); + lines[2].Should().BeEquivalentTo("--stringvalue Define a string value here."); + lines[3].Should().BeEquivalentTo("--shape Define a enum value here. Valid values: Circle, Square,"); + lines[4].Should().BeEquivalentTo("Triangle"); + lines[5].Should().BeEquivalentTo("--help Display this help screen."); + lines[6].Should().BeEquivalentTo("--version Display version information."); + // Teardown + } + } + +} diff --git a/tests/CommandLine.Tests/Unit/Issue389Tests.cs b/tests/CommandLine.Tests/Unit/Issue389Tests.cs new file mode 100644 index 00000000..5c81e6e0 --- /dev/null +++ b/tests/CommandLine.Tests/Unit/Issue389Tests.cs @@ -0,0 +1,76 @@ +using System.Collections.Generic; +using System.Linq; +using Xunit; +using CommandLine.Text; + +namespace CommandLine.Tests.Unit +{ + //Reference: PR# 392 + public class Issue389Tests + { + + private const int ERROR_SUCCESS = 0; + + // Test method (xUnit) which fails + [Fact] + public void CallMain_GiveHelpArgument_ExpectSuccess() + { + var result = Program.__Main(new[] { "--help" }); + + Assert.Equal(ERROR_SUCCESS, result); + } + + // main program + internal class Program + { + + + internal static int __Main(string[] args) + { + bool hasError = false; + bool helpOrVersionRequested = false; + + ParserResult parsedOptions = Parser.Default.ParseArguments(args) + .WithNotParsed(errors => { + helpOrVersionRequested = errors.Any( + x => x.Tag == ErrorType.HelpRequestedError + || x.Tag == ErrorType.VersionRequestedError); + hasError = true; + }); + + if(helpOrVersionRequested) + { + return ERROR_SUCCESS; + } + + // Execute as a normal call + // ... + return ERROR_SUCCESS; + } + + } + + // Options + internal class Options + { + + [Option('c', "connectionString", Required = true, HelpText = "Texts.ExplainConnection")] + public string ConnectionString { get; set; } + + [Option('j', "jobId", Required = true, HelpText = "Texts.ExplainJob")] + public int JobId { get; set; } + + [Usage(ApplicationAlias = "Importer.exe")] + public static IEnumerable Examples + { + get => new[] { + new Example("Texts.ExplainExampleExecution", new Options() { + ConnectionString="Server=MyServer;Database=MyDatabase", + JobId = 5 + }), + }; + } + + } + } +} diff --git a/tests/CommandLine.Tests/Unit/Issue418Tests.cs b/tests/CommandLine.Tests/Unit/Issue418Tests.cs new file mode 100644 index 00000000..cac1e9f1 --- /dev/null +++ b/tests/CommandLine.Tests/Unit/Issue418Tests.cs @@ -0,0 +1,32 @@ +using System; +using System.IO; +using FluentAssertions; +using Xunit; +using CommandLine.Tests.Fakes; + +namespace CommandLine.Tests.Unit +{ + //issue#418, --version does not print a new line at the end cause trouble in Linux + public class Issue418Tests + { + + [Fact] + public void Explicit_version_request_generates_version_info_screen_with_eol() + { + // Fixture setup + var help = new StringWriter(); + var sut = new Parser(config => config.HelpWriter = help); + + // Exercize system + sut.ParseArguments(new[] { "--version" }); + var result = help.ToString(); + // Verify outcome + var lines = result.ToNotEmptyLines(); + result.Length.Should().BeGreaterThan(0); + result.Should().EndWith(Environment.NewLine); + result.ToNotEmptyLines().Length.Should().Be(1); + + // Teardown + } + } +} diff --git a/tests/CommandLine.Tests/Unit/Issue424Tests.cs b/tests/CommandLine.Tests/Unit/Issue424Tests.cs new file mode 100644 index 00000000..c828c3ed --- /dev/null +++ b/tests/CommandLine.Tests/Unit/Issue424Tests.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using Xunit; + +namespace CommandLine.Tests.Unit +{ + + //MailAndSmsWarningSenderTests + public class Issue424Tests + { + private MailAndSmsWarningSender _sut; + + public Issue424Tests() + { + _sut = new MailAndSmsWarningSender(); + } + + [Fact] + public void SendSmsOnWarning() + { + //Arrange + void Action() => _sut.ParseArgumentsAndRun( + new[] { "--task", "MailAndSmsWarningSender", "--test", "hejtest" }); + // Act & Assert + Assert.Throws((Action)Action); + } + } + + public class MailAndSmsWarningSender + { + internal class Options + { + [Option("task")] + public string Task { get; set; } + } + + public void ParseArgumentsAndRun(string[] args) + { + Parser.Default.ParseArguments(args) + .WithParsed(ExecuteTaskWithOptions) + .WithNotParsed(HandleParseError); + } + + private void HandleParseError(IEnumerable errs) + { + throw new NotImplementedException(); + } + + private void ExecuteTaskWithOptions(Options opts) + { + Console.WriteLine("Executing"); + } + + } +} \ No newline at end of file diff --git a/tests/CommandLine.Tests/Unit/Issue482Tests.cs b/tests/CommandLine.Tests/Unit/Issue482Tests.cs new file mode 100644 index 00000000..9d2ea971 --- /dev/null +++ b/tests/CommandLine.Tests/Unit/Issue482Tests.cs @@ -0,0 +1,186 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using CommandLine.Tests.Fakes; +using CommandLine.Text; +using Xunit; +using FluentAssertions; + +namespace CommandLine.Tests.Unit +{ + public class Issue482Tests + { + [Fact] + public void AutoBuild_without_ordering() + { + string expectedCompany = "Company"; + + + var parser = Parser.Default; + var parseResult = parser.ParseArguments( + new[] { "verb1", "--help" }) + .WithNotParsed(errors => { ; }) + .WithParsed(args => {; }); + + var message = HelpText.AutoBuild(parseResult, + error =>error, + ex => ex + ); + + string helpMessage = message.ToString(); + var helps = helpMessage.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries).Skip(2).ToList(); + List expected = new List() + { + " -a, --alpha Required.", + " -b, --alpha2 Required.", + " -d, --charlie", + " -c, --bravo", + "-f, --foxtrot", + "-e, --echo", + "--help Display this help screen.", + "--version Display version information.", + "value pos. 0" + }; + expected.Count.Should().Be(helps.Count); + int i = 0; + foreach (var expect in expected) + { + expect.Trim().Should().Be(helps[i].Trim()); + i++; + } + + ; + } + + [Fact] + public void AutoBuild_with_ordering() + { + string expectedCompany = "Company"; + + + var parser = Parser.Default; + var parseResult = parser.ParseArguments( + new[] { "verb1", "--help" }) + .WithNotParsed(errors => { ; }) + .WithParsed(args => {; }); + + Comparison comparison = HelpText.RequiredThenAlphaComparison; + + string message = HelpText.AutoBuild(parseResult, + error => + { + error.OptionComparison = HelpText.RequiredThenAlphaComparison; + return error; + }, + ex => ex); + + + string helpMessage = message.ToString(); + var helps = helpMessage.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries).Skip(2).ToList(); + List expected = new List() + { + " -a, --alpha Required.", + " -b, --alpha2 Required.", + " -c, --bravo", + " -d, --charlie", + "-e, --echo", + "-f, --foxtrot", + "--help Display this help screen.", + "--version Display version information.", + "value pos. 0" + }; + expected.Count.Should().Be(helps.Count); + int i = 0; + foreach (var expect in expected) + { + expect.Trim().Should().Be(helps[i].Trim()); + i++; + } + + ; + } + + [Fact] + public void AutoBuild_with_ordering_on_shortName() + { + string expectedCompany = "Company"; + + + var parser = Parser.Default; + var parseResult = parser.ParseArguments( + new[] { "verb1", "--help" }) + .WithNotParsed(errors => { ; }) + .WithParsed(args => {; }); + + Comparison orderOnShortName = (ComparableOption attr1, ComparableOption attr2) => + { + if (attr1.IsOption && attr2.IsOption) + { + if (attr1.Required && !attr2.Required) + { + return -1; + } + else if (!attr1.Required && attr2.Required) + { + return 1; + } + else + { + if (string.IsNullOrEmpty(attr1.ShortName) && !string.IsNullOrEmpty(attr2.ShortName)) + { + return 1; + } + else if (!string.IsNullOrEmpty(attr1.ShortName) && string.IsNullOrEmpty(attr2.ShortName)) + { + return -1; + } + return String.Compare(attr1.ShortName, attr2.ShortName, StringComparison.Ordinal); + } + } + else if (attr1.IsOption && attr2.IsValue) + { + return -1; + } + else + { + return 1; + } + }; + + string message = HelpText.AutoBuild(parseResult, + error => + { + error.OptionComparison = orderOnShortName; + return error; + }, + ex => ex, + false, + 80 + ); + + + var helps = message.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries).Skip(2).ToList(); + List expected = new List() + { + " -a, --alpha Required.", + " -b, --alpha2 Required.", + " -c, --bravo", + " -d, --charlie", + "-e, --echo", + "-f, --foxtrot", + "--help Display this help screen.", + "--version Display version information.", + "value pos. 0" + }; + expected.Count.Should().Be(helps.Count); + int i = 0; + foreach (var expect in expected) + { + expect.Trim().Should().Be(helps[i].Trim()); + i++; + } + } + + + } +} diff --git a/tests/CommandLine.Tests/Unit/Issue543Tests.cs b/tests/CommandLine.Tests/Unit/Issue543Tests.cs new file mode 100644 index 00000000..61a83512 --- /dev/null +++ b/tests/CommandLine.Tests/Unit/Issue543Tests.cs @@ -0,0 +1,108 @@ +using System.Collections.Generic; +using System.Linq; +using Xunit; +using CommandLine.Text; + +namespace CommandLine.Tests.Unit +{ + //Reference: PR# 634 + public class Issue543Tests + { + + private const int ERROR_SUCCESS = 0; + + [Fact] + public void Parser_GiveHelpArgument_ExpectSuccess() + { + var result = Parser.Default.ParseArguments(new[] { "--help" }); + + Assert.Equal(ParserResultType.NotParsed, result.Tag); + Assert.Null(result.Value); + Assert.NotEmpty(result.Errors); + } + + [Fact] + public void Parser_GiveConnectionStringAndJobId_ExpectSuccess() + { + var result = Parser.Default.ParseArguments(new[] { + "-c", "someConnectionString", + "-j", "1234", + }); + + Assert.Equal(ParserResultType.Parsed, result.Tag); + Assert.NotNull(result.Value); + Assert.Empty(result.Errors); + Assert.Equal("someConnectionString", result.Value.ConnectionString); + Assert.Equal(1234, result.Value.JobId); + } + + [Fact] + public void Parser_GiveVerb1_ExpectSuccess() + { + var result = Parser.Default.ParseArguments(new[] { + "verb1", + "-j", "1234", + }); + + Assert.Equal(ParserResultType.Parsed, result.Tag); + Assert.Empty(result.Errors); + Assert.NotNull(result.Value); + Assert.NotNull(result.Value as Verb1Options); + Assert.Equal(1234, (result.Value as Verb1Options).JobId); + } + + [Fact] + public void Parser_GiveVerb2_ExpectSuccess() + { + var result = Parser.Default.ParseArguments(new[] { + "verb2", + "-c", "someConnectionString", + }); + + Assert.Equal(ParserResultType.Parsed, result.Tag); + Assert.Empty(result.Errors); + Assert.NotNull(result.Value); + Assert.NotNull(result.Value as Verb2Options); + Assert.Equal("someConnectionString", (result.Value as Verb2Options).ConnectionString); + } + + // Options + internal class Options + { + [Option('c', "connectionString", Required = true, HelpText = "Texts.ExplainConnection")] + public string ConnectionString { get; set; } + + [Option('j', "jobId", Required = true, HelpText = "Texts.ExplainJob")] + public int JobId { get; set; } + + [Usage(ApplicationAlias = "Importer.exe")] + public static IEnumerable Examples + { + get => new[] { + new Example("Texts.ExplainExampleExecution", new Options() { + ConnectionString="Server=MyServer;Database=MyDatabase", + JobId = 5 + }), + }; + } + } + + // Options + [Verb("verb1")] + internal class Verb1Options + { + [Option('j', "jobId", Required = false, HelpText = "Texts.ExplainJob")] + public int JobId { get; set; } + } + + // Options + [Verb("verb2")] + internal class Verb2Options + { + [Option('c', "connectionString", Required = false, HelpText = "Texts.ExplainConnection")] + public string ConnectionString { get; set; } + } + + } +} + diff --git a/tests/CommandLine.Tests/Unit/Issue591Tests.cs b/tests/CommandLine.Tests/Unit/Issue591Tests.cs new file mode 100644 index 00000000..41b66b74 --- /dev/null +++ b/tests/CommandLine.Tests/Unit/Issue591Tests.cs @@ -0,0 +1,29 @@ +using System.Linq; +using CommandLine.Tests.Fakes; +using CommandLine.Text; +using FluentAssertions; +using Xunit; +using Xunit.Abstractions; + +//Issue #591 +//When options class is only having explicit interface declarations, it should be detected as mutable. + +namespace CommandLine.Tests.Unit +{ + public class Issue591Tests + { + [Fact] + public void Parse_option_with_only_explicit_interface_implementation() + { + string actual = string.Empty; + + var arguments = new[] { "--inputfile", "file2.txt" }; + var result = Parser.Default.ParseArguments(arguments); + result.WithParsed(options => { + actual = ((IInterface_With_Two_Scalar_Options)options).InputFile; + }); + + actual.Should().Be("file2.txt"); + } + } +} diff --git a/tests/CommandLine.Tests/Unit/Issue6Tests.cs b/tests/CommandLine.Tests/Unit/Issue6Tests.cs new file mode 100644 index 00000000..2f6ea3f4 --- /dev/null +++ b/tests/CommandLine.Tests/Unit/Issue6Tests.cs @@ -0,0 +1,255 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using CommandLine.Tests.Fakes; +using CommandLine.Text; +using FluentAssertions; +using Microsoft.FSharp.Core; +using Xunit; +using Xunit.Abstractions; + +//Issue #6 +//Support Aliases on verbs (i.e. "move" and "mv" are the same verb). + +namespace CommandLine.Tests.Unit +{ + public class Issue6Tests + { + /// + /// Test Verb aliases when one verb is set as a default + /// + /// + /// + [Theory] + [InlineData("move -a bob", typeof(AliasedVerbOption1))] + [InlineData("mv -a bob", typeof(AliasedVerbOption1))] + [InlineData("copy -a bob", typeof(AliasedVerbOption2))] + [InlineData("cp -a bob", typeof(AliasedVerbOption2))] + [InlineData("-a bob", typeof(AliasedVerbOption2))] + public void Parse_option_with_aliased_verbs(string args, Type expectedArgType) + { + var arguments = args.Split(' '); + object options = null; + IEnumerable errors = null; + var result = Parser.Default.ParseArguments(arguments) + .WithParsed(o => options = o) + .WithNotParsed(o => errors = o) + ; + if (errors != null && errors.Any()) + { + foreach (Error e in errors) + { + System.Console.WriteLine(e.ToString()); + } + } + + Assert.NotNull(options); + Assert.Equal(expectedArgType, options.GetType()); + } + + /// + /// Test verb aliases with no default verb and 1 verb with no aliases + /// + /// + /// + [Theory] + [InlineData("move -a bob", typeof(AliasedVerbOption1))] + [InlineData("mv -a bob", typeof(AliasedVerbOption1))] + [InlineData("delete -b fred", typeof(VerbNoAlias))] + public void Parse_option_with_aliased_verb(string args, Type expectedArgType) + { + var arguments = args.Split(' '); + object options = null; + IEnumerable errors = null; + var result = Parser.Default.ParseArguments(arguments) + .WithParsed(o => options = o) + .WithNotParsed(o => errors = o) + ; + if (errors != null && errors.Any()) + { + foreach (Error e in errors) + { + System.Console.WriteLine(e.ToString()); + } + } + + Assert.NotNull(options); + Assert.Equal(expectedArgType, options.GetType()); + } + + /// + /// Verify auto-help generation. + /// + /// + /// + /// + [Theory] + [InlineData("--help", true, new string[] + { + "copy, cp, cpy (Default Verb) Copy some stuff", + "move, mv", + "delete Delete stuff", + "help Display more information on a specific command.", + "version Display version information.", + })] + [InlineData("help", true, new string[] + { + "copy, cp, cpy (Default Verb) Copy some stuff", + "move, mv", + "delete Delete stuff", + "help Display more information on a specific command.", + "version Display version information.", + })] + [InlineData("move --help", false, new string[] + { + "-a, --alpha Required.", + "--help Display this help screen.", + "--version Display version information.", + })] + [InlineData("mv --help", false, new string[] + { + "-a, --alpha Required.", + "--help Display this help screen.", + "--version Display version information.", + })] + [InlineData("delete --help", false, new string[] + { + "-b, --beta Required.", + "--help Display this help screen.", + "--version Display version information.", + })] + public void Parse_help_option_for_aliased_verbs(string args, bool verbsIndex, string[] expected) + { + var arguments = args.Split(' '); + object options = null; + IEnumerable errors = null; + // the order of the arguments here drives the order of the commands shown + // in the help message + var result = Parser.Default.ParseArguments< + AliasedVerbOption2, + AliasedVerbOption1, + VerbNoAlias + >(arguments) + .WithParsed(o => options = o) + .WithNotParsed(o => errors = o) + ; + + var message = HelpText.AutoBuild(result, + error => error, + ex => ex, + verbsIndex: verbsIndex + ); + + string helpMessage = message.ToString(); + var helps = helpMessage.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries).Skip(2).ToList(); + + expected.Length.Should().Be(helps.Count); + int i = 0; + foreach (var expect in expected) + { + helps[i].Trim().Should().Be(expect); + i++; + } + } + + /// + /// Verify auto-help generation with no default verb. + /// + /// + /// + /// + [Theory] + [InlineData("--help", true, new string[] + { + "move, mv", + "delete Delete stuff", + "help Display more information on a specific command.", + "version Display version information.", + })] + [InlineData("help", true, new string[] + { + "move, mv", + "delete Delete stuff", + "help Display more information on a specific command.", + "version Display version information.", + })] + [InlineData("move --help", false, new string[] + { + "-a, --alpha Required.", + "--help Display this help screen.", + "--version Display version information.", + })] + [InlineData("mv --help", false, new string[] + { + "-a, --alpha Required.", + "--help Display this help screen.", + "--version Display version information.", + })] + [InlineData("delete --help", false, new string[] + { + "-b, --beta Required.", + "--help Display this help screen.", + "--version Display version information.", + })] + public void Parse_help_option_for_aliased_verbs_no_default(string args, bool verbsIndex, string[] expected) + { + var arguments = args.Split(' '); + object options = null; + IEnumerable errors = null; + // the order of the arguments here drives the order of the commands shown + // in the help message + var result = Parser.Default.ParseArguments< + AliasedVerbOption1, + VerbNoAlias + >(arguments) + .WithParsed(o => options = o) + .WithNotParsed(o => errors = o) + ; + + var message = HelpText.AutoBuild(result, + error => error, + ex => ex, + verbsIndex: verbsIndex + ); + + string helpMessage = message.ToString(); + var helps = helpMessage.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries).Skip(2).ToList(); + + expected.Length.Should().Be(helps.Count); + int i = 0; + foreach (var expect in expected) + { + helps[i].Trim().Should().Be(expect); + i++; + } + } + + [Verb("move", + aliases: new string[] { "mv" } + )] + public class AliasedVerbOption1 + { + [Option('a', "alpha", Required = true)] + public string Option { get; set; } + } + + [Verb("copy", + isDefault: true, + aliases: new string[] { "cp", "cpy" }, + HelpText = "Copy some stuff" + )] + public class AliasedVerbOption2 + { + [Option('a', "alpha", Required = true)] + public string Option { get; set; } + } + + [Verb("delete", HelpText = "Delete stuff")] + public class VerbNoAlias + { + [Option('b', "beta", Required = true)] + public string Option { get; set; } + } + } +} diff --git a/tests/CommandLine.Tests/Unit/Issue70Tests.cs b/tests/CommandLine.Tests/Unit/Issue70Tests.cs new file mode 100644 index 00000000..0acc1116 --- /dev/null +++ b/tests/CommandLine.Tests/Unit/Issue70Tests.cs @@ -0,0 +1,29 @@ +using System.Linq; +using CommandLine.Tests.Fakes; +using CommandLine.Text; +using FluentAssertions; +using Xunit; +using Xunit.Abstractions; + +//Issue #70 +//When the factory overload is used for ParseArguments, there should be no constraint not having an empty constructor. + +namespace CommandLine.Tests.Unit +{ + public class Issue70Tests + { + [Fact] + public void Create_instance_with_factory_method_should_not_fail() + { + bool actual = false; + + var arguments = new[] { "--amend" }; + var result = Parser.Default.ParseArguments(() => Mutable_Without_Empty_Constructor.Create(), arguments); + result.WithParsed(options => { + actual = options.Amend; + }); + + actual.Should().BeTrue(); + } + } +} diff --git a/tests/CommandLine.Tests/Unit/Issue776Tests.cs b/tests/CommandLine.Tests/Unit/Issue776Tests.cs new file mode 100644 index 00000000..2e247f9b --- /dev/null +++ b/tests/CommandLine.Tests/Unit/Issue776Tests.cs @@ -0,0 +1,36 @@ +using FluentAssertions; +using Xunit; + +// Issue #776 and #797 +// When IgnoreUnknownArguments is used and there are unknown arguments with explicitly assigned values, other arguments with explicit assigned values should not be influenced. +// The bug only occured when the value was the same for a known and an unknown argument. + +namespace CommandLine.Tests.Unit +{ + public class Issue776Tests + { + [Theory] + [InlineData("3")] + [InlineData("4")] + public void IgnoreUnknownArguments_should_work_for_all_values(string dummyValue) + { + var arguments = new[] { "--cols=4", $"--dummy={dummyValue}" }; + var result = new Parser(with => { with.IgnoreUnknownArguments = true; }) + .ParseArguments(arguments); + + Assert.Empty(result.Errors); + Assert.Equal(ParserResultType.Parsed, result.Tag); + + result.WithParsed(options => + { + options.Cols.Should().Be(4); + }); + } + + private class Options + { + [Option("cols", Required = false)] + public int Cols { get; set; } + } + } +} diff --git a/tests/CommandLine.Tests/Unit/ParserResultExtensionsTests.cs b/tests/CommandLine.Tests/Unit/ParserResultExtensionsTests.cs index 97755940..a6ed812d 100644 --- a/tests/CommandLine.Tests/Unit/ParserResultExtensionsTests.cs +++ b/tests/CommandLine.Tests/Unit/ParserResultExtensionsTests.cs @@ -1,11 +1,10 @@ // Copyright 2005-2015 Giacomo Stelluti Scala & Contributors. All rights reserved. See License.md in the project root for license information. -using System; -using System.Collections.Generic; using System.Linq; -using CommandLine.Tests.Fakes; using Xunit; using FluentAssertions; +using CommandLine.Tests.Fakes; +using System.Threading.Tasks; namespace CommandLine.Tests.Unit { @@ -21,6 +20,16 @@ public static void Invoke_parsed_lambda_when_parsed() "value".Should().BeEquivalentTo(expected); } + [Fact] + public static async Task Invoke_parsed_lambda_when_parsedAsync() + { + var expected = string.Empty; + await Parser.Default.ParseArguments(new[] { "--stringvalue", "value" }) + .WithParsedAsync(opts => Task.Run(() => expected = opts.StringValue)); + + "value".Should().BeEquivalentTo(expected); + } + [Fact] public static void Invoke_parsed_lambda_when_parsed_for_verbs() { @@ -34,6 +43,20 @@ public static void Invoke_parsed_lambda_when_parsed_for_verbs() "https://value.org/user/file.git".Should().BeEquivalentTo(expected); } + [Fact] + public static async Task Invoke_parsed_lambda_when_parsed_for_verbsAsync() + { + var expected = string.Empty; + var parsedArguments = Parser.Default.ParseArguments( + new[] { "clone", "https://value.org/user/file.git" }); + + await parsedArguments.WithParsedAsync(opts => Task.Run(() => expected = "wrong1")); + await parsedArguments.WithParsedAsync(opts => Task.Run(() => expected = "wrong2")); + await parsedArguments.WithParsedAsync(opts => Task.Run(() => expected = opts.Urls.First())); + + "https://value.org/user/file.git".Should().BeEquivalentTo(expected); + } + [Fact] public static void Invoke_not_parsed_lambda_when_not_parsed() { @@ -44,6 +67,16 @@ public static void Invoke_not_parsed_lambda_when_not_parsed() "changed".Should().BeEquivalentTo(expected); } + [Fact] + public static async Task Invoke_not_parsed_lambda_when_not_parsedAsync() + { + var expected = "a default"; + await Parser.Default.ParseArguments(new[] { "-i", "aaa" }) + .WithNotParsedAsync(_ => Task.Run(() => expected = "changed")); + + "changed".Should().BeEquivalentTo(expected); + } + [Fact] public static void Invoke_not_parsed_lambda_when_parsed_for_verbs() { @@ -57,6 +90,20 @@ public static void Invoke_not_parsed_lambda_when_parsed_for_verbs() "changed".Should().BeEquivalentTo(expected); } + [Fact] + public static async Task Invoke_not_parsed_lambda_when_parsed_for_verbsAsync() + { + var expected = "a default"; + var parsedArguments = Parser.Default.ParseArguments(new[] { "undefined", "-xyz" }); + + await parsedArguments.WithParsedAsync(opts => Task.Run(() => expected = "wrong1")); + await parsedArguments.WithParsedAsync(opts => Task.Run(() => expected = "wrong2")); + await parsedArguments.WithParsedAsync(opts => Task.Run(() => expected = "wrong3")); + await parsedArguments.WithNotParsedAsync(_ => Task.Run(() => expected = "changed")); + + "changed".Should().BeEquivalentTo(expected); + } + [Fact] public static void Invoke_proper_lambda_when_parsed() { @@ -68,6 +115,18 @@ public static void Invoke_proper_lambda_when_parsed() "value".Should().BeEquivalentTo(expected); } + [Fact] + public static async Task Invoke_proper_lambda_when_parsedAsync() + { + var expected = string.Empty; + var parsedArguments = Parser.Default.ParseArguments(new[] { "--stringvalue", "value" }); + + await parsedArguments.WithParsedAsync(opts => Task.Run(() => expected = opts.StringValue)); + await parsedArguments.WithNotParsedAsync(_ => Task.Run(() => expected = "changed")); + + "value".Should().BeEquivalentTo(expected); + } + [Fact] public static void Invoke_proper_lambda_when_not_parsed() { @@ -79,6 +138,18 @@ public static void Invoke_proper_lambda_when_not_parsed() "changed".Should().BeEquivalentTo(expected); } + [Fact] + public static async Task Invoke_proper_lambda_when_not_parsedAsync() + { + var expected = "a default"; + var parsedArguments = Parser.Default.ParseArguments(new[] { "-i", "aaa" }); + + await parsedArguments.WithParsedAsync(opts => Task.Run(() => expected = opts.StringValue)); + await parsedArguments.WithNotParsedAsync(_ => Task.Run(() => expected = "changed")); + + "changed".Should().BeEquivalentTo(expected); + } + [Fact] public static void Turn_sucessful_parsing_into_exit_code() { @@ -139,6 +210,21 @@ public static void Invoke_parsed_lambda_when_parsed_for_base_verbs() "dummy.bin".Should().BeEquivalentTo(expected); } + [Fact] + public static async Task Invoke_parsed_lambda_when_parsed_for_base_verbsAsync() + { + var expected = string.Empty; + var parsedArguments = Parser.Default.ParseArguments( + new[] { "derivedadd", "dummy.bin" }); + + await parsedArguments.WithParsedAsync(opts => Task.Run(() => expected = "wrong1")); + await parsedArguments.WithParsedAsync(opts => Task.Run(() => expected = "wrong2")); + await parsedArguments.WithParsedAsync(opts => Task.Run(() => expected = "wrong3")); + await parsedArguments.WithParsedAsync(opts => Task.Run(() => expected = opts.FileName)); + + "dummy.bin".Should().BeEquivalentTo(expected); + } + [Fact] public static void Turn_sucessful_parsing_into_exit_code_for_single_base_verbs() { diff --git a/tests/CommandLine.Tests/Unit/ParserSettingsTests.cs b/tests/CommandLine.Tests/Unit/ParserSettingsTests.cs index 86691c8f..c8da9063 100644 --- a/tests/CommandLine.Tests/Unit/ParserSettingsTests.cs +++ b/tests/CommandLine.Tests/Unit/ParserSettingsTests.cs @@ -1,11 +1,6 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using FluentAssertions; +using System.IO; using Xunit; +using FluentAssertions; namespace CommandLine.Tests.Unit { diff --git a/tests/CommandLine.Tests/Unit/ParserTests.cs b/tests/CommandLine.Tests/Unit/ParserTests.cs index c183c47d..b079ce0f 100644 --- a/tests/CommandLine.Tests/Unit/ParserTests.cs +++ b/tests/CommandLine.Tests/Unit/ParserTests.cs @@ -2,12 +2,12 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.IO; using System.Linq; -using CommandLine.Tests.Fakes; -using FluentAssertions; using Xunit; +using FluentAssertions; +using CommandLine.Text; +using CommandLine.Tests.Fakes; namespace CommandLine.Tests.Unit { @@ -95,6 +95,36 @@ public void Parse_options_with_short_name(string outputFile, string[] args) // Teardown } + [Theory] + [InlineData(new string[0], 0, 0)] + [InlineData(new[] { "-v" }, 1, 0)] + [InlineData(new[] { "-vv" }, 2, 0)] + [InlineData(new[] { "-v", "-v" }, 2, 0)] + [InlineData(new[] { "-v", "-v", "-v" }, 3, 0)] + [InlineData(new[] { "-v", "-vv" }, 3, 0)] + [InlineData(new[] { "-vv", "-v" }, 3, 0)] + [InlineData(new[] { "-vvv" }, 3, 0)] + [InlineData(new[] { "-v", "-s", "-v", "-v" }, 3, 1)] + [InlineData(new[] { "-v", "-ss", "-v", "-v" }, 3, 2)] + [InlineData(new[] { "-v", "-s", "-sv", "-v" }, 3, 2)] + [InlineData(new[] { "-vsvv" }, 3, 1)] + [InlineData(new[] { "-vssvv" }, 3, 2)] + [InlineData(new[] { "-vsvsv" }, 3, 2)] + public void Parse_FlagCounter_options_with_short_name(string[] args, int verboseCount, int silentCount) + { + // Fixture setup + var expectedOptions = new Options_With_FlagCounter_Switches { Verbose = verboseCount, Silent = silentCount }; + var sut = new Parser(with => with.AllowMultiInstance = true); + + // Exercize system + var result = sut.ParseArguments(args); + + // Verify outcome + // ((NotParsed)result).Errors.Should().BeEmpty(); + ((Parsed)result).Value.Should().BeEquivalentTo(expectedOptions); + // Teardown + } + [Fact] public void Parse_repeated_options_with_default_parser() { @@ -106,6 +136,7 @@ public void Parse_repeated_options_with_default_parser() // Verify outcome Assert.IsType>(result); + // NOTE: Once GetoptMode becomes the default, it will imply MultiInstance and the above check will fail because it will be Parsed. // Teardown } @@ -132,6 +163,41 @@ public void Parse_options_with_double_dash() // Teardown } + [Fact] + public void Parse_options_with_repeated_value_in_values_sequence_and_option() + { + var text = "x1 x2 x3 -c x1"; // x1 is the same in -c option and first value + var args = text.Split(); + var parser = new Parser(with => + { + with.HelpWriter = Console.Out; + }); + var result = parser.ParseArguments(args); + var options= (result as Parsed).Value; + options.Compress.Should().BeEquivalentTo(new[] { "x1" }); + options.InputDirs.Should().BeEquivalentTo(new[] { "x1","x2","x3" }); + } + + [Fact] + public void Parse_options_with_double_dash_and_option_sequence() + { + var expectedOptions = new Options_With_Option_Sequence_And_Value_Sequence + { + OptionSequence = new[] { "option1", "option2", "option3" }, + ValueSequence = new[] { "value1", "value2", "value3" } + }; + + var sut = new Parser(with => with.EnableDashDash = true); + + // Exercize system + var result = + sut.ParseArguments( + new[] { "--option-seq", "option1", "option2", "option3", "--", "value1", "value2", "value3" }); + + // Verify outcome + ((Parsed)result).Value.Should().BeEquivalentTo(expectedOptions); + } + [Fact] public void Parse_options_with_double_dash_in_verbs_scenario() { @@ -233,6 +299,7 @@ public void Parse_repeated_options_with_default_parser_in_verbs_scenario() // Verify outcome Assert.IsType>(result); + // NOTE: Once GetoptMode becomes the default, it will imply MultiInstance and the above check will fail because it will be Parsed. // Teardown } @@ -326,7 +393,7 @@ public void Explicit_version_request_generates_version_requested_error() // Teardown } - //[Fact] + [Fact] public void Explicit_version_request_generates_version_info_screen() { // Fixture setup @@ -340,17 +407,12 @@ public void Explicit_version_request_generates_version_info_screen() // Verify outcome result.Length.Should().BeGreaterThan(0); var lines = result.ToNotEmptyLines().TrimStringArray(); - lines.Should().HaveCount(x => x == 1); -#if !PLATFORM_DOTNET - lines[0].Should().StartWithEquivalent("CommandLine"); -#else - // Takes the name of the xUnit test program - lines[0].Should().StartWithEquivalent("xUnit"); -#endif + lines.Should().HaveCount(x => x == 1); + lines[0].Should().Be(HeadingInfo.Default.ToString()); // Teardown } - //[Fact] + [Fact] public void Implicit_help_screen_in_verb_scenario() { // Fixture setup @@ -364,14 +426,8 @@ public void Implicit_help_screen_in_verb_scenario() // Verify outcome result.Length.Should().BeGreaterThan(0); var lines = result.ToNotEmptyLines().TrimStringArray(); -#if !PLATFORM_DOTNET - lines[0].Should().StartWithEquivalent("CommandLine"); - lines[1].Should().BeEquivalentTo("Copyright (c) 2005 - 2018 Giacomo Stelluti Scala & Contributors"); -#else - // Takes the name of the xUnit test program - lines[0].Should().StartWithEquivalent("xUnit"); - lines[1].Should().StartWithEquivalent("Copyright (C) Outercurve Foundation"); -#endif + lines[0].Should().Be(HeadingInfo.Default.ToString()); + lines[1].Should().Be(CopyrightInfo.Default.ToString()); lines[2].Should().BeEquivalentTo("ERROR(S):"); lines[3].Should().BeEquivalentTo("No verb selected."); lines[4].Should().BeEquivalentTo("add Add file contents to the index."); @@ -381,8 +437,31 @@ public void Implicit_help_screen_in_verb_scenario() lines[8].Should().BeEquivalentTo("version Display version information."); // Teardown } + + [Fact] + public void Help_screen_in_default_verb_scenario() + { + // Fixture setup + var help = new StringWriter(); + var sut = new Parser(config => config.HelpWriter = help); - //[Fact] + // Exercise system + sut.ParseArguments(new string[] {"--help" }); + var result = help.ToString(); + + // Verify outcome + result.Length.Should().BeGreaterThan(0); + var lines = result.ToNotEmptyLines().TrimStringArray(); + lines[0].Should().Be(HeadingInfo.Default.ToString()); + lines[1].Should().Be(CopyrightInfo.Default.ToString()); + lines[2].Should().BeEquivalentTo("add (Default Verb) Add file contents to the index."); + lines[3].Should().BeEquivalentTo("commit Record changes to the repository."); + lines[4].Should().BeEquivalentTo("clone Clone a repository into a new directory."); + lines[5].Should().BeEquivalentTo("help Display more information on a specific command."); + lines[6].Should().BeEquivalentTo("version Display version information."); + + } + [Fact] public void Double_dash_help_dispalys_verbs_index_in_verbs_scenario() { // Fixture setup @@ -395,14 +474,8 @@ public void Double_dash_help_dispalys_verbs_index_in_verbs_scenario() // Verify outcome var lines = result.ToNotEmptyLines().TrimStringArray(); -#if !PLATFORM_DOTNET - lines[0].Should().StartWithEquivalent("CommandLine"); - lines[1].Should().BeEquivalentTo("Copyright (c) 2005 - 2018 Giacomo Stelluti Scala & Contributors"); -#else - // Takes the name of the xUnit test program - lines[0].Should().StartWithEquivalent("xUnit"); - lines[1].Should().StartWithEquivalent("Copyright (C) Outercurve Foundation"); -#endif + lines[0].Should().Be(HeadingInfo.Default.ToString()); + lines[1].Should().Be(CopyrightInfo.Default.ToString()); lines[2].Should().BeEquivalentTo("add Add file contents to the index."); lines[3].Should().BeEquivalentTo("commit Record changes to the repository."); lines[4].Should().BeEquivalentTo("clone Clone a repository into a new directory."); @@ -411,7 +484,7 @@ public void Double_dash_help_dispalys_verbs_index_in_verbs_scenario() // Teardown } - //[Theory] + [Theory] [InlineData("--version")] [InlineData("version")] public void Explicit_version_request_generates_version_info_screen_in_verbs_scenario(string command) @@ -428,16 +501,11 @@ public void Explicit_version_request_generates_version_info_screen_in_verbs_scen result.Length.Should().BeGreaterThan(0); var lines = result.ToNotEmptyLines().TrimStringArray(); lines.Should().HaveCount(x => x == 1); -#if !PLATFORM_DOTNET - lines[0].Should().StartWithEquivalent("CommandLine"); -#else - // Takes the name of the xUnit test program - lines[0].Should().StartWithEquivalent("xUnit"); -#endif + lines[0].Should().Be(HeadingInfo.Default.ToString()); // Teardown } - //[Fact] + [Fact] public void Errors_of_type_MutuallyExclusiveSetError_are_properly_formatted() { // Fixture setup @@ -450,14 +518,8 @@ public void Errors_of_type_MutuallyExclusiveSetError_are_properly_formatted() // Verify outcome var lines = result.ToNotEmptyLines().TrimStringArray(); -#if !PLATFORM_DOTNET - lines[0].Should().StartWithEquivalent("CommandLine"); - lines[1].Should().BeEquivalentTo("Copyright (c) 2005 - 2018 Giacomo Stelluti Scala & Contributors"); -#else - // Takes the name of the xUnit test program - lines[0].Should().StartWithEquivalent("xUnit"); - lines[1].Should().StartWithEquivalent("Copyright (C) Outercurve Foundation"); -#endif + lines[0].Should().Be(HeadingInfo.Default.ToString()); + lines[1].Should().Be(CopyrightInfo.Default.ToString()); lines[2].Should().BeEquivalentTo("ERROR(S):"); lines[3].Should().BeEquivalentTo("Option: 'weburl' is not compatible with: 'ftpurl'."); lines[4].Should().BeEquivalentTo("Option: 'ftpurl' is not compatible with: 'weburl'."); @@ -485,7 +547,7 @@ public void Explicit_help_request_with_specific_verb_generates_help_screen() // Teardown } - //[Fact] + [Fact] public void Properly_formatted_help_screen_is_displayed_when_usage_is_defined_in_verb_scenario() { // Fixture setup @@ -503,14 +565,8 @@ public void Properly_formatted_help_screen_is_displayed_when_usage_is_defined_in // Verify outcome var lines = result.ToNotEmptyLines().TrimStringArray(); -#if !PLATFORM_DOTNET - lines[0].Should().StartWithEquivalent("CommandLine"); - lines[1].Should().BeEquivalentTo("Copyright (c) 2005 - 2018 Giacomo Stelluti Scala & Contributors"); -#else - // Takes the name of the xUnit test program - lines[0].Should().StartWithEquivalent("xUnit"); - lines[1].Should().StartWithEquivalent("Copyright (C) Outercurve Foundation"); -#endif + lines[0].Should().Be(HeadingInfo.Default.ToString()); + lines[1].Should().Be(CopyrightInfo.Default.ToString()); lines[2].Should().BeEquivalentTo("ERROR(S):"); lines[3].Should().BeEquivalentTo("Option 'badoption' is unknown."); lines[4].Should().BeEquivalentTo("USAGE:"); @@ -530,7 +586,7 @@ public void Properly_formatted_help_screen_is_displayed_when_usage_is_defined_in // Teardown } - //[Fact] + [Fact] public void Properly_formatted_help_screen_is_displayed_when_there_is_a_hidden_verb() { // Fixture setup @@ -543,14 +599,8 @@ public void Properly_formatted_help_screen_is_displayed_when_there_is_a_hidden_v // Verify outcome var lines = result.ToNotEmptyLines().TrimStringArray(); -#if !PLATFORM_DOTNET - lines[0].Should().StartWithEquivalent("CommandLine"); - lines[1].Should().BeEquivalentTo("Copyright (c) 2005 - 2018 Giacomo Stelluti Scala & Contributors"); -#else - // Takes the name of the xUnit test program - lines[0].Should().StartWithEquivalent("xUnit"); - lines[1].Should().StartWithEquivalent("Copyright (C) Outercurve Foundation"); -#endif + lines[0].Should().Be(HeadingInfo.Default.ToString()); + lines[1].Should().Be(CopyrightInfo.Default.ToString()); lines[2].Should().BeEquivalentTo("ERROR(S):"); lines[3].Should().BeEquivalentTo("No verb selected."); lines[4].Should().BeEquivalentTo("add Add file contents to the index."); @@ -560,7 +610,7 @@ public void Properly_formatted_help_screen_is_displayed_when_there_is_a_hidden_v // Teardown } - //[Fact] + [Fact] public void Properly_formatted_help_screen_is_displayed_when_there_is_a_hidden_verb_selected_usage_displays_with_hidden_option() { // Fixture setup @@ -573,14 +623,8 @@ public void Properly_formatted_help_screen_is_displayed_when_there_is_a_hidden_v // Verify outcome var lines = result.ToNotEmptyLines().TrimStringArray(); -#if !PLATFORM_DOTNET - lines[0].Should().StartWithEquivalent("CommandLine"); - lines[1].Should().BeEquivalentTo("Copyright (c) 2005 - 2018 Giacomo Stelluti Scala & Contributors"); -#else - // Takes the name of the xUnit test program - lines[0].Should().StartWithEquivalent("xUnit"); - lines[1].Should().StartWithEquivalent("Copyright (C) Outercurve Foundation"); -#endif + lines[0].Should().Be(HeadingInfo.Default.ToString()); + lines[1].Should().Be(CopyrightInfo.Default.ToString()); lines[2].Should().BeEquivalentTo("-f, --force Allow adding otherwise ignored files."); lines[3].Should().BeEquivalentTo("--help Display this help screen."); lines[4].Should().BeEquivalentTo("--version Display version information."); @@ -627,7 +671,7 @@ public void Parse_options_when_given_hidden_verb_with_hidden_option() // Teardown } - //[Fact] + [Fact] public void Specific_verb_help_screen_should_be_displayed_regardless_other_argument() { // Fixture setup @@ -645,14 +689,8 @@ public void Specific_verb_help_screen_should_be_displayed_regardless_other_argum // Verify outcome var lines = result.ToNotEmptyLines().TrimStringArray(); -#if !PLATFORM_DOTNET - lines[0].Should().StartWithEquivalent("CommandLine"); - lines[1].Should().BeEquivalentTo("Copyright (c) 2005 - 2018 Giacomo Stelluti Scala & Contributors"); -#else - // Takes the name of the xUnit test program - lines[0].Should().StartWithEquivalent("xUnit"); - lines[1].Should().StartWithEquivalent("Copyright (C) Outercurve Foundation"); -#endif + lines[0].Should().Be(HeadingInfo.Default.ToString()); + lines[1].Should().Be(CopyrightInfo.Default.ToString()); lines[2].Should().BeEquivalentTo("--no-hardlinks Optimize the cloning process from a repository on a local"); lines[3].Should().BeEquivalentTo("filesystem by copying files."); lines[4].Should().BeEquivalentTo("-q, --quiet Suppress summary message."); @@ -701,7 +739,7 @@ public void When_IgnoreUnknownArguments_is_set_valid_unknown_arguments_avoid_a_f // Teardown } - //[Fact] + [Fact] public void Properly_formatted_help_screen_excludes_help_as_unknown_option() { // Fixture setup @@ -719,14 +757,8 @@ public void Properly_formatted_help_screen_excludes_help_as_unknown_option() // Verify outcome var lines = result.ToNotEmptyLines().TrimStringArray(); -#if !PLATFORM_DOTNET - lines[0].Should().StartWithEquivalent("CommandLine"); - lines[1].Should().BeEquivalentTo("Copyright (c) 2005 - 2018 Giacomo Stelluti Scala & Contributors"); -#else - // Takes the name of the xUnit test program - lines[0].Should().StartWithEquivalent("xUnit"); - lines[1].Should().StartWithEquivalent("Copyright (C) Outercurve Foundation"); -#endif + lines[0].Should().Be(HeadingInfo.Default.ToString()); + lines[1].Should().Be(CopyrightInfo.Default.ToString()); lines[2].Should().BeEquivalentTo("ERROR(S):"); lines[3].Should().BeEquivalentTo("Option 'bad-arg' is unknown."); lines[4].Should().BeEquivalentTo("--no-hardlinks Optimize the cloning process from a repository on a local"); @@ -739,15 +771,16 @@ public void Properly_formatted_help_screen_excludes_help_as_unknown_option() // Teardown } - //[Fact] - public static void Breaking_mutually_exclusive_set_constraint_with_set_name_with_partial_string_right_side_equality_gererates_MissingValueOptionError() + [Fact] + public static void Breaking_mutually_exclusive_set_constraint_with_both_set_name_with_gererates_Error() { // Fixture setup var expectedResult = new[] - { - new MutuallyExclusiveSetError(new NameInfo("", "weburl"), string.Empty), - new MutuallyExclusiveSetError(new NameInfo("", "somethingelese"), string.Empty) - }; + { + new MutuallyExclusiveSetError(new NameInfo("", "weburl"), "theweb"), + new MutuallyExclusiveSetError(new NameInfo("", "somethingelse"), "theweb"), + + }; var sut = new Parser(); // Exercize system @@ -756,8 +789,7 @@ public static void Breaking_mutually_exclusive_set_constraint_with_set_name_with // Verify outcome ((NotParsed)result).Errors.Should().BeEquivalentTo(expectedResult); - - // Teardown + } [Fact] @@ -859,6 +891,139 @@ public void Parse_options_with_shuffled_index_values() Assert.Equal("one", args.Arg1); Assert.Equal("two", args.Arg2); }); + + } + + + [Fact] + public void Blank_lines_are_inserted_between_verbs() + { + // Fixture setup + var help = new StringWriter(); + var sut = new Parser(config => config.HelpWriter = help); + + // Exercize system + sut.ParseArguments(new string[] { }); + var result = help.ToString(); + + // Verify outcome + var lines = result.ToLines().TrimStringArray(); + lines[6].Should().BeEquivalentTo("add Add file contents to the index."); + lines[8].Should().BeEquivalentTo("help Display more information on a specific command."); + lines[10].Should().BeEquivalentTo("version Display version information."); + // 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_repeated_options_in_verbs_scenario_with_multi_instance() + { + using (var sut = new Parser(settings => settings.AllowMultiInstance = true)) + { + var longVal1 = 100; + var longVal2 = 200; + var longVal3 = 300; + var stringVal = "shortSeq1"; + + var result = sut.ParseArguments( + new[] { "sequence", "--long-seq", $"{longVal1}", "-s", stringVal, "--long-seq", $"{longVal2};{longVal3}" }, + typeof(Add_Verb), typeof(Commit_Verb), typeof(SequenceOptions)); + + Assert.IsType>(result); + Assert.IsType(((Parsed)result).Value); + result.WithParsed(verb => + { + Assert.Equal(new long[] { longVal1, longVal2, longVal3 }, verb.LongSequence); + Assert.Equal(new[] { stringVal }, verb.StringSequence); + }); + } + } + + [Fact] + public void Parse_repeated_options_in_verbs_scenario_without_multi_instance() + { + // NOTE: Once GetoptMode becomes the default, it will imply MultiInstance and this test will fail because the parser result will be Parsed. + using (var sut = new Parser(settings => settings.AllowMultiInstance = false)) + { + var longVal1 = 100; + var longVal2 = 200; + var longVal3 = 300; + var stringVal = "shortSeq1"; + + var result = sut.ParseArguments( + new[] { "sequence", "--long-seq", $"{longVal1}", "-s", stringVal, "--long-seq", $"{longVal2};{longVal3}" }, + typeof(Add_Verb), typeof(Commit_Verb), typeof(SequenceOptions)); + + Assert.IsType>(result); + result.WithNotParsed(errors => Assert.All(errors, e => + { + if (e is RepeatedOptionError) + { + // expected + } + else + { + throw new Exception($"{nameof(RepeatedOptionError)} expected"); + } + })); + } + } + + [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); + }); + } + //Fix Issue #409 for WPF + [Fact] + public void When_HelpWriter_is_null_it_should_not_fire_exception() + { + // Arrange + + //Act + var sut = new Parser(config => config.HelpWriter = null); + sut.ParseArguments(new[] {"--dummy"}); + //Assert + sut.Settings.MaximumDisplayWidth.Should().BeGreaterThan(1); } } } diff --git a/tests/CommandLine.Tests/Unit/SequenceParsingTests.cs b/tests/CommandLine.Tests/Unit/SequenceParsingTests.cs new file mode 100644 index 00000000..cb65e42f --- /dev/null +++ b/tests/CommandLine.Tests/Unit/SequenceParsingTests.cs @@ -0,0 +1,140 @@ +using System.Collections.Generic; +using System.Linq; +using System; +using Xunit; +using CommandLine.Text; +using CommandLine.Tests.Fakes; +using FluentAssertions; +using CommandLine.Core; +using System.Reflection; +using CSharpx; +using RailwaySharp.ErrorHandling; + +namespace CommandLine.Tests.Unit +{ + // Reference: PR #684 + public class SequenceParsingTests + { + // Issue #91 + [Theory] + [InlineData(false)] + [InlineData(true)] + public static void Enumerable_with_separator_before_values_does_not_try_to_parse_too_much(bool useGetoptMode) + { + var args = "--exclude=a,b InputFile.txt".Split(); + var expected = new Options_For_Issue_91 { + Excluded = new[] { "a", "b" }, + Included = Enumerable.Empty(), + InputFileName = "InputFile.txt", + }; + var sut = new Parser(parserSettings => { parserSettings.GetoptMode = useGetoptMode; }); + var result = sut.ParseArguments(args); + result.Should().BeOfType>(); + result.As>().Value.Should().BeEquivalentTo(expected); + } + + // Issue #396 + [Theory] + [InlineData(false)] + [InlineData(true)] + public static void Options_with_similar_names_are_not_ambiguous(bool useGetoptMode) + { + var args = new[] { "--configure-profile", "deploy", "--profile", "local" }; + var expected = new Options_With_Similar_Names { ConfigureProfile = "deploy", Profile = "local", Deploys = Enumerable.Empty() }; + var sut = new Parser(parserSettings => { parserSettings.GetoptMode = useGetoptMode; }); + var result = sut.ParseArguments(args); + result.Should().BeOfType>(); + result.As>().Value.Should().BeEquivalentTo(expected); + } + + // Issue #420 + [Fact] + + public static void Values_with_same_name_as_sequence_option_do_not_cause_later_values_to_split_on_separators() + { + var args = new[] { "c", "x,y" }; + var tokensExpected = new[] { Token.Value("c"), Token.Value("x,y") }; + var typeInfo = typeof(Options_With_Similar_Names_And_Separator); + + var specProps = typeInfo.GetSpecifications(pi => SpecificationProperty.Create( + Specification.FromProperty(pi), pi, Maybe.Nothing())) + .Select(sp => sp.Specification) + .OfType(); + + var tokenizerResult = Tokenizer.ConfigureTokenizer(StringComparer.InvariantCulture, false, false)(args, specProps); + var tokens = tokenizerResult.SucceededWith(); + tokens.Should().BeEquivalentTo(tokensExpected); + } + + // Issue #454 + [Theory] + [InlineData(false)] + [InlineData(true)] + + public static void Enumerable_with_colon_separator_before_values_does_not_try_to_parse_too_much(bool useGetoptMode) + { + var args = "-c chanA:chanB file.hdf5".Split(); + var expected = new Options_For_Issue_454 { + Channels = new[] { "chanA", "chanB" }, + ArchivePath = "file.hdf5", + }; + var sut = new Parser(parserSettings => { parserSettings.GetoptMode = useGetoptMode; }); + var result = sut.ParseArguments(args); + result.Should().BeOfType>(); + result.As>().Value.Should().BeEquivalentTo(expected); + } + + // Issue #510 + [Theory] + [InlineData(false)] + [InlineData(true)] + + public static void Enumerable_before_values_does_not_try_to_parse_too_much(bool useGetoptMode) + { + var args = new[] { "-a", "1,2", "c" }; + var expected = new Options_For_Issue_510 { A = new[] { "1", "2" }, C = "c" }; + var sut = new Parser(parserSettings => { parserSettings.GetoptMode = useGetoptMode; }); + var result = sut.ParseArguments(args); + result.Should().BeOfType>(); + result.As>().Value.Should().BeEquivalentTo(expected); + } + + // Issue #617 + [Theory] + [InlineData(false)] + [InlineData(true)] + + public static void Enumerable_with_enum_before_values_does_not_try_to_parse_too_much(bool useGetoptMode) + { + var args = "--fm D,C a.txt".Split(); + var expected = new Options_For_Issue_617 { + Mode = new[] { FMode.D, FMode.C }, + Files = new[] { "a.txt" }, + }; + var sut = new Parser(parserSettings => { parserSettings.GetoptMode = useGetoptMode; }); + var result = sut.ParseArguments(args); + result.Should().BeOfType>(); + result.As>().Value.Should().BeEquivalentTo(expected); + } + + // Issue #619 + [Theory] + [InlineData(false)] + [InlineData(true)] + + public static void Separator_just_before_values_does_not_try_to_parse_values(bool useGetoptMode) + { + var args = "--outdir ./x64/Debug --modules ../utilities/x64/Debug,../auxtool/x64/Debug m_xfunit.f03 m_xfunit_assertion.f03".Split(); + var expected = new Options_For_Issue_619 { + OutDir = "./x64/Debug", + ModuleDirs = new[] { "../utilities/x64/Debug", "../auxtool/x64/Debug" }, + Ignores = Enumerable.Empty(), + Srcs = new[] { "m_xfunit.f03", "m_xfunit_assertion.f03" }, + }; + var sut = new Parser(parserSettings => { parserSettings.GetoptMode = useGetoptMode; }); + var result = sut.ParseArguments(args); + result.Should().BeOfType>(); + result.As>().Value.Should().BeEquivalentTo(expected); + } + } +} diff --git a/tests/CommandLine.Tests/Unit/StringBuilderExtensionsTests.cs b/tests/CommandLine.Tests/Unit/StringBuilderExtensionsTests.cs new file mode 100644 index 00000000..fdbd6526 --- /dev/null +++ b/tests/CommandLine.Tests/Unit/StringBuilderExtensionsTests.cs @@ -0,0 +1,101 @@ +using System.Collections.Generic; +using System.Text; +using Xunit; +using FluentAssertions; +using CommandLine.Infrastructure; + +namespace CommandLine.Tests.Unit +{ + public class StringBuilderExtensionsTests + { + private static StringBuilder _sb = new StringBuilder("test string"); + private static StringBuilder _emptySb = new StringBuilder(); + private static StringBuilder _nullSb = null; + + public static IEnumerable GoodStartsWithData => new [] + { + new object[] { "t" }, + new object[] { "te" }, + new object[] { "test " }, + new object[] { "test string" } + }; + + public static IEnumerable BadTestData => new [] + { + new object[] { null }, + new object[] { "" }, + new object[] { "xyz" }, + new object[] { "some long test string" } + }; + + public static IEnumerable GoodEndsWithData => new[] + { + new object[] { "g" }, + new object[] { "ng" }, + new object[] { " string" }, + new object[] { "test string" } + }; + + + + [Theory] + [MemberData(nameof(GoodStartsWithData))] + [MemberData(nameof(BadTestData))] + public void StartsWith_null_builder_returns_false(string input) + { + _nullSb.SafeStartsWith(input).Should().BeFalse(); + } + + [Theory] + [MemberData(nameof(GoodStartsWithData))] + [MemberData(nameof(BadTestData))] + public void StartsWith_empty_builder_returns_false(string input) + { + _emptySb.SafeStartsWith(input).Should().BeFalse(); + } + + [Theory] + [MemberData(nameof(GoodStartsWithData))] + public void StartsWith_good_data_returns_true(string input) + { + _sb.SafeStartsWith(input).Should().BeTrue(); + } + + [Theory] + [MemberData(nameof(BadTestData))] + public void StartsWith_bad_data_returns_false(string input) + { + _sb.SafeStartsWith(input).Should().BeFalse(); + } + + [Theory] + [MemberData(nameof(GoodEndsWithData))] + [MemberData(nameof(BadTestData))] + public void EndsWith_null_builder_returns_false(string input) + { + _nullSb.SafeEndsWith(input).Should().BeFalse(); + } + + [Theory] + [MemberData(nameof(GoodEndsWithData))] + [MemberData(nameof(BadTestData))] + public void EndsWith_empty_builder_returns_false(string input) + { + _emptySb.SafeEndsWith(input).Should().BeFalse(); + } + + [Theory] + [MemberData(nameof(GoodEndsWithData))] + public void EndsWith_good_data_returns_true(string input) + { + _sb.SafeEndsWith(input).Should().BeTrue(); + } + + [Theory] + [MemberData(nameof(BadTestData))] + public void EndsWith_bad_data_returns_false(string input) + { + _sb.SafeEndsWith(input).Should().BeFalse(); + } + } +} diff --git a/tests/CommandLine.Tests/Unit/Text/HelpTextAutoBuildFix.cs b/tests/CommandLine.Tests/Unit/Text/HelpTextAutoBuildFix.cs new file mode 100644 index 00000000..d777c8f9 --- /dev/null +++ b/tests/CommandLine.Tests/Unit/Text/HelpTextAutoBuildFix.cs @@ -0,0 +1,92 @@ +using System; +using System.Linq; +using Xunit; +using FluentAssertions; +using CommandLine.Tests.Fakes; +using CommandLine.Text; + +namespace CommandLine.Tests.Unit.Text +{ + public class HelpTextAutoBuildFix + { + [Fact] + public void HelpText_with_AdditionalNewLineAfterOption_true_should_have_newline() + { + // Fixture setup + // Exercize system + var sut = new HelpText { AdditionalNewLineAfterOption = true } + .AddOptions(new NotParsed(TypeInfo.Create(typeof(Simple_Options)), + Enumerable.Empty())); + + // Verify outcome + + var lines = sut.ToString().ToLines(); + + lines[2].Should().BeEquivalentTo(" stringvalue Define a string value here."); + lines[3].Should().BeEquivalentTo(String.Empty); + lines[4].Should().BeEquivalentTo(" s, shortandlong Example with both short and long name."); + lines[5].Should().BeEquivalentTo(String.Empty); + lines[7].Should().BeEquivalentTo(String.Empty); + lines[9].Should().BeEquivalentTo(String.Empty); + lines[11].Should().BeEquivalentTo(String.Empty); + lines[13].Should().BeEquivalentTo(String.Empty); + lines[14].Should().BeEquivalentTo(" value pos. 0 Define a long value here."); + // Teardown + } + + [Fact] + public void HelpText_with_AdditionalNewLineAfterOption_false_should_not_have_newline() + { + // Fixture setup + // Exercize system + var sut = new HelpText { AdditionalNewLineAfterOption = false } + .AddOptions(new NotParsed(TypeInfo.Create(typeof(Simple_Options)), + Enumerable.Empty())); + + // Verify outcome + + var lines = sut.ToString().ToLines(); + + lines[2].Should().BeEquivalentTo(" stringvalue Define a string value here."); + + lines[3].Should().BeEquivalentTo(" s, shortandlong Example with both short and long name."); + lines[8].Should().BeEquivalentTo(" value pos. 0 Define a long value here."); + // Teardown + } + [Fact] + public void HelpText_with_by_default_should_include_help_version_option() + { + // Fixture setup + // Exercize system + var sut = new HelpText () + .AddOptions(new NotParsed(TypeInfo.Create(typeof(Simple_Options)), + Enumerable.Empty())); + + // Verify outcome + + var lines = sut.ToString().ToNotEmptyLines(); + lines.Should().HaveCount(c => c ==7); + lines.Should().Contain(" help Display more information on a specific command."); + lines.Should().Contain(" version Display version information."); + // Teardown + } + + [Fact] + public void HelpText_with_AutoHelp_false_should_hide_help_option() + { + // Fixture setup + // Exercize system + var sut = new HelpText { AutoHelp = false,AutoVersion = false} + .AddOptions(new NotParsed(TypeInfo.Create(typeof(Simple_Options)), + Enumerable.Empty())); + + // Verify outcome + + var lines = sut.ToString().ToNotEmptyLines(); + lines.Should().HaveCount(c => c ==5); + lines.Should().NotContain(" help Display more information on a specific command."); + lines.Should().NotContain(" version Display version information."); + // Teardown + } + } +} diff --git a/tests/CommandLine.Tests/Unit/Text/HelpTextTests.cs b/tests/CommandLine.Tests/Unit/Text/HelpTextTests.cs index 0c6e5022..9811f7be 100644 --- a/tests/CommandLine.Tests/Unit/Text/HelpTextTests.cs +++ b/tests/CommandLine.Tests/Unit/Text/HelpTextTests.cs @@ -5,75 +5,101 @@ using System.Globalization; using System.Linq; using System.Reflection; +using System.Text; +using Xunit; +using FluentAssertions; using CommandLine.Core; using CommandLine.Infrastructure; using CommandLine.Tests.Fakes; -using CommandLine.Tests.Unit.Infrastructure; using CommandLine.Text; -using FluentAssertions; -using Xunit; -using System.Text; namespace CommandLine.Tests.Unit.Text { - public class HelpTextTests + public class HelpTextTests : IDisposable { + private readonly HeadingInfo headingInfo = new HeadingInfo("CommandLine.Tests.dll", "1.9.4.131"); + + public void Dispose() + { + ReflectionHelper.SetAttributeOverride(null); + } + [Fact] public void Create_empty_instance() { string.Empty.Should().BeEquivalentTo(new HelpText().ToString()); } - [Fact] - public void Create_instance_without_options() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void Create_instance_without_options(bool newlineBetweenSections) { // Fixture setup // Exercize system var sut = - new HelpText(new HeadingInfo("Unit-tests", "2.0"), new CopyrightInfo(true, "Author", 2005, 2013)) - .AddPreOptionsLine("pre-options line 1") + new HelpText(new HeadingInfo("Unit-tests", "2.0"), new CopyrightInfo(true, "Author", 2005, 2013)); + sut.AddNewLineBetweenHelpSections = newlineBetweenSections; + sut.AddPreOptionsLine("pre-options line 1") .AddPreOptionsLine("pre-options line 2") .AddPostOptionsLine("post-options line 1") .AddPostOptionsLine("post-options line 2"); // Verify outcome - var lines = sut.ToString().ToNotEmptyLines(); + var expected = new List() + { + "Unit-tests 2.0", + "Copyright (C) 2005 - 2013 Author", + "pre-options line 1", + "pre-options line 2", + "post-options line 1", + "post-options line 2" + }; + + if (newlineBetweenSections) + { + expected.Insert(2, ""); + expected.Insert(5, ""); + } - lines[0].Should().BeEquivalentTo("Unit-tests 2.0"); - lines[1].Should().BeEquivalentTo("Copyright (C) 2005 - 2013 Author"); - lines[2].Should().BeEquivalentTo("pre-options line 1"); - lines[3].Should().BeEquivalentTo("pre-options line 2"); - lines[4].Should().BeEquivalentTo("post-options line 1"); - lines[5].Should().BeEquivalentTo("post-options line 2"); - // Teardown + var lines = sut.ToString().ToLines(); + lines.Should().StartWith(expected); } - [Fact] - public void Create_instance_with_options() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void Create_instance_with_options(bool newlineBetweenSections) { // Fixture setup // Exercize system - var sut = new HelpText { AddDashesToOption = true } + var sut = new HelpText { AddDashesToOption = true, AddNewLineBetweenHelpSections = newlineBetweenSections } .AddPreOptionsLine("pre-options") .AddOptions(new NotParsed(TypeInfo.Create(typeof(Simple_Options)), Enumerable.Empty())) .AddPostOptionsLine("post-options"); // Verify outcome - - var lines = sut.ToString().ToNotEmptyLines().TrimStringArray(); - lines[0].Should().BeEquivalentTo("pre-options"); - lines[1].Should().BeEquivalentTo("--stringvalue Define a string value here."); - lines[2].Should().BeEquivalentTo("-s, --shortandlong Example with both short and long name."); - lines[3].Should().BeEquivalentTo("-i Define a int sequence here."); - lines[4].Should().BeEquivalentTo("-x Define a boolean or switch value here."); - lines[5].Should().BeEquivalentTo("--help Display this help screen."); - lines[6].Should().BeEquivalentTo("--version Display version information."); - lines[7].Should().BeEquivalentTo("value pos. 0 Define a long value here."); - lines[8].Should().BeEquivalentTo("post-options"); - // Teardown + var expected = new [] + { + "", + "pre-options", + "", + "--stringvalue Define a string value here.", + "-s, --shortandlong Example with both short and long name.", + "-i Define a int sequence here.", + "-x Define a boolean or switch value here.", + "--help Display this help screen.", + "--version Display version information.", + "value pos. 0 Define a long value here.", + "", + "post-options" + }; + + var lines = sut.ToString().ToLines().TrimStringArray(); + lines.Should().StartWith(expected); } - //[Fact] + [Fact] public void Create_instance_with_enum_options_enabled() { // Fixture setup @@ -139,7 +165,7 @@ public void When_help_text_is_longer_than_width_it_will_wrap_around_as_if_in_a_c { // Fixture setup // Exercize system - var sut = new HelpText(new HeadingInfo("CommandLine.Tests.dll", "1.9.4.131")); + var sut = new HelpText(headingInfo); sut.MaximumDisplayWidth = 40; sut.AddOptions( new NotParsed( @@ -150,21 +176,19 @@ public void When_help_text_is_longer_than_width_it_will_wrap_around_as_if_in_a_c var lines = sut.ToString().Split(new[] { Environment.NewLine }, StringSplitOptions.None); lines[2].Should().BeEquivalentTo(" v, verbose This is the description"); //"The first line should have the arguments and the start of the Help Text."); //string formattingMessage = "Beyond the second line should be formatted as though it's in a column."; - lines[3].Should().BeEquivalentTo(" of the verbosity to "); - lines[4].Should().BeEquivalentTo(" test out the wrapping "); - lines[5].Should().BeEquivalentTo(" capabilities of the "); - lines[6].Should().BeEquivalentTo(" Help Text."); + lines[3].Should().BeEquivalentTo(" of the verbosity to test"); + lines[4].Should().BeEquivalentTo(" out the wrapping"); + lines[5].Should().BeEquivalentTo(" capabilities of the Help"); + lines[6].Should().BeEquivalentTo(" Text."); // Teardown } - - [Fact] public void When_help_text_is_longer_than_width_it_will_wrap_around_as_if_in_a_column_given_width_of_100() { // Fixture setup // Exercize system - var sut = new HelpText(new HeadingInfo("CommandLine.Tests.dll", "1.9.4.131")) { MaximumDisplayWidth = 100} ; + var sut = new HelpText(headingInfo) { MaximumDisplayWidth = 100 }; sut.AddOptions( new NotParsed( TypeInfo.Create(typeof(Simple_Options_With_HelpText_Set_To_Long_Description)), @@ -172,18 +196,18 @@ public void When_help_text_is_longer_than_width_it_will_wrap_around_as_if_in_a_c // Verify outcome var lines = sut.ToString().Split(new[] { Environment.NewLine }, StringSplitOptions.None); - lines[2].Should().BeEquivalentTo(" v, verbose This is the description of the verbosity to test out the wrapping capabilities of "); //"The first line should have the arguments and the start of the Help Text."); + lines[2].Should().BeEquivalentTo(" v, verbose This is the description of the verbosity to test out the wrapping capabilities of"); //"The first line should have the arguments and the start of the Help Text."); //string formattingMessage = "Beyond the second line should be formatted as though it's in a column."; lines[3].Should().BeEquivalentTo(" the Help Text."); // Teardown } - //[Fact] + [Fact] public void When_help_text_has_hidden_option_it_should_not_be_added_to_help_text_output() { // Fixture setup // Exercize system - var sut = new HelpText(new HeadingInfo("CommandLine.Tests.dll", "1.9.4.131")); + var sut = new HelpText(headingInfo); sut.MaximumDisplayWidth = 80; sut.AddOptions( new NotParsed( @@ -192,7 +216,7 @@ public void When_help_text_has_hidden_option_it_should_not_be_added_to_help_text // Verify outcome var lines = sut.ToString().Split(new[] { Environment.NewLine }, StringSplitOptions.None); - lines[2].Should().BeEquivalentTo(" v, verbose This is the description of the verbosity to test out the "); //"The first line should have the arguments and the start of the Help Text."); + lines[2].Should().BeEquivalentTo(" v, verbose This is the description of the verbosity to test out the"); //"The first line should have the arguments and the start of the Help Text."); //string formattingMessage = "Beyond the second line should be formatted as though it's in a column."; lines[3].Should().BeEquivalentTo(" wrapping capabilities of the Help Text."); // Teardown @@ -203,7 +227,7 @@ public void Long_help_text_without_spaces() { // Fixture setup // Exercize system - var sut = new HelpText(new HeadingInfo("CommandLine.Tests.dll", "1.9.4.131")); + var sut = new HelpText(headingInfo); sut.MaximumDisplayWidth = 40; sut.AddOptions( new NotParsed( @@ -212,10 +236,10 @@ public void Long_help_text_without_spaces() // Verify outcome var lines = sut.ToString().ToNotEmptyLines(); - lines[1].Should().BeEquivalentTo(" v, verbose Before "); + lines[1].Should().BeEquivalentTo(" v, verbose Before"); lines[2].Should().BeEquivalentTo(" 012345678901234567890123"); lines[3].Should().BeEquivalentTo(" After"); - lines[4].Should().BeEquivalentTo(" input-file Before "); + lines[4].Should().BeEquivalentTo(" input-file Before"); lines[5].Should().BeEquivalentTo(" 012345678901234567890123"); lines[6].Should().BeEquivalentTo(" 456789 After"); // Teardown @@ -234,20 +258,25 @@ public void Long_pre_and_post_lines_without_spaces() // Verify outcome var lines = sut.ToString().ToNotEmptyLines(); - lines[1].Should().BeEquivalentTo("Before "); + lines[1].Should().BeEquivalentTo("Before"); lines[2].Should().BeEquivalentTo("0123456789012345678901234567890123456789"); lines[3].Should().BeEquivalentTo("012 After"); - lines[lines.Length - 3].Should().BeEquivalentTo("Before "); + lines[lines.Length - 3].Should().BeEquivalentTo("Before"); lines[lines.Length - 2].Should().BeEquivalentTo("0123456789012345678901234567890123456789"); - lines[lines.Length - 1].Should().BeEquivalentTo(" After"); + lines[lines.Length - 1].Should().BeEquivalentTo("After"); // Teardown } - + [Fact] public void Invoking_RenderParsingErrorsText_returns_appropriate_formatted_text() { // Fixture setup + var optionsInGroup = new List + { + new NameInfo("t", "testOption1"), + new NameInfo("c", "testOption2") + }; var fakeResult = new NotParsed( TypeInfo.Create(typeof(NullInstance)), new Error[] @@ -260,30 +289,36 @@ public void Invoking_RenderParsingErrorsText_returns_appropriate_formatted_text( new NoVerbSelectedError(), new BadVerbSelectedError("badverb"), new HelpRequestedError(), // should be ignored - new HelpVerbRequestedError(null, null, false) // should be ignored + new HelpVerbRequestedError(null, null, false), // should be ignored + new MissingGroupOptionError("bad-option-group", optionsInGroup), }); Func fakeRenderer = err => + { + switch (err.Tag) { - switch (err.Tag) - { - case ErrorType.BadFormatTokenError: - return "ERR " + ((BadFormatTokenError)err).Token; - case ErrorType.MissingValueOptionError: - return "ERR " + ((MissingValueOptionError)err).NameInfo.NameText; - case ErrorType.UnknownOptionError: - return "ERR " + ((UnknownOptionError)err).Token; - case ErrorType.MissingRequiredOptionError: - return "ERR " + ((MissingRequiredOptionError)err).NameInfo.NameText; - case ErrorType.SequenceOutOfRangeError: - return "ERR " + ((SequenceOutOfRangeError)err).NameInfo.NameText; - case ErrorType.NoVerbSelectedError: - return "ERR no-verb-selected"; - case ErrorType.BadVerbSelectedError: - return "ERR " + ((BadVerbSelectedError)err).Token; - default: - throw new InvalidOperationException(); - } - }; + case ErrorType.BadFormatTokenError: + return "ERR " + ((BadFormatTokenError)err).Token; + case ErrorType.MissingValueOptionError: + return "ERR " + ((MissingValueOptionError)err).NameInfo.NameText; + case ErrorType.UnknownOptionError: + return "ERR " + ((UnknownOptionError)err).Token; + case ErrorType.MissingRequiredOptionError: + return "ERR " + ((MissingRequiredOptionError)err).NameInfo.NameText; + case ErrorType.SequenceOutOfRangeError: + return "ERR " + ((SequenceOutOfRangeError)err).NameInfo.NameText; + case ErrorType.NoVerbSelectedError: + return "ERR no-verb-selected"; + case ErrorType.BadVerbSelectedError: + return "ERR " + ((BadVerbSelectedError)err).Token; + case ErrorType.MissingGroupOptionError: + { + var groupErr = (MissingGroupOptionError)err; + return "ERR " + groupErr.Group + ": " + string.Join("---", groupErr.Names.Select(n => n.NameText)); + } + default: + throw new InvalidOperationException(); + } + }; Func, string> fakeMutExclRenderer = _ => string.Empty; @@ -292,7 +327,6 @@ public void Invoking_RenderParsingErrorsText_returns_appropriate_formatted_text( // Verify outcome var lines = errorsText.ToNotEmptyLines(); - lines[0].Should().BeEquivalentTo(" ERR badtoken"); lines[1].Should().BeEquivalentTo(" ERR x, switch"); lines[2].Should().BeEquivalentTo(" ERR unknown"); @@ -300,10 +334,11 @@ public void Invoking_RenderParsingErrorsText_returns_appropriate_formatted_text( lines[4].Should().BeEquivalentTo(" ERR s, sequence"); lines[5].Should().BeEquivalentTo(" ERR no-verb-selected"); lines[6].Should().BeEquivalentTo(" ERR badverb"); + lines[7].Should().BeEquivalentTo(" ERR bad-option-group: t, testOption1---c, testOption2"); // Teardown } - //[Fact] + [Fact] public void Invoke_AutoBuild_for_Options_returns_appropriate_formatted_text() { // Fixture setup @@ -319,27 +354,27 @@ public void Invoke_AutoBuild_for_Options_returns_appropriate_formatted_text() var helpText = HelpText.AutoBuild(fakeResult); // Verify outcome - var lines = helpText.ToString().ToNotEmptyLines().TrimStringArray(); -#if !PLATFORM_DOTNET - lines[0].Should().StartWithEquivalent("CommandLine"); - lines[1].Should().StartWithEquivalent("Copyright (c)"); -#else - // Takes the name of the xUnit test program - lines[0].Should().StartWithEquivalent("xUnit"); - lines[1].Should().StartWithEquivalent("Copyright (C) Outercurve Foundation"); -#endif - lines[2].Should().BeEquivalentTo("ERROR(S):"); - lines[3].Should().BeEquivalentTo("Token 'badtoken' is not recognized."); - lines[4].Should().BeEquivalentTo("A sequence option 'i' is defined with fewer or more items than required."); - lines[5].Should().BeEquivalentTo("--stringvalue Define a string value here."); - lines[6].Should().BeEquivalentTo("-s, --shortandlong Example with both short and long name."); - lines[7].Should().BeEquivalentTo("-i Define a int sequence here."); - lines[8].Should().BeEquivalentTo("-x Define a boolean or switch value here."); - lines[9].Should().BeEquivalentTo("--help Display this help screen."); + var lines = helpText.ToString().ToLines().TrimStringArray(); + lines[0].Should().Be(HeadingInfo.Default.ToString()); + lines[1].Should().Be(CopyrightInfo.Default.ToString()); + lines[2].Should().BeEmpty(); + lines[3].Should().BeEquivalentTo("ERROR(S):"); + lines[4].Should().BeEquivalentTo("Token 'badtoken' is not recognized."); + lines[5].Should().BeEquivalentTo("A sequence option 'i' is defined with fewer or more items than required."); + lines[6].Should().BeEmpty(); + lines[7].Should().BeEquivalentTo("--stringvalue Define a string value here."); + lines[8].Should().BeEmpty(); + lines[9].Should().BeEquivalentTo("-s, --shortandlong Example with both short and long name."); + lines[10].Should().BeEmpty(); + lines[11].Should().BeEquivalentTo("-i Define a int sequence here."); + lines[12].Should().BeEmpty(); + lines[13].Should().BeEquivalentTo("-x Define a boolean or switch value here."); + lines[14].Should().BeEmpty(); + lines[15].Should().BeEquivalentTo("--help Display this help screen."); // Teardown } - //[Fact] + [Fact] public void Invoke_AutoBuild_for_Verbs_with_specific_verb_returns_appropriate_formatted_text() { // Fixture setup @@ -354,25 +389,23 @@ public void Invoke_AutoBuild_for_Verbs_with_specific_verb_returns_appropriate_fo var helpText = HelpText.AutoBuild(fakeResult); // Verify outcome - var lines = helpText.ToString().ToNotEmptyLines().TrimStringArray(); - -#if !PLATFORM_DOTNET - lines[0].Should().StartWithEquivalent("CommandLine"); - lines[1].Should().StartWithEquivalent("Copyright (c)"); -#else - // Takes the name of the xUnit test program - lines[0].Should().StartWithEquivalent("xUnit"); - lines[1].Should().StartWithEquivalent("Copyright (C) Outercurve Foundation"); -#endif - lines[2].Should().BeEquivalentTo("-p, --patch Use the interactive patch selection interface to chose which"); - lines[3].Should().BeEquivalentTo("changes to commit."); - lines[4].Should().BeEquivalentTo("--amend Used to amend the tip of the current branch."); - lines[5].Should().BeEquivalentTo("-m, --message Use the given message as the commit message."); - lines[6].Should().BeEquivalentTo("--help Display this help screen."); + var lines = helpText.ToString().ToLines().TrimStringArray(); + + lines[0].Should().Be(HeadingInfo.Default.ToString()); + lines[1].Should().Be(CopyrightInfo.Default.ToString()); + lines[2].Should().BeEmpty(); + lines[3].Should().BeEquivalentTo("-p, --patch Use the interactive patch selection interface to chose which"); + lines[4].Should().BeEquivalentTo("changes to commit."); + lines[5].Should().BeEmpty(); + lines[6].Should().BeEquivalentTo("--amend Used to amend the tip of the current branch."); + lines[7].Should().BeEmpty(); + lines[8].Should().BeEquivalentTo("-m, --message Use the given message as the commit message."); + lines[9].Should().BeEmpty(); + lines[10].Should().BeEquivalentTo("--help Display this help screen."); // Teardown } - //[Fact] + [Fact] public void Invoke_AutoBuild_for_Verbs_with_specific_verb_returns_appropriate_formatted_text_given_display_width_100() { // Fixture setup @@ -384,19 +417,12 @@ public void Invoke_AutoBuild_for_Verbs_with_specific_verb_returns_appropriate_fo }); // Exercize system - var helpText = HelpText.AutoBuild(fakeResult, maxDisplayWidth: 100); + var helpText = HelpText.AutoBuild(fakeResult, maxDisplayWidth: 100); // Verify outcome var lines = helpText.ToString().ToNotEmptyLines().TrimStringArray(); - -#if !PLATFORM_DOTNET - lines[0].Should().StartWithEquivalent("CommandLine"); - lines[1].Should().BeEquivalentTo("Copyright (c) 2005 - 2018 Giacomo Stelluti Scala & Contributors"); -#else - // Takes the name of the xUnit test program - lines[0].Should().StartWithEquivalent("xUnit"); - lines[1].Should().StartWithEquivalent("Copyright (C) Outercurve Foundation"); -#endif + lines[0].Should().Be(HeadingInfo.Default.ToString()); + lines[1].Should().Be(CopyrightInfo.Default.ToString()); lines[2].Should().BeEquivalentTo("-p, --patch Use the interactive patch selection interface to chose which changes to commit."); lines[3].Should().BeEquivalentTo("--amend Used to amend the tip of the current branch."); lines[4].Should().BeEquivalentTo("-m, --message Use the given message as the commit message."); @@ -404,7 +430,7 @@ public void Invoke_AutoBuild_for_Verbs_with_specific_verb_returns_appropriate_fo // Teardown } - //[Fact] + [Fact] public void Invoke_AutoBuild_for_Verbs_with_unknown_verb_returns_appropriate_formatted_text() { // Fixture setup @@ -421,14 +447,8 @@ public void Invoke_AutoBuild_for_Verbs_with_unknown_verb_returns_appropriate_for // Verify outcome var lines = helpText.ToString().ToNotEmptyLines().TrimStringArray(); -#if !PLATFORM_DOTNET - lines[0].Should().StartWithEquivalent("CommandLine"); - lines[1].Should().StartWithEquivalent("Copyright (c)"); -#else - // Takes the name of the xUnit test program - lines[0].Should().StartWithEquivalent("xUnit"); - lines[1].Should().StartWithEquivalent("Copyright (C) Outercurve Foundation"); -#endif + lines[0].Should().Be(HeadingInfo.Default.ToString()); + lines[1].Should().Be(CopyrightInfo.Default.ToString()); lines[2].Should().BeEquivalentTo("add Add file contents to the index."); lines[3].Should().BeEquivalentTo("commit Record changes to the repository."); lines[4].Should().BeEquivalentTo("clone Clone a repository into a new directory."); @@ -442,23 +462,26 @@ public void Create_instance_with_options_and_values() { // Fixture setup // Exercize system - var sut = new HelpText { AddDashesToOption = true } + var sut = new HelpText { AddDashesToOption = true, AdditionalNewLineAfterOption = false } .AddPreOptionsLine("pre-options") .AddOptions(new NotParsed(TypeInfo.Create(typeof(Options_With_HelpText_And_MetaValue)), Enumerable.Empty())) .AddPostOptionsLine("post-options"); // Verify outcome - var lines = sut.ToString().ToNotEmptyLines().TrimStringArray(); - lines[0].Should().BeEquivalentTo("pre-options"); - lines[1].Should().BeEquivalentTo("--stringvalue=STR Define a string value here."); - lines[2].Should().BeEquivalentTo("-i INTSEQ Define a int sequence here."); - lines[3].Should().BeEquivalentTo("-x Define a boolean or switch value here."); - lines[4].Should().BeEquivalentTo("--help Display this help screen."); - lines[5].Should().BeEquivalentTo("--version Display version information."); - lines[6].Should().BeEquivalentTo("number (pos. 0) NUM Define a long value here."); - lines[7].Should().BeEquivalentTo("paintcolor (pos. 1) COLOR Define a color value here."); - lines[8].Should().BeEquivalentTo("post-options", lines[8]); + var lines = sut.ToString().ToLines().TrimStringArray(); + lines[0].Should().BeEmpty(); + lines[1].Should().BeEquivalentTo("pre-options"); + lines[2].Should().BeEmpty(); + lines[3].Should().BeEquivalentTo("--stringvalue=STR Define a string value here."); + lines[4].Should().BeEquivalentTo("-i INTSEQ Define a int sequence here."); + lines[5].Should().BeEquivalentTo("-x Define a boolean or switch value here."); + lines[6].Should().BeEquivalentTo("--help Display this help screen."); + lines[7].Should().BeEquivalentTo("--version Display version information."); + lines[8].Should().BeEquivalentTo("number (pos. 0) NUM Define a long value here."); + lines[9].Should().BeEquivalentTo("paintcolor (pos. 1) COLOR Define a color value here."); + lines[10].Should().BeEmpty(); + lines[11].Should().BeEquivalentTo("post-options", lines[11]); // Teardown } @@ -469,7 +492,7 @@ public static void RenderUsageText_returns_properly_formatted_text() ParserResult result = new NotParsed( TypeInfo.Create(typeof(Options_With_Usage_Attribute)), Enumerable.Empty()); - + // Exercize system var text = HelpText.RenderUsageText(result); @@ -490,8 +513,10 @@ public static void RenderUsageText_returns_properly_formatted_text() lines[10].Should().BeEquivalentTo(" mono testapp.exe value"); } - //[Fact] - public void Invoke_AutoBuild_for_Options_with_Usage_returns_appropriate_formatted_text() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void Invoke_AutoBuild_for_Options_with_Usage_returns_appropriate_formatted_text(bool newlineBetweenSections) { // Fixture setup var fakeResult = new NotParsed( @@ -502,47 +527,114 @@ public void Invoke_AutoBuild_for_Options_with_Usage_returns_appropriate_formatte }); // Exercize system - var helpText = HelpText.AutoBuild(fakeResult); + var helpText = HelpText.AutoBuild(fakeResult, + h => + { + h.AddNewLineBetweenHelpSections = newlineBetweenSections; + return HelpText.DefaultParsingErrorsHandler(fakeResult, h); + }, + e => e + ); // Verify outcome + var expected = new List() + { + HeadingInfo.Default.ToString(), + CopyrightInfo.Default.ToString(), + "", + "ERROR(S):", + "Token 'badtoken' is not recognized.", + "USAGE:", + "Normal scenario:", + "mono testapp.exe --input file.bin --output out.bin", + "Logging warnings:", + "mono testapp.exe -w --input file.bin", + "Logging errors:", + "mono testapp.exe -e --input file.bin", + "mono testapp.exe --errs --input=file.bin", + "List:", + "mono testapp.exe -l 1,2", + "Value:", + "mono testapp.exe value", + "", + "-i, --input Set input file.", + "", + "-i, --output Set output file.", + "", + "--verbose Set verbosity level.", + "", + "-w, --warns Log warnings.", + "", + "-e, --errs Log errors.", + "", + "-l List.", + "", + "--help Display this help screen.", + "", + "--version Display version information.", + "", + "value pos. 0 Value." + }; + + if (newlineBetweenSections) + expected.Insert(5, ""); + var text = helpText.ToString(); - var lines = text.ToNotEmptyLines(); -#if !PLATFORM_DOTNET - lines[0].Should().StartWithEquivalent("CommandLine"); - lines[1].Should().StartWithEquivalent("Copyright (c)"); -#else - // Takes the name of the xUnit test program - lines[0].Should().StartWithEquivalent("xUnit"); - lines[1].Should().StartWithEquivalent("Copyright (C) Outercurve Foundation"); -#endif - lines[2].Should().BeEquivalentTo("ERROR(S):"); - lines[3].Should().BeEquivalentTo("Token 'badtoken' is not recognized."); - lines[4].Should().BeEquivalentTo("USAGE:"); - lines[5].Should().BeEquivalentTo("Normal scenario:"); - lines[6].Should().BeEquivalentTo("mono testapp.exe --input file.bin --output out.bin"); - lines[7].Should().BeEquivalentTo("Logging warnings:"); - lines[8].Should().BeEquivalentTo("mono testapp.exe -w --input file.bin"); - lines[9].Should().BeEquivalentTo("Logging errors:"); - lines[10].Should().BeEquivalentTo("mono testapp.exe -e --input file.bin"); - lines[11].Should().BeEquivalentTo("mono testapp.exe --errs --input=file.bin"); - lines[12].Should().BeEquivalentTo("List:"); - lines[13].Should().BeEquivalentTo("mono testapp.exe -l 1,2"); - lines[14].Should().BeEquivalentTo("Value:"); - lines[15].Should().BeEquivalentTo("mono testapp.exe value"); - lines[16].Should().BeEquivalentTo("-i, --input Set input file."); - lines[17].Should().BeEquivalentTo("-i, --output Set output file."); - lines[18].Should().BeEquivalentTo("--verbose Set verbosity level."); - lines[19].Should().BeEquivalentTo("-w, --warns Log warnings."); - lines[20].Should().BeEquivalentTo("-e, --errs Log errors."); - lines[21].Should().BeEquivalentTo("-l List."); - lines[22].Should().BeEquivalentTo("--help Display this help screen."); - lines[23].Should().BeEquivalentTo("--version Display version information."); - lines[24].Should().BeEquivalentTo("value pos. 0 Value."); + var lines = text.ToLines().TrimStringArray(); - // Teardown + lines.Should().StartWith(expected); + } + + [Theory] + [InlineData(true, true)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(false, false)] + public void AutoBuild_with_errors_and_preoptions_renders_correctly(bool startWithNewline, bool newlineBetweenSections) + { + // Fixture setup + var fakeResult = new NotParsed( + TypeInfo.Create(typeof(Simple_Options_Without_HelpText)), + new Error[] + { + new BadFormatTokenError("badtoken") + }); + + // Exercize system + var helpText = HelpText.AutoBuild(fakeResult, + h => + { + h.AddNewLineBetweenHelpSections = newlineBetweenSections; + h.AddPreOptionsLine((startWithNewline ? Environment.NewLine : null) + "pre-options"); + return HelpText.DefaultParsingErrorsHandler(fakeResult, h); + }, + e => e + ); + + // Verify outcome + var expected = new List() + { + HeadingInfo.Default.ToString(), + CopyrightInfo.Default.ToString(), + "pre-options", + "", + "ERROR(S):", + "Token 'badtoken' is not recognized.", + "", + "-v, --verbose", + "", + "--input-file" + }; + + if (newlineBetweenSections || startWithNewline) + expected.Insert(2, ""); + + var text = helpText.ToString(); + var lines = text.ToLines().TrimStringArray(); + + lines.Should().StartWith(expected); } -#if !PLATFORM_DOTNET [Fact] public void Default_set_to_sequence_should_be_properly_printed() { @@ -568,96 +660,60 @@ public void Default_set_to_sequence_should_be_properly_printed() // Teardown } -#endif [Fact] public void AutoBuild_when_no_assembly_attributes() { - try - { - string expectedCopyright = "Copyright (C) 1 author"; + string expectedCopyright = $"Copyright (C) {DateTime.Now.Year} author"; - ReflectionHelper.SetAttributeOverride(new Attribute[0]); + ReflectionHelper.SetAttributeOverride(new Attribute[0]); - ParserResult fakeResult = new NotParsed( - TypeInfo.Create(typeof (Simple_Options)), new Error[0]); - bool onErrorCalled = false; - HelpText actualResult = HelpText.AutoBuild(fakeResult, ht => - { - onErrorCalled = true; - return ht; - }, ex => ex); - - onErrorCalled.Should().BeTrue(); - actualResult.Copyright.Should().Be(expectedCopyright); - } - finally - { - ReflectionHelper.SetAttributeOverride(null); - } + ParserResult fakeResult = new NotParsed( + TypeInfo.Create(typeof(Simple_Options)), new Error[0]); + HelpText actualResult = HelpText.AutoBuild(fakeResult, ht => ht, ex => ex); + actualResult.Copyright.Should().Be(expectedCopyright); } + [Fact] public void AutoBuild_with_assembly_title_and_version_attributes_only() { - try - { - string expectedTitle = "Title"; - string expectedVersion = "1.2.3.4"; + string expectedTitle = "Title"; + string expectedVersion = "1.2.3.4"; - ReflectionHelper.SetAttributeOverride(new Attribute[] - { - new AssemblyTitleAttribute(expectedTitle), - new AssemblyInformationalVersionAttribute(expectedVersion) - }); - - ParserResult fakeResult = new NotParsed( - TypeInfo.Create(typeof (Simple_Options)), new Error[0]); - bool onErrorCalled = false; - HelpText actualResult = HelpText.AutoBuild(fakeResult, ht => - { - onErrorCalled = true; - return ht; - }, ex => ex); - - onErrorCalled.Should().BeTrue(); - actualResult.Heading.Should().Be(string.Format("{0} {1}", expectedTitle, expectedVersion)); - } - finally + ReflectionHelper.SetAttributeOverride(new Attribute[] { - ReflectionHelper.SetAttributeOverride(null); - } + new AssemblyTitleAttribute(expectedTitle), + new AssemblyInformationalVersionAttribute(expectedVersion) + }); + + ParserResult fakeResult = new NotParsed( + TypeInfo.Create(typeof(Simple_Options)), new Error[0]); + HelpText actualResult = HelpText.AutoBuild(fakeResult, ht => ht, ex => ex); + actualResult.Heading.Should().Be(string.Format("{0} {1}", expectedTitle, expectedVersion)); } - [Fact] public void AutoBuild_with_assembly_company_attribute_only() { - try - { - string expectedCompany = "Company"; + string expectedCompany = "Company"; - ReflectionHelper.SetAttributeOverride(new Attribute[] - { - new AssemblyCompanyAttribute(expectedCompany) - }); - - ParserResult fakeResult = new NotParsed( - TypeInfo.Create(typeof (Simple_Options)), new Error[0]); - bool onErrorCalled = false; - HelpText actualResult = HelpText.AutoBuild(fakeResult, ht => - { - onErrorCalled = true; - return ht; - }, ex => ex); + ReflectionHelper.SetAttributeOverride(new Attribute[] + { + new AssemblyCompanyAttribute(expectedCompany) + }); - onErrorCalled.Should().BeFalse(); // Other attributes have fallback logic - actualResult.Copyright.Should().Be(string.Format("Copyright (C) {0} {1}", DateTime.Now.Year, expectedCompany)); - } - finally + ParserResult fakeResult = new NotParsed( + TypeInfo.Create(typeof(Simple_Options)), new Error[0]); + bool onErrorCalled = false; + HelpText actualResult = HelpText.AutoBuild(fakeResult, ht => { - ReflectionHelper.SetAttributeOverride(null); - } + onErrorCalled = true; + return ht; + }, ex => ex); + + onErrorCalled.Should().BeFalse(); // Other attributes have fallback logic + actualResult.Copyright.Should().Be(string.Format("Copyright (C) {0} {1}", DateTime.Now.Year, expectedCompany)); } [Fact] @@ -670,5 +726,301 @@ public void Add_line_with_two_empty_spaces_at_the_end() Assert.Equal("T" + Environment.NewLine + "e" + Environment.NewLine + "s" + Environment.NewLine + "t", b.ToString()); } + + [Fact] + public void HelpTextHonoursLineBreaks() + { + // Fixture setup + // Exercize system + var sut = new HelpText { AddDashesToOption = true } + .AddOptions(new NotParsed(TypeInfo.Create(typeof(HelpTextWithLineBreaks_Options)), + Enumerable.Empty())); + + // Verify outcome + + var lines = sut.ToString().ToNotEmptyLines(); + lines[0].Should().BeEquivalentTo(" --stringvalue This is a help text description."); + lines[1].Should().BeEquivalentTo(" It has multiple lines."); + lines[2].Should().BeEquivalentTo(" We also want to ensure that indentation is correct."); + + // Teardown + } + + [Fact] + public void HelpTextHonoursIndentationAfterLineBreaks() + { + // Fixture setup + // Exercize system + var sut = new HelpText { AddDashesToOption = true } + .AddOptions(new NotParsed(TypeInfo.Create(typeof(HelpTextWithLineBreaks_Options)), + Enumerable.Empty())); + + // Verify outcome + + var lines = sut.ToString().ToNotEmptyLines(); + lines[3].Should().BeEquivalentTo(" --stringvalu2 This is a help text description where we want"); + lines[4].Should().BeEquivalentTo(" the left pad after a linebreak to be honoured so that"); + lines[5].Should().BeEquivalentTo(" we can sub-indent within a description."); + + // Teardown + } + + [Fact] + public void HelpTextPreservesIndentationAcrossWordWrap() + { + // Fixture setup + // Exercise system + var sut = new HelpText { AddDashesToOption = true, MaximumDisplayWidth = 60 } + .AddOptions(new NotParsed(TypeInfo.Create(typeof(HelpTextWithLineBreaksAndSubIndentation_Options)), + Enumerable.Empty())); + + // Verify outcome + + var lines = sut.ToString().ToNotEmptyLines(); + lines[0].Should().BeEquivalentTo(" --stringvalue This is a help text description where we"); + lines[1].Should().BeEquivalentTo(" want:"); + lines[2].Should().BeEquivalentTo(" * The left pad after a linebreak to"); + lines[3].Should().BeEquivalentTo(" be honoured and the indentation to be"); + lines[4].Should().BeEquivalentTo(" preserved across to the next line"); + lines[5].Should().BeEquivalentTo(" * The ability to return to no indent."); + lines[6].Should().BeEquivalentTo(" Like this."); + + // Teardown + } + + [Fact] + public void HelpTextIsConsitentRegardlessOfCompileTimeLineStyle() + { + // Fixture setup + // Exercize system + var sut = new HelpText { AddDashesToOption = true } + .AddOptions(new NotParsed(TypeInfo.Create(typeof(HelpTextWithMixedLineBreaks_Options)), + Enumerable.Empty())); + + // Verify outcome + + var lines = sut.ToString().ToNotEmptyLines(); + lines[0].Should().BeEquivalentTo(" --stringvalue This is a help text description"); + lines[1].Should().BeEquivalentTo(" It has multiple lines."); + lines[2].Should().BeEquivalentTo(" Third line"); + + // Teardown + } + + [Fact] + public void HelpTextPreservesIndentationAcrossWordWrapWithSmallMaximumDisplayWidth() + { + // Fixture setup + // Exercise system + var sut = new HelpText { AddDashesToOption = true, MaximumDisplayWidth = 10 } + .AddOptions(new NotParsed(TypeInfo.Create(typeof(HelpTextWithLineBreaksAndSubIndentation_Options)), + Enumerable.Empty())); + + // Verify outcome + + Assert.True(sut.ToString().Length > 0); + + // Teardown + } + + [Fact] + public void Options_should_be_separated_by_spaces() + { + // Fixture setup + var handlers = new CultureInfo("en-US").MakeCultureHandlers(); + var fakeResult = + new NotParsed( + typeof(Options_With_Default_Set_To_Sequence).ToTypeInfo(), + Enumerable.Empty() + ); + + // Exercize system + handlers.ChangeCulture(); + var helpText = HelpText.AutoBuild(fakeResult); + handlers.ResetCulture(); + + // Verify outcome + var text = helpText.ToString(); + var lines = text.ToLines().TrimStringArray(); + + lines[3].Should().Be("-z, --strseq (Default: a b c)"); + lines[5].Should().Be("-y, --intseq (Default: 1 2 3)"); + lines[7].Should().Be("-q, --dblseq (Default: 1.1 2.2 3.3)"); + + // Teardown + } + + [Fact] + public void Options_Should_Render_OptionGroup_In_Parenthesis_When_Available() + { + var sut = new HelpText(headingInfo) { AddDashesToOption = true, MaximumDisplayWidth = 100 } + .AddOptions( + new NotParsed(TypeInfo.Create(typeof(Simple_Options_With_OptionGroup)), Enumerable.Empty())); + + var text = sut.ToString(); + var lines = text.ToLines().TrimStringArray(); + + + lines[0].Should().BeEquivalentTo(headingInfo.ToString()); + lines[1].Should().BeEmpty(); + lines[2].Should().BeEquivalentTo("--stringvalue (Group: string-group) Define a string value here."); + lines[3].Should().BeEquivalentTo("-s, --shortandlong (Group: string-group) Example with both short and long name."); + lines[4].Should().BeEquivalentTo("-x Define a boolean or switch value here."); + lines[5].Should().BeEquivalentTo("--help Display this help screen."); + lines[6].Should().BeEquivalentTo("--version Display version information."); + } + + [Fact] + public void Options_Should_Render_OptionGroup_When_Available_And_Should_Not_Render_Required() + { + var sut = new HelpText(headingInfo) { AddDashesToOption = true, MaximumDisplayWidth = 100 } + .AddOptions( + new NotParsed(TypeInfo.Create(typeof(Simple_Options_With_Required_OptionGroup)), Enumerable.Empty())); + + var text = sut.ToString(); + var lines = text.ToLines().TrimStringArray(); + + + lines[0].Should().BeEquivalentTo(headingInfo.ToString()); + lines[1].Should().BeEmpty(); + lines[2].Should().BeEquivalentTo("--stringvalue (Group: string-group) Define a string value here."); + lines[3].Should().BeEquivalentTo("-s, --shortandlong (Group: string-group) Example with both short and long name."); + lines[4].Should().BeEquivalentTo("-x Define a boolean or switch value here."); + lines[5].Should().BeEquivalentTo("--help Display this help screen."); + lines[6].Should().BeEquivalentTo("--version Display version information."); + } + + [Fact] + public void Options_Should_Render_Multiple_OptionGroups_When_Available() + { + var sut = new HelpText(headingInfo) { AddDashesToOption = true, MaximumDisplayWidth = 100 } + .AddOptions( + new NotParsed(TypeInfo.Create(typeof(Simple_Options_With_Multiple_OptionGroups)), Enumerable.Empty())); + + var text = sut.ToString(); + var lines = text.ToLines().TrimStringArray(); + + + lines[0].Should().BeEquivalentTo(headingInfo.ToString()); + lines[1].Should().BeEmpty(); + lines[2].Should().BeEquivalentTo("--stringvalue (Group: string-group) Define a string value here."); + lines[3].Should().BeEquivalentTo("-s, --shortandlong (Group: string-group) Example with both short and long name."); + lines[4].Should().BeEquivalentTo("-x (Group: second-group) Define a boolean or switch value here."); + lines[5].Should().BeEquivalentTo("-i (Group: second-group) Define a int sequence here."); + lines[6].Should().BeEquivalentTo("--help Display this help screen."); + lines[7].Should().BeEquivalentTo("--version Display version information."); + } + + #region Custom Help + + [Fact] + [Trait("Category", "CustomHelp")] + public void AutoBuild_with_custom_copyright_using_onError_action() + { + string expectedCopyright = "Copyright (c) 2019 Global.com"; + var expectedHeading = "MyApp 2.0.0-beta"; + ParserResult fakeResult = new NotParsed( + TypeInfo.Create(typeof(Simple_Options)), + new Error[] { new HelpRequestedError() }); + bool onErrorCalled = false; + HelpText actualResult = HelpText.AutoBuild(fakeResult, ht => + { + ht.AdditionalNewLineAfterOption = false; + ht.Heading = "MyApp 2.0.0-beta"; + ht.Copyright = "Copyright (c) 2019 Global.com"; + return ht; + }); + actualResult.Copyright.Should().Be(expectedCopyright); + actualResult.Heading.Should().Be(expectedHeading); + } + + [Fact] + [Trait("Category", "CustomHelp")] + public void AutoBuild_with_custom_help_and_version_request() + { + string expectedTitle = "Title"; + string expectedVersion = "1.2.3.4"; + + ReflectionHelper.SetAttributeOverride(new Attribute[] + { + new AssemblyTitleAttribute(expectedTitle), + new AssemblyInformationalVersionAttribute(expectedVersion) + }); + + ParserResult fakeResult = new NotParsed( + TypeInfo.Create(typeof(Simple_Options)), + new Error[] { new VersionRequestedError() }); + + HelpText helpText = HelpText.AutoBuild(fakeResult, ht => ht); + helpText.ToString().Trim().Should().Be($"{expectedTitle} {expectedVersion}"); + } + [Fact] + [Trait("Category", "CustomHelp")] + public void Invoke_Custom_AutoBuild_for_Verbs_with_specific_verb_and_no_AdditionalNewLineAfterOption_returns_appropriate_formatted_text() + { + // Fixture setup + var fakeResult = new NotParsed( + TypeInfo.Create(typeof(NullInstance)), + new Error[] + { + new HelpVerbRequestedError("commit", typeof(Commit_Verb), true) + }); + + // Exercize system + var helpText = HelpText.AutoBuild(fakeResult, h => { + h.AdditionalNewLineAfterOption = false; + return h; + }); + + // Verify outcome + var lines = helpText.ToString().ToLines().TrimStringArray(); + var i = 0; + lines[i++].Should().Be(HeadingInfo.Default.ToString()); + lines[i++].Should().Be(CopyrightInfo.Default.ToString()); + lines[i++].Should().BeEmpty(); + lines[i++].Should().BeEquivalentTo("-p, --patch Use the interactive patch selection interface to chose which"); + lines[i++].Should().BeEquivalentTo("changes to commit."); + lines[i++].Should().BeEquivalentTo("--amend Used to amend the tip of the current branch."); + lines[i++].Should().BeEquivalentTo("-m, --message Use the given message as the commit message."); + lines[i++].Should().BeEquivalentTo("--help Display this help screen."); + + } + [Fact] + [Trait("Category", "CustomHelp")] + public void Invoke_AutoBuild_for_Options_with_custom_help_returns_appropriate_formatted_text() + { + // Fixture setup + var fakeResult = new NotParsed( + TypeInfo.Create(typeof(Simple_Options)), + new Error[] + { + new BadFormatTokenError("badtoken"), + new SequenceOutOfRangeError(new NameInfo("i", "")) + }); + + // Exercize system + var helpText = HelpText.AutoBuild(fakeResult, h => h); + + // Verify outcome + var lines = helpText.ToString().ToLines().TrimStringArray(); + lines[0].Should().Be(HeadingInfo.Default.ToString()); + lines[1].Should().Be(CopyrightInfo.Default.ToString()); + lines[2].Should().BeEmpty(); + lines[3].Should().BeEquivalentTo("ERROR(S):"); + lines[4].Should().BeEquivalentTo("Token 'badtoken' is not recognized."); + lines[5].Should().BeEquivalentTo("A sequence option 'i' is defined with fewer or more items than required."); + lines[6].Should().BeEmpty(); + lines[7].Should().BeEquivalentTo("--stringvalue Define a string value here."); + lines[8].Should().BeEmpty(); + lines[9].Should().BeEquivalentTo("-s, --shortandlong Example with both short and long name."); + lines[10].Should().BeEmpty(); + lines[11].Should().BeEquivalentTo("-i Define a int sequence here."); + lines[12].Should().BeEmpty(); + lines[13].Should().BeEquivalentTo("-x Define a boolean or switch value here."); + lines[14].Should().BeEmpty(); + lines[15].Should().BeEquivalentTo("--help Display this help screen."); + + } + #endregion } } diff --git a/tests/CommandLine.Tests/Unit/UnParserExtensionsTests.cs b/tests/CommandLine.Tests/Unit/UnParserExtensionsTests.cs index 461fc969..7e878f32 100644 --- a/tests/CommandLine.Tests/Unit/UnParserExtensionsTests.cs +++ b/tests/CommandLine.Tests/Unit/UnParserExtensionsTests.cs @@ -1,12 +1,16 @@ // Copyright 2005-2015 Giacomo Stelluti Scala & Contributors. All rights reserved. See License.md in the project root for license information. +using System; using System.Collections.Generic; +using System.IO; using System.Linq; -using CommandLine.Tests.Fakes; using Xunit; using FluentAssertions; +using CommandLine.Tests.Fakes; +#if !SKIP_FSHARP using Microsoft.FSharp.Core; - +#endif + namespace CommandLine.Tests.Unit { public class UnParserExtensionsTests @@ -20,6 +24,33 @@ public static void UnParsing_instance_returns_command_line(Simple_Options option .Should().BeEquivalentTo(result); } + [Theory] + [MemberData(nameof(UnParseData))] + public static void UnParsing_instance_with_splitArgs_returns_same_option_class(Simple_Options options, string result) + { + new Parser() + .FormatCommandLineArgs(options) + .Should().BeEquivalentTo(result.SplitArgs()); + + } + + [Theory] + [MemberData(nameof(UnParseFileDirectoryData))] + public static void UnParsing_instance_returns_command_line_for_file_directory_paths(Options_With_FileDirectoryInfo options, string result) + { + new Parser() + .FormatCommandLine(options) + .Should().BeEquivalentTo(result); + } + + [Theory] + [MemberData(nameof(UnParseFileDirectoryData))] + public static void UnParsing_instance_by_splitArgs_returns_command_line_for_file_directory_paths(Options_With_FileDirectoryInfo options, string result) + { + new Parser() + .FormatCommandLineArgs(options) + .Should().BeEquivalentTo(result.SplitArgs()); + } [Theory] [MemberData(nameof(UnParseDataVerbs))] public static void UnParsing_instance_returns_command_line_for_verbs(Add_Verb verb, string result) @@ -29,6 +60,15 @@ public static void UnParsing_instance_returns_command_line_for_verbs(Add_Verb ve .Should().BeEquivalentTo(result); } + [Theory] + [MemberData(nameof(UnParseDataVerbs))] + public static void UnParsing_instance_to_splitArgs_returns_command_line_for_verbs(Add_Verb verb, string result) + { + new Parser() + .FormatCommandLineArgs(verb) + .Should().BeEquivalentTo(result.SplitArgs()); + } + [Theory] [MemberData(nameof(UnParseDataImmutable))] public static void UnParsing_immutable_instance_returns_command_line(Immutable_Simple_Options options, string result) @@ -39,7 +79,7 @@ public static void UnParsing_immutable_instance_returns_command_line(Immutable_S } [Theory] - [MemberData("UnParseDataHidden")] + [MemberData(nameof(UnParseDataHidden))] public static void Unparsing_hidden_option_returns_command_line(Hidden_Option options, bool showHidden, string result) { new Parser() @@ -103,6 +143,227 @@ public static void UnParsing_instance_with_dash_in_value_and_dashdash_disabled_r .Should().BeEquivalentTo("-something with dash"); } + #region Issue 579 + [Fact] + public static void UnParsing_instance_with_TimeSpan_returns_the_value_unquoted_in_command_line() + { + var options = new Options_With_TimeSpan { Duration = TimeSpan.FromMinutes(1) }; + new Parser() + .FormatCommandLine(options) + .Should().Be("--duration 00:01:00"); + } + #endregion + + #region PR 550 + + [Fact] + public static void UnParsing_instance_with_default_values_when_skip_default_is_false() + { + var options = new Options_With_Defaults { P2 = "xyz", P1 = 99, P3 = 88, P4 = Shapes.Square }; + new Parser() + .FormatCommandLine(options) + .Should().BeEquivalentTo("--p1 99 --p2 xyz --p3 88 --p4 Square"); + } + + [Theory] + [InlineData(true, "--p2 xyz")] + [InlineData(false, "--p1 99 --p2 xyz --p3 88 --p4 Square")] + public static void UnParsing_instance_with_default_values_when_skip_default_is_true(bool skipDefault, string expected) + { + var options = new Options_With_Defaults { P2 = "xyz", P1 = 99, P3 = 88, P4 = Shapes.Square }; + new Parser() + .FormatCommandLine(options, x => x.SkipDefault = skipDefault) + .Should().BeEquivalentTo(expected); + } + + [Theory] + [InlineData(true, "--p2 xyz")] + [InlineData(false, "--p1 99 --p2 xyz --p3 88 --p4 Square")] + public static void UnParsing_instance_with_nullable_default_values_when_skip_default_is_true(bool skipDefault, string expected) + { + var options = new Nuulable_Options_With_Defaults { P2 = "xyz", P1 = 99, P3 = 88, P4 = Shapes.Square }; + new Parser() + .FormatCommandLine(options, x => x.SkipDefault = skipDefault) + .Should().BeEquivalentTo(expected); + } + [Fact] + public static void UnParsing_instance_with_datetime() + { + var date = new DateTime(2019, 5, 1); + var options = new Options_Date { Start = date }; + var result = new Parser() + .FormatCommandLine(options) + .Should().MatchRegex("--start\\s\".+\""); + } + + [Fact] + public static void UnParsing_instance_with_datetime_nullable() + { + var date = new DateTime(2019, 5, 1); + var options = new Options_Date_Nullable { Start = date }; + var result = new Parser() + .FormatCommandLine(options) + .Should().MatchRegex("--start\\s\".+\""); + } + + [Fact] + public static void UnParsing_instance_with_datetime_offset() + { + DateTimeOffset date = new DateTime(2019, 5, 1); + var options = new Options_DateTimeOffset { Start = date }; + var result = new Parser() + .FormatCommandLine(options) + .Should().MatchRegex("--start\\s\".+\""); + } + + [Fact] + public static void UnParsing_instance_with_timespan() + { + var ts = new TimeSpan(1,2,3); + var options = new Options_TimeSpan { Start = ts }; + var result = new Parser() + .FormatCommandLine(options) + .Should().BeEquivalentTo("--start 01:02:03"); //changed for issue 579 + } + + [Theory] + [InlineData(false, 0, "")] //default behaviour based on type + [InlineData(false, 1, "-v 1")] //default skip=false + [InlineData(false, 2, "-v 2")] + [InlineData(true, 1, "")] //default skip=true + public static void UnParsing_instance_with_int(bool skipDefault, int value, string expected) + { + var options = new Option_Int { VerboseLevel = value }; + var result = new Parser() + .FormatCommandLine(options, x => x.SkipDefault = skipDefault) + .Should().BeEquivalentTo(expected); + + } + + [Theory] + [InlineData(false, 0, "-v 0")] + [InlineData(false, 1, "-v 1")] //default + [InlineData(false, 2, "-v 2")] + [InlineData(false, null, "")] + [InlineData(true, 1, "")] //default + public static void UnParsing_instance_with_int_nullable(bool skipDefault, int? value, string expected) + { + var options = new Option_Int_Nullable { VerboseLevel = value }; + var result = new Parser() + .FormatCommandLine(options, x => x.SkipDefault = skipDefault) + .Should().BeEquivalentTo(expected); + + } + + [Theory] + [InlineData(false, false, 0, "")] + [InlineData(false, false, 1, "-v")] // default but not skipped + [InlineData(false, false, 2, "-v -v")] + [InlineData(false, true, 2, "-vv")] + [InlineData(false, false, 3, "-v -v -v")] + [InlineData(false, true, 3, "-vvv")] + [InlineData(true, false, 1, "")] // default, skipped + public static void UnParsing_instance_with_flag_counter(bool skipDefault, bool groupSwitches, int value, string expected) + { + var options = new Option_FlagCounter { VerboseLevel = value }; + var result = new Parser() + .FormatCommandLine(options, x => { x.SkipDefault = skipDefault; x.GroupSwitches = groupSwitches; }) + .Should().BeEquivalentTo(expected); + } + + [Theory] + [InlineData(Shapes.Circle, "--shape Circle")] + [InlineData(Shapes.Square, "--shape Square")] + [InlineData(null, "")] + public static void UnParsing_instance_with_nullable_enum(Shapes? shape, string expected) + { + var options = new Option_Nullable_Enum { Shape = shape }; + var result = new Parser() + .FormatCommandLine(options) + .Should().BeEquivalentTo(expected); + } + + [Theory] + [InlineData(true, "-v True")] + [InlineData(false, "-v False")] + [InlineData(null, "")] + public static void UnParsing_instance_with_nullable_bool(bool? flag, string expected) + { + var options = new Option_Nullable_Bool { Verbose = flag }; + var result = new Parser() + .FormatCommandLine(options) + .Should().BeEquivalentTo(expected); + } + #region SplitArgs + [Theory] + [InlineData("--shape Circle", new[] { "--shape","Circle" })] + [InlineData(" --shape Circle ", new[] { "--shape", "Circle" })] + [InlineData("-a --shape Circle", new[] {"-a", "--shape", "Circle" })] + [InlineData("-a --shape Circle -- -x1 -x2", new[] { "-a", "--shape", "Circle","--","-x1","-x2" })] + [InlineData("--name \"name with space and quote\" -x1", new[] { "--name", "name with space and quote","-x1" })] + public static void Split_arguments(string command, string[] expectedArgs) + { + var args = command.SplitArgs(); + args.Should().BeEquivalentTo(expectedArgs); + } + [Theory] + [InlineData("--shape Circle", new[] { "--shape", "Circle" })] + [InlineData(" --shape Circle ", new[] { "--shape", "Circle" })] + [InlineData("-a --shape Circle", new[] { "-a", "--shape", "Circle" })] + [InlineData("-a --shape Circle -- -x1 -x2", new[] { "-a", "--shape", "Circle", "--", "-x1", "-x2" })] + [InlineData("--name \"name with space and quote\" -x1", new[] { "--name", "\"name with space and quote\"", "-x1" })] + public static void Split_arguments_with_keep_quote(string command, string[] expectedArgs) + { + var args = command.SplitArgs(true); + args.Should().BeEquivalentTo(expectedArgs); + } + #endregion + class Option_Int_Nullable + { + [Option('v', Default = 1)] + public int? VerboseLevel { get; set; } + } + class Option_Int + { + [Option('v', Default = 1)] + public int VerboseLevel { get; set; } + } + class Option_FlagCounter + { + [Option('v', Default = 1, FlagCounter=true)] + public int VerboseLevel { get; set; } + } + class Option_Nullable_Bool + { + [Option('v')] + public bool? Verbose { get; set; } + } + class Option_Nullable_Enum + { + [Option] + public Shapes? Shape { get; set; } + } + class Options_Date + { + [Option] + public DateTime Start { get; set; } + } + class Options_Date_Nullable + { + [Option] + public DateTime? Start { get; set; } + } + class Options_TimeSpan + { + [Option] + public TimeSpan Start { get; set; } + } + class Options_DateTimeOffset + { + [Option] + public DateTimeOffset Start { get; set; } + } + #endregion public static IEnumerable UnParseData { get @@ -120,6 +381,15 @@ public static IEnumerable UnParseData } } + public static IEnumerable UnParseFileDirectoryData + { + get + { + yield return new object[] { new Options_With_FileDirectoryInfo(), "" }; + yield return new object[] { new Options_With_FileDirectoryInfo { FilePath = new FileInfo(@"C:\my path\with spaces\file with spaces.txt"), DirectoryPath = new DirectoryInfo(@"C:\my path\with spaces\"), StringPath = @"C:\my path\with spaces\file with spaces.txt" }, @"--directoryPath ""C:\my path\with spaces\"" --filePath ""C:\my path\with spaces\file with spaces.txt"" --stringPath ""C:\my path\with spaces\file with spaces.txt""" }; + } + } + public static IEnumerable UnParseDataVerbs { @@ -136,15 +406,15 @@ public static IEnumerable UnParseDataImmutable get { yield return new object[] { new Immutable_Simple_Options("", Enumerable.Empty(), default(bool), default(long)), "" }; - yield return new object[] { new Immutable_Simple_Options ("", Enumerable.Empty(), true, default(long) ), "-x" }; - yield return new object[] { new Immutable_Simple_Options ("", new[] { 1, 2, 3 }, default(bool), default(long) ), "-i 1 2 3" }; - yield return new object[] { new Immutable_Simple_Options ("nospaces", Enumerable.Empty(), default(bool), default(long)), "--stringvalue nospaces" }; - yield return new object[] { new Immutable_Simple_Options (" with spaces ", Enumerable.Empty(), default(bool), default(long)), "--stringvalue \" with spaces \"" }; - yield return new object[] { new Immutable_Simple_Options ("with\"quote", Enumerable.Empty(), default(bool), default(long)), "--stringvalue \"with\\\"quote\"" }; - yield return new object[] { new Immutable_Simple_Options ("with \"quotes\" spaced", Enumerable.Empty(), default(bool), default(long)), "--stringvalue \"with \\\"quotes\\\" spaced\"" }; - yield return new object[] { new Immutable_Simple_Options ("", Enumerable.Empty(), default(bool), 123456789), "123456789" }; - yield return new object[] { new Immutable_Simple_Options ("nospaces", new[] { 1, 2, 3 }, true, 123456789), "-i 1 2 3 --stringvalue nospaces -x 123456789" }; - yield return new object[] { new Immutable_Simple_Options ("with \"quotes\" spaced", new[] { 1, 2, 3 }, true, 123456789), "-i 1 2 3 --stringvalue \"with \\\"quotes\\\" spaced\" -x 123456789" }; + yield return new object[] { new Immutable_Simple_Options("", Enumerable.Empty(), true, default(long)), "-x" }; + yield return new object[] { new Immutable_Simple_Options("", new[] { 1, 2, 3 }, default(bool), default(long)), "-i 1 2 3" }; + yield return new object[] { new Immutable_Simple_Options("nospaces", Enumerable.Empty(), default(bool), default(long)), "--stringvalue nospaces" }; + yield return new object[] { new Immutable_Simple_Options(" with spaces ", Enumerable.Empty(), default(bool), default(long)), "--stringvalue \" with spaces \"" }; + yield return new object[] { new Immutable_Simple_Options("with\"quote", Enumerable.Empty(), default(bool), default(long)), "--stringvalue \"with\\\"quote\"" }; + yield return new object[] { new Immutable_Simple_Options("with \"quotes\" spaced", Enumerable.Empty(), default(bool), default(long)), "--stringvalue \"with \\\"quotes\\\" spaced\"" }; + yield return new object[] { new Immutable_Simple_Options("", Enumerable.Empty(), default(bool), 123456789), "123456789" }; + yield return new object[] { new Immutable_Simple_Options("nospaces", new[] { 1, 2, 3 }, true, 123456789), "-i 1 2 3 --stringvalue nospaces -x 123456789" }; + yield return new object[] { new Immutable_Simple_Options("with \"quotes\" spaced", new[] { 1, 2, 3 }, true, 123456789), "-i 1 2 3 --stringvalue \"with \\\"quotes\\\" spaced\" -x 123456789" }; } } @@ -153,7 +423,7 @@ public static IEnumerable UnParseDataHidden get { yield return new object[] { new Hidden_Option { HiddenOption = "hidden" }, true, "--hiddenOption hidden" }; - yield return new object[] { new Hidden_Option { HiddenOption = "hidden" }, false, ""}; + yield return new object[] { new Hidden_Option { HiddenOption = "hidden" }, false, "" }; } } #if !SKIP_FSHARP diff --git a/tests/CommandLine.Tests/Unit/VerbAttributeTests.cs b/tests/CommandLine.Tests/Unit/VerbAttributeTests.cs new file mode 100644 index 00000000..386e3015 --- /dev/null +++ b/tests/CommandLine.Tests/Unit/VerbAttributeTests.cs @@ -0,0 +1,51 @@ +using System; +using Xunit; + +namespace CommandLine.Tests +{ + //Test localization of VerbAttribute + public class VerbAttributeTests + { + [Theory] + [InlineData("", null, "")] + [InlineData("", typeof(Fakes.StaticResource), "")] + [InlineData("Help text", null, "Help text")] + [InlineData("HelpText", typeof(Fakes.StaticResource), "Localized HelpText")] + [InlineData("HelpText", typeof(Fakes.NonStaticResource), "Localized HelpText")] + public static void VerbHelpText(string helpText, Type resourceType, string expected) + { + TestVerbAttribute verbAttribute = new TestVerbAttribute + { + HelpText = helpText, + ResourceType = resourceType + }; + + Assert.Equal(expected, verbAttribute.HelpText); + } + + [Theory] + [InlineData("HelpText", typeof(Fakes.NonStaticResource_WithNonStaticProperty))] + [InlineData("WriteOnlyText", typeof(Fakes.NonStaticResource))] + [InlineData("PrivateOnlyText", typeof(Fakes.NonStaticResource))] + [InlineData("HelpText", typeof(Fakes.InternalResource))] + public void ThrowsHelpText(string helpText, Type resourceType) + { + TestVerbAttribute verbAttribute = new TestVerbAttribute + { + HelpText = helpText, + ResourceType = resourceType + }; + + // Verify exception + Assert.Throws(() => verbAttribute.HelpText); + } + + private class TestVerbAttribute : VerbAttribute + { + public TestVerbAttribute() : base("verb") + { + // Do nothing + } + } + } +}