diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..b713459b --- /dev/null +++ b/.editorconfig @@ -0,0 +1,26 @@ +#top-most EditorConfig for project +root = true + +[*] +end_of_line = crlf +insert_final_newline = true + +[*.cs] +indent_style = space +indent_size = 4 + +[*.{xml,csproj,config}] +indent_style = tab +indent_size = 4 + +[*.json] +indent_style = space +indent_size = 2 + +[*.js] +indent_style = space +indent_size = 2 + +[*.yml] +indent_style = space +indent_size = 2 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/.paket/Paket.Restore.targets b/.paket/Paket.Restore.targets deleted file mode 100644 index a86be3a1..00000000 --- a/.paket/Paket.Restore.targets +++ /dev/null @@ -1,272 +0,0 @@ - - - - - - - $(MSBuildAllProjects);$(MSBuildThisFileFullPath) - - true - $(MSBuildThisFileDirectory) - $(MSBuildThisFileDirectory)..\ - $(PaketRootPath)paket-files\paket.restore.cached - $(PaketRootPath)paket.lock - /Library/Frameworks/Mono.framework/Commands/mono - mono - - $(PaketRootPath)paket.exe - $(PaketToolsPath)paket.exe - "$(PaketExePath)" - $(MonoPath) --runtime=v4.0.30319 "$(PaketExePath)" - - - <_PaketExeExtension>$([System.IO.Path]::GetExtension("$(PaketExePath)")) - dotnet "$(PaketExePath)" - - $(PaketRootPath)paket.bootstrapper.exe - $(PaketToolsPath)paket.bootstrapper.exe - "$(PaketBootStrapperExePath)" - $(MonoPath) --runtime=v4.0.30319 "$(PaketBootStrapperExePath)" - - - - - true - true - - - - - - - true - $(NoWarn);NU1603 - - - - - /usr/bin/shasum $(PaketRestoreCacheFile) | /usr/bin/awk '{ print $1 }' - /usr/bin/shasum $(PaketLockFilePath) | /usr/bin/awk '{ print $1 }' - - - - - - - - - - - - - $([System.IO.File]::ReadAllText('$(PaketRestoreCacheFile)')) - $([System.IO.File]::ReadAllText('$(PaketLockFilePath)')) - true - false - true - - - - - - - - - $(MSBuildProjectDirectory)\obj\$(MSBuildProjectFile).paket.references.cached - - $(MSBuildProjectFullPath).paket.references - - $(MSBuildProjectDirectory)\$(MSBuildProjectName).paket.references - - $(MSBuildProjectDirectory)\paket.references - $(MSBuildProjectDirectory)\obj\$(MSBuildProjectFile).$(TargetFramework).paket.resolved - true - references-file-or-cache-not-found - - - - - $([System.IO.File]::ReadAllText('$(PaketReferencesCachedFilePath)')) - $([System.IO.File]::ReadAllText('$(PaketOriginalReferencesFilePath)')) - references-file - false - - - - - false - - - - - true - target-framework '$(TargetFramework)' - - - - - - - - - - - - - - - - - $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[0]) - $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[1]) - $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[4]) - - - %(PaketReferencesFileLinesInfo.PackageVersion) - All - - - - - $(MSBuildProjectDirectory)/obj/$(MSBuildProjectFile).paket.clitools - - - - - - - - - $([System.String]::Copy('%(PaketCliToolFileLines.Identity)').Split(',')[0]) - $([System.String]::Copy('%(PaketCliToolFileLines.Identity)').Split(',')[1]) - - - %(PaketCliToolFileLinesInfo.PackageVersion) - - - - - $(MSBuildProjectDirectory)/obj/$(MSBuildProjectFile).NuGet.Config - - - - - - - false - - - - - - <_NuspecFilesNewLocation Include="$(BaseIntermediateOutputPath)$(Configuration)\*.nuspec"/> - - - - $(MSBuildProjectDirectory)/$(MSBuildProjectFile) - true - false - true - $(BaseIntermediateOutputPath)$(Configuration) - $(BaseIntermediateOutputPath) - - - - <_NuspecFiles Include="$(AdjustedNuspecOutputPath)\*.nuspec"/> - - - - - - - - - - - - - - - - diff --git a/.paket/paket.bootstrapper.exe b/.paket/paket.bootstrapper.exe deleted file mode 100644 index b98e000b..00000000 Binary files a/.paket/paket.bootstrapper.exe and /dev/null differ 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.nuspec b/CommandLine.nuspec deleted file mode 100644 index 86eb1a0c..00000000 --- a/CommandLine.nuspec +++ /dev/null @@ -1,44 +0,0 @@ - - - - CommandLineParser - $version$ - Command Line Parser Library - gsscoder nemec ericnewton76 - 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. - - Copyright (c) 2005 - 2018 Giacomo Stelluti Scala & Contributors - https://github.com/commandlineparser/commandline/blob/master/License.md - https://github.com/commandlineparser/commandline - Giacomo Stelluti Scala - https://raw.githubusercontent.com/commandlineparser/commandline/master/art/CommandLine20.png - false - command line commandline argument option parser parsing library syntax shell - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/CommandLine.sln b/CommandLine.sln index 76c37ff2..102837f1 100644 --- a/CommandLine.sln +++ b/CommandLine.sln @@ -1,27 +1,13 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 14 -VisualStudioVersion = 14.0.25420.1 +# 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("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".Welcome", ".Welcome", "{D9C1F005-94A7-4A2A-81F3-9C053D5A6AEC}" - ProjectSection(SolutionItems) = preProject - License.md = License.md - README.md = README.md - EndProjectSection +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}") = "FSharp", "FSharp", "{751E6303-1623-4418-B298-4FF97DA5C86E}" - ProjectSection(SolutionItems) = preProject - demo\fsharp-demo.fsx = demo\fsharp-demo.fsx - EndProjectSection -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{A6C99AE7-2FE1-4393-9281-7BFCF46A6F53}" - ProjectSection(SolutionItems) = preProject - build.fsx = build.fsx - EndProjectSection -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommandLine.Tests", "tests\CommandLine.Tests\CommandLine.Tests.csproj", "{0A15C4D2-B3E9-43AB-8155-1B39F7AC8A5E}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{1361E8B1-D0E1-493E-B8C1-7380A7B7C472}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -41,6 +27,12 @@ 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 GlobalSection(MonoDevelopProperties) = preSolution StartupItem = src\CommandLine\CommandLine.csproj EndGlobalSection diff --git a/CommandLine.sln.DotSettings b/CommandLine.sln.DotSettings deleted file mode 100644 index e418e867..00000000 --- a/CommandLine.sln.DotSettings +++ /dev/null @@ -1,4 +0,0 @@ - - <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> \ No newline at end of file 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/Commandline.FSharp.nuspec b/Commandline.FSharp.nuspec deleted file mode 100644 index b08c9b35..00000000 --- a/Commandline.FSharp.nuspec +++ /dev/null @@ -1,50 +0,0 @@ - - - - CommandLineParser.FSharp - $version$ - Command Line Parser Library - gsscoder nemec ericnewton76 - 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://github.com/commandlineparser/commandline/blob/master/License.md - https://github.com/commandlineparser/commandline - Giacomo Stelluti Scala - https://raw.githubusercontent.com/commandlineparser/commandline/master/art/CommandLine20.png - false - command line commandline argument option parser parsing library syntax shell - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 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 acb67113..79a16fa7 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ -[![Build status](https://img.shields.io/appveyor/ci/commandlineparser/commandline.svg)](https://ci.appveyor.com/project/commandlineparser/commandline) -[![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) -[![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) +[![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)](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 @@ -11,25 +11,43 @@ The Command Line Parser Library offers CLR applications a clean and concise API for manipulating command line arguments and related tasks, such as defining switches, options and verb commands. It allows you to display a help screen with a high degree of customization and a simple way to report syntax errors to the end user. ``` -C:\Project> Nuget Install CommandLineParser +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._ +- 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). @@ -37,7 +55,7 @@ __This library provides _hassle free_ command line parsing with a constantly upd You can utilize the parser library in several ways: -- Install via Nuget/Paket +- Install via NuGet/Paket: [https://www.nuget.org/packages/CommandLineParser/](https://www.nuget.org/packages/CommandLineParser/) - Integrate directly into your project by copying the .cs files into your project. - ILMerge during your build process. @@ -48,7 +66,7 @@ You can utilize the parser library in several ways: C# Quick Start: -```csharp +```cs using System; using CommandLine; @@ -69,8 +87,8 @@ namespace QuickStart { if (o.Verbose) { - Console.WriteLine($"Verbose output has been enabled. Current Arguments: -v {o.Verbose}"); - Console.WriteLine("Quick Start Example! App is in Verbose mode so verbose messages will be shown!"); + Console.WriteLine($"Verbose output enabled. Current Arguments: -v {o.Verbose}"); + Console.WriteLine("Quick Start Example! App is in Verbose mode!"); } else { @@ -83,19 +101,27 @@ namespace QuickStart } ``` -C# Examples: +## C# Examples: + +
+ Click to expand! + +```cs -```csharp class Options { [Option('r', "read", Required = true, HelpText = "Input files to be processed.")] public IEnumerable InputFiles { get; set; } // Omitting long name, defaults to name of property, ie "--verbose" - [Option(Default = false, HelpText = "Prints all messages to standard output.")] + [Option( + Default = false, + HelpText = "Prints all messages to standard output.")] public bool Verbose { get; set; } - - [Option("stdin", Default = false, HelpText = "Read from stdin")] + + [Option("stdin", + Default = false, + HelpText = "Read from stdin")] public bool stdin { get; set; } [Value(0, MetaName = "offset", HelpText = "File offset.")] @@ -105,18 +131,35 @@ 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; - [] language : string; + [] language : string; [] offset : int64 option; } @@ -126,23 +169,32 @@ let main argv = | :? Parsed as parsed -> run parsed.Value | :? NotParsed as notParsed -> fail notParsed.Errors ``` +
-VB.Net: - -```VB.NET -Class Options - - Public Property InputFiles As IEnumerable(Of String) +## VB.NET Example: - ' Omitting long name, defaults to name of property, ie "--verbose" - - Public Property Verbose As Boolean +
+ Click to expand! - - Public Property Language As String +```vb - - Public Property Offset As Long? +Class Options + + Public Property InputFiles As IEnumerable(Of String) + + ' Omitting long name, defaults to name of property, ie "--verbose" + + Public Property Verbose As Boolean + + + Public Property Language As String + + + Public Property Offset As Long? End Class Sub Main(ByVal args As String()) @@ -151,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.")] @@ -167,11 +224,11 @@ class AddOptions { } [Verb("commit", HelpText = "Record changes to the repository.")] class CommitOptions { - //normal options here + //commit options here } [Verb("clone", HelpText = "Clone a repository into a new directory.")] class CloneOptions { - //normal options here + //clone options here } int Main(string[] args) { @@ -183,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 @@ -210,10 +272,14 @@ Function Main(ByVal args As String()) As Integer ) End Function ``` +
-F# Example: +### F# Example: -```fsharp +
+ Click to expand! + +```fs open CommandLine [] @@ -240,8 +306,13 @@ let main args = | :? CloneOptions as opts -> RunCloneAndReturnExitCode opts | :? CommandLine.NotParsed -> 1 ``` +
+ +# Release History + +See the [changelog](CHANGELOG.md) -# Contibutors +# Contributors First off, _Thank you!_ All contributions are welcome. Please consider sticking with the GNU getopt standard for command line parsing. @@ -255,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) @@ -276,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 255504e9..d11d6abf 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,14 +1,13 @@ -#version should be changed with any pull requests -version: 2.2.{build} +#version should be only changed with RELEASE eminent, see RELEASE.md + +version: 2.9.1-ci-{build} + +image: Visual Studio 2019 clone_depth: 1 pull_requests: do_not_increment_build_number: true -#cache: -#- packages -> paket.dependencies -#- paket-files > paket.dependencies - init: - ps: | git config --global core.autocrlf input @@ -18,50 +17,26 @@ init: if($ver.StartsWith("v") -eq $true) { $ver = $ver.Substring(1) } Update-AppveyorBuild -Version $ver } - elseif([System.String]::IsNullOrEmpty($env:APPVEYOR_PULL_REQUEST_NUMBER) -eq $true) { - $ver = $env:APPVEYOR_BUILD_VERSION - $commit = $env:APPVEYOR_REPO_COMMIT.substring(0,7) - Update-AppveyorBuild -Version "$ver-$commit" - } - -#version patching -assembly_info: - file: src\SharedAssemblyInfo.cs - patch: true - assembly_version: '{version}' - assembly_file_version: '{version}' - assembly_informational_version: $(APPVEYOR_BUILD_VERSION) -dotnet_csproj: - patch: true - file: '**\*.csproj' - version: '{version}' - package_version: '{version}' - assembly_version: '{version}' - file_version: '{version}' - informational_version: '{version}' - environment: matrix: - BUILD_TARGET: base - NUSPEC_FILE: CommandLine - BUILD_TARGET: fsharp - NUSPEC_FILE: CommandLine.FSharp build_script: - - cmd: build.cmd %BUILD_TARGET% +- cmd: dotnet build src/CommandLine/ -c Release --version-suffix %APPVEYOR_BUILD_VERSION% /p:BuildTarget=%BUILD_TARGET% -after_build: -- ps: | - powershell get-childitem Release\* -include *.dll | foreach-object { "{0}`t{1}" -f $_.Path, [System.Diagnostics.FileVersionInfo]::GetVersionInfo($_).FileVersion } - - .\build-nuget-pack.cmd $env:BUILD_TARGET $env:NUSPEC_FILE +test_script: +- cmd: dotnet test tests/CommandLine.Tests/ /p:BuildTarget=%BUILD_TARGET% -test: auto +after_test: +- cmd: dotnet pack src/CommandLine/ -c Release --version-suffix %APPVEYOR_BUILD_VERSION% /p:BuildTarget=%BUILD_TARGET% artifacts: -- path: 'Release/**/*.nupkg' - name: NugetPackages +- path: 'src/CommandLine/bin/Release/*.nupkg' + name: NuGetPackages +- path: 'src/CommandLine/bin/Release/*.snupkg' + name: symbol on_failure: - cmd: | @@ -71,9 +46,7 @@ on_failure: deploy: - provider: NuGet api_key: - secure: +Zxb8M5W+UJV1yd9n8seu3PvH/hGNPEmgriGBnsSmtxjKPQAJ4+iL7tKAmfPHAuG - artifact: 'NuGetPackages' + secure: llMIgYMuLHh9thyKMEAmkWraTaA9Zvcm1F8/yRwm0HCiPIt/ehR/GI4kJKyMTPyf + artifact: /.*(\.|\.s)nupkg/ on: - branch: /master|v\d+\.\d+\.\d+[.*]/ APPVEYOR_REPO_TAG: true - diff --git a/build-nuget-pack.cmd b/build-nuget-pack.cmd deleted file mode 100644 index ef3af1f2..00000000 --- a/build-nuget-pack.cmd +++ /dev/null @@ -1,19 +0,0 @@ -@ECHO OFF -setlocal - -if "%1" == "" goto :USAGE -if "%2" == "" goto :USAGE - -pushd Release\%1 - -copy ..\..\README.md -copy ..\..\%2.nuspec . -nuget pack "%2.nuspec" -properties Version=%APPVEYOR_BUILD_VERSION% -if errorlevel 1 popd&exit 1 /b -goto :END - -:USAGE -echo build-nuget-pack - -:END -popd diff --git a/build.cmd b/build.cmd deleted file mode 100644 index 595b806e..00000000 --- a/build.cmd +++ /dev/null @@ -1,49 +0,0 @@ -@echo off -setlocal - -cls - -if "%1" == "" goto :USAGE -if "%1" == "base" set BUILD_TARGET=base -if "%1" == "fsharp" set BUILD_TARGET=fsharp - -echo. -echo SKIP_RESTORE=%SKIP_RESTORE% ^<^< Set to true if have already restored packages -if "%SKIP_RESTORE%" == "" choice /T 5 /D Y /M "Continue?" - -if "%SKIP_RESTORE%" == "true" goto :BUILD_NET -.paket\paket.bootstrapper.exe -if errorlevel 1 ( - exit /b %errorlevel% -) - -.paket\paket.exe restore -if errorlevel 1 ( - exit /b %errorlevel% -) - -:BUILD_NET -echo. - -msbuild CommandLine.sln /p:Configuration=Release /p:OutputPath=%~dp0\release\%BUILD_TARGET%\net4x - -if "%SKIP_RESTORE%" == "true" goto :BUILD_NETSTD -echo. -echo dotnet restore -dotnet restore - -:BUILD_NETSTD -echo. -echo dotnet build --output %~dp0\release\%BUILD_TARGET%\netstandard1.5 -dotnet build --configuration Release --output %~dp0release\%BUILD_TARGET%\netstandard1.5 --framework netstandard1.5 src\commandline - -goto :END - -:USAGE -echo. -echo Invalid arguments specified. -echo. -echo Usage: build -echo where is base or fsharp - -:END diff --git a/build.fsx b/build.fsx deleted file mode 100644 index 7ef072f0..00000000 --- a/build.fsx +++ /dev/null @@ -1,47 +0,0 @@ -#r "packages/FAKE/tools/FakeLib.dll" -open Fake -open Fake.Testing - -let buildDir = "./build/" -let testDir = "./build/test/" -let packagingDir = "./nuget/" - -let authors = ["Giacomo Stelluti Scala"] -let projectDescription = "The Command Line Parser Library offers to CLR applications a clean and concise API for manipulating command line arguments and related tasks." -let projectSummary = "Command Line Parser Library" -let buildVersion = "2.2.0.0" - -Target "Clean" (fun _ -> - CleanDirs [buildDir; testDir] -) - -Target "Default" (fun _ -> - trace "Command Line Parser Library 2.0 pre-release" -) - -Target "BuildLib" (fun _ -> - !! "src/CommandLine/CommandLine.csproj" - |> MSBuildRelease buildDir "Build" - |> Log "LibBuild-Output: " -) - -Target "BuildTest" (fun _ -> - !! "tests/CommandLine.Tests/CommandLine.Tests.csproj" - |> MSBuildDebug testDir "Build" - |> Log "TestBuild-Output: " -) - -Target "Test" (fun _ -> - //trace "Running Tests..." - !! (testDir @@ "\CommandLine.Tests.dll") - |> xUnit2 (fun p -> {p with HtmlOutputPath = Some(testDir @@ "xunit.html")}) -) - -// Dependencies -"Clean" - ==> "BuildLib" - ==> "BuildTest" - ==> "Test" - ==> "Default" - -RunTargetOrDefault "Default" diff --git a/build.sh b/build.sh deleted file mode 100755 index 83c11f4c..00000000 --- a/build.sh +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/env bash -if test "$OS" = "Windows_NT" -then - # use .Net - - .paket/paket.bootstrapper.exe - exit_code=$? - if [ $exit_code -ne 0 ]; then - exit $exit_code - fi - - .paket/paket.exe restore - exit_code=$? - if [ $exit_code -ne 0 ]; then - exit $exit_code - fi - - packages/FAKE/tools/FAKE.exe $@ --fsiargs -d:MONO build.fsx -else - # use mono - mono .paket/paket.bootstrapper.exe - exit_code=$? - if [ $exit_code -ne 0 ]; then - exit $exit_code - fi - - mono .paket/paket.exe restore - exit_code=$? - if [ $exit_code -ne 0 ]; then - exit $exit_code - fi - mono packages/FAKE/tools/FAKE.exe $@ --fsiargs -d:MONO build.fsx -fi - -dotnet build --configuration Release 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/global.json b/global.json deleted file mode 100644 index f83a8f81..00000000 --- a/global.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "projects": [ - "src", - "tests" - ], - "sdk": { - "version": "1.0.0-preview2-003121" - } -} diff --git a/paket.dependencies b/paket.dependencies deleted file mode 100644 index b59d342c..00000000 --- a/paket.dependencies +++ /dev/null @@ -1,14 +0,0 @@ -source https://nuget.org/api/v2 - -nuget FAKE -nuget FSharp.Core -nuget FluentAssertions -nuget xunit -nuget xunit.runner.visualstudio version_in_path: true -nuget xunit.runner.console -nuget FSharp.Formatting -nuget FsCheck - -github gsscoder/CSharpx src/CSharpx/Maybe.cs -github gsscoder/CSharpx src/CSharpx/EnumerableExtensions.cs -github gsscoder/railwaysharp src/RailwaySharp/ErrorHandling.cs diff --git a/paket.lock b/paket.lock deleted file mode 100644 index a2282355..00000000 --- a/paket.lock +++ /dev/null @@ -1,240 +0,0 @@ -NUGET - remote: https://www.nuget.org/api/v2 - FAKE (4.3.4) - FluentAssertions (4.0) - System.Collections (>= 4.0.10) - framework: dnxcore50 - System.Diagnostics.Debug (>= 4.0.10) - framework: dnxcore50 - System.Globalization (>= 4.0.10) - framework: dnxcore50 - System.Linq (>= 4.0) - framework: dnxcore50 - System.Linq.Expressions (>= 4.0.10) - framework: dnxcore50 - System.ObjectModel (>= 4.0.10) - framework: dnxcore50 - System.Reflection (>= 4.0.10) - framework: dnxcore50 - System.Reflection.Extensions (>= 4.0) - framework: dnxcore50 - System.Reflection.Primitives (>= 4.0) - framework: dnxcore50 - System.Reflection.TypeExtensions (>= 4.0) - framework: dnxcore50 - System.Runtime (>= 4.0.20) - framework: dnxcore50 - System.Runtime.Extensions (>= 4.0.10) - framework: dnxcore50 - System.Text.RegularExpressions (>= 4.0.10) - framework: dnxcore50 - System.Threading (>= 4.0.10) - framework: dnxcore50 - System.Threading.Tasks (>= 4.0.10) - framework: dnxcore50 - System.Xml.XDocument (>= 4.0.10) - framework: dnxcore50 - FsCheck (2.0.7) - FSharp.Core (>= 3.1.2.5) - FSharp.Compiler.Service (1.4.0.1) - FSharp.Core (4.0.0.1) - FSharp.Formatting (2.10) - FSharp.Compiler.Service (>= 0.0.87) - FSharpVSPowerTools.Core (1.8) - FSharpVSPowerTools.Core (1.8) - FSharp.Compiler.Service (>= 0.0.87) - System.Collections (4.0.10) - framework: dnxcore50 - System.Diagnostics.Debug (>= 4.0) - framework: dnxcore50 - System.Resources.ResourceManager (>= 4.0) - framework: dnxcore50 - System.Runtime (>= 4.0) - framework: dnxcore50 - System.Runtime (>= 4.0.20) - framework: dnxcore50 - System.Runtime.Extensions (>= 4.0) - framework: dnxcore50 - System.Threading (>= 4.0) - framework: dnxcore50 - System.Diagnostics.Contracts (4.0) - framework: dnxcore50 - System.Runtime (>= 4.0) - framework: dnxcore50 - System.Diagnostics.Debug (4.0.10) - framework: dnxcore50 - System.Runtime (>= 4.0) - framework: dnxcore50 - System.Globalization (4.0.10) - framework: dnxcore50 - System.Runtime (>= 4.0) - framework: dnxcore50 - System.IO (4.0.10) - framework: dnxcore50 - System.Globalization (>= 4.0) - framework: dnxcore50 - System.Runtime (>= 4.0.20) - framework: dnxcore50 - System.Text.Encoding (>= 4.0) - framework: dnxcore50 - System.Text.Encoding (>= 4.0.10) - framework: dnxcore50 - System.Text.Encoding.Extensions (>= 4.0) - framework: dnxcore50 - System.Threading (>= 4.0) - framework: dnxcore50 - System.Threading.Tasks (>= 4.0) - framework: dnxcore50 - System.IO.FileSystem (4.0) - framework: dnxcore50 - System.Collections (>= 4.0.10) - framework: dnxcore50 - System.Diagnostics.Debug (>= 4.0.10) - framework: dnxcore50 - System.IO (>= 4.0) - framework: dnxcore50 - System.IO (>= 4.0.10) - framework: dnxcore50 - System.IO.FileSystem.Primitives (>= 4.0) - framework: dnxcore50 - System.Resources.ResourceManager (>= 4.0) - framework: dnxcore50 - System.Runtime (>= 4.0) - framework: dnxcore50 - System.Runtime (>= 4.0.20) - framework: dnxcore50 - System.Runtime.Extensions (>= 4.0.10) - framework: dnxcore50 - System.Runtime.Handles (>= 4.0) - framework: dnxcore50 - System.Runtime.InteropServices (>= 4.0.20) - framework: dnxcore50 - System.Runtime.WindowsRuntime (>= 4.0) - framework: dnxcore50 - System.Text.Encoding (>= 4.0) - framework: dnxcore50 - System.Text.Encoding (>= 4.0.10) - framework: dnxcore50 - System.Text.Encoding.Extensions (>= 4.0.10) - framework: dnxcore50 - System.Threading (>= 4.0.10) - framework: dnxcore50 - System.Threading.Overlapped (>= 4.0) - framework: dnxcore50 - System.Threading.Tasks (>= 4.0) - framework: dnxcore50 - System.Threading.Tasks (>= 4.0.10) - framework: dnxcore50 - System.IO.FileSystem.Primitives (4.0) - framework: dnxcore50 - System.Runtime (>= 4.0.20) - framework: dnxcore50 - System.Linq (4.0) - framework: dnxcore50 - System.Collections (>= 4.0.10) - framework: dnxcore50 - System.Diagnostics.Debug (>= 4.0.10) - framework: dnxcore50 - System.Resources.ResourceManager (>= 4.0) - framework: dnxcore50 - System.Runtime (>= 4.0.20) - framework: dnxcore50 - System.Runtime.Extensions (>= 4.0.10) - framework: dnxcore50 - System.Linq.Expressions (4.0.10) - framework: dnxcore50 - System.Collections (>= 4.0) - framework: dnxcore50 - System.Diagnostics.Debug (>= 4.0) - framework: dnxcore50 - System.Globalization (>= 4.0) - framework: dnxcore50 - System.IO (>= 4.0) - framework: dnxcore50 - System.Linq (>= 4.0) - framework: dnxcore50 - System.ObjectModel (>= 4.0) - framework: dnxcore50 - System.Reflection (>= 4.0) - framework: dnxcore50 - System.Reflection.Emit (>= 4.0) - framework: dnxcore50 - System.Reflection.Extensions (>= 4.0) - framework: dnxcore50 - System.Reflection.Primitives (>= 4.0) - framework: dnxcore50 - System.Reflection.TypeExtensions (>= 4.0) - framework: dnxcore50 - System.Resources.ResourceManager (>= 4.0) - framework: dnxcore50 - System.Runtime (>= 4.0) - framework: dnxcore50 - System.Runtime (>= 4.0.20) - framework: dnxcore50 - System.Runtime.Extensions (>= 4.0) - framework: dnxcore50 - System.Threading (>= 4.0) - framework: dnxcore50 - System.ObjectModel (4.0.10) - framework: dnxcore50 - System.Collections (>= 4.0.10) - framework: dnxcore50 - System.Diagnostics.Debug (>= 4.0.10) - framework: dnxcore50 - System.Resources.ResourceManager (>= 4.0) - framework: dnxcore50 - System.Runtime (>= 4.0.20) - framework: dnxcore50 - System.Threading (>= 4.0.10) - framework: dnxcore50 - System.Private.Uri (4.0) - framework: dnxcore50 - System.Reflection (4.0.10) - framework: dnxcore50 - System.IO (>= 4.0) - framework: dnxcore50 - System.Reflection.Primitives (>= 4.0) - framework: dnxcore50 - System.Runtime (>= 4.0.20) - framework: dnxcore50 - System.Reflection.Emit (4.0) - framework: dnxcore50 - System.IO (>= 4.0) - framework: dnxcore50 - System.Reflection (>= 4.0) - framework: dnxcore50 - System.Reflection.Emit.ILGeneration (>= 4.0) - framework: dnxcore50 - System.Reflection.Primitives (>= 4.0) - framework: dnxcore50 - System.Runtime (>= 4.0) - framework: dnxcore50 - System.Reflection.Emit.ILGeneration (4.0) - framework: dnxcore50 - System.Reflection (>= 4.0) - framework: dnxcore50 - System.Reflection.Primitives (>= 4.0) - framework: dnxcore50 - System.Runtime (>= 4.0) - framework: dnxcore50 - System.Reflection.Extensions (4.0) - framework: dnxcore50 - System.Diagnostics.Debug (>= 4.0.10) - framework: dnxcore50 - System.Reflection (>= 4.0) - framework: dnxcore50 - System.Reflection (>= 4.0.10) - framework: dnxcore50 - System.Reflection.Primitives (>= 4.0) - framework: dnxcore50 - System.Reflection.TypeExtensions (>= 4.0) - framework: dnxcore50 - System.Resources.ResourceManager (>= 4.0) - framework: dnxcore50 - System.Runtime (>= 4.0) - framework: dnxcore50 - System.Runtime (>= 4.0.20) - framework: dnxcore50 - System.Runtime.Extensions (>= 4.0.10) - framework: dnxcore50 - System.Reflection.Primitives (4.0) - framework: dnxcore50 - System.Runtime (>= 4.0) - framework: dnxcore50 - System.Threading (>= 4.0) - framework: dnxcore50 - System.Reflection.TypeExtensions (4.0) - framework: dnxcore50 - System.Diagnostics.Contracts (>= 4.0) - framework: dnxcore50 - System.Diagnostics.Debug (>= 4.0.10) - framework: dnxcore50 - System.Linq (>= 4.0) - framework: dnxcore50 - System.Reflection (>= 4.0) - framework: dnxcore50 - System.Reflection (>= 4.0.10) - framework: dnxcore50 - System.Reflection.Primitives (>= 4.0) - framework: dnxcore50 - System.Resources.ResourceManager (>= 4.0) - framework: dnxcore50 - System.Runtime (>= 4.0) - framework: dnxcore50 - System.Runtime (>= 4.0.20) - framework: dnxcore50 - System.Runtime.Extensions (>= 4.0.10) - framework: dnxcore50 - System.Resources.ResourceManager (4.0) - framework: dnxcore50 - System.Globalization (>= 4.0) - framework: dnxcore50 - System.Reflection (>= 4.0) - framework: dnxcore50 - System.Reflection (>= 4.0.10) - framework: dnxcore50 - System.Runtime (>= 4.0) - framework: dnxcore50 - System.Runtime (>= 4.0.20) - framework: dnxcore50 - System.Runtime (4.0.20) - framework: dnxcore50 - System.Private.Uri (>= 4.0) - framework: dnxcore50 - System.Runtime.Extensions (4.0.10) - framework: dnxcore50 - System.Runtime (>= 4.0.20) - framework: dnxcore50 - System.Runtime.Handles (4.0) - framework: dnxcore50 - System.Runtime (>= 4.0) - framework: dnxcore50 - System.Runtime.InteropServices (4.0.20) - framework: dnxcore50 - System.Reflection (>= 4.0) - framework: dnxcore50 - System.Reflection.Primitives (>= 4.0) - framework: dnxcore50 - System.Runtime (>= 4.0) - framework: dnxcore50 - System.Runtime.Handles (>= 4.0) - framework: dnxcore50 - System.Runtime.WindowsRuntime (4.0.10) - framework: dnxcore50 - System.Diagnostics.Debug (>= 4.0.10) - framework: dnxcore50 - System.Globalization (>= 4.0) - framework: dnxcore50 - System.IO (>= 4.0) - framework: dnxcore50 - System.IO (>= 4.0.10) - framework: dnxcore50 - System.ObjectModel (>= 4.0) - framework: dnxcore50 - System.Resources.ResourceManager (>= 4.0) - framework: dnxcore50 - System.Runtime (>= 4.0) - framework: dnxcore50 - System.Runtime (>= 4.0.20) - framework: dnxcore50 - System.Runtime.Extensions (>= 4.0) - framework: dnxcore50 - System.Runtime.InteropServices (>= 4.0.20) - framework: dnxcore50 - System.Threading (>= 4.0.10) - framework: dnxcore50 - System.Threading.Tasks (>= 4.0) - framework: dnxcore50 - System.Threading.Tasks (>= 4.0.10) - framework: dnxcore50 - System.Text.Encoding (4.0.10) - framework: dnxcore50 - System.Runtime (>= 4.0) - framework: dnxcore50 - System.Text.Encoding.Extensions (4.0.10) - framework: dnxcore50 - System.Runtime (>= 4.0) - framework: dnxcore50 - System.Text.Encoding (>= 4.0.10) - framework: dnxcore50 - System.Text.RegularExpressions (4.0.10) - framework: dnxcore50 - System.Collections (>= 4.0.10) - framework: dnxcore50 - System.Globalization (>= 4.0.10) - framework: dnxcore50 - System.Resources.ResourceManager (>= 4.0) - framework: dnxcore50 - System.Runtime (>= 4.0.20) - framework: dnxcore50 - System.Runtime.Extensions (>= 4.0.10) - framework: dnxcore50 - System.Threading (>= 4.0.10) - framework: dnxcore50 - System.Threading (4.0.10) - framework: dnxcore50 - System.Runtime (>= 4.0) - framework: dnxcore50 - System.Threading.Tasks (>= 4.0) - framework: dnxcore50 - System.Threading.Overlapped (4.0) - framework: dnxcore50 - System.Resources.ResourceManager (>= 4.0) - framework: dnxcore50 - System.Runtime (>= 4.0) - framework: dnxcore50 - System.Runtime (>= 4.0.20) - framework: dnxcore50 - System.Runtime.Extensions (>= 4.0) - framework: dnxcore50 - System.Runtime.Handles (>= 4.0) - framework: dnxcore50 - System.Runtime.InteropServices (>= 4.0.20) - framework: dnxcore50 - System.Threading (>= 4.0.10) - framework: dnxcore50 - System.Threading.Tasks (4.0.10) - framework: dnxcore50 - System.Runtime (>= 4.0) - framework: dnxcore50 - System.Xml.ReaderWriter (4.0.10) - framework: dnxcore50 - System.Collections (>= 4.0.10) - framework: dnxcore50 - System.Diagnostics.Debug (>= 4.0.10) - framework: dnxcore50 - System.Globalization (>= 4.0.10) - framework: dnxcore50 - System.IO (>= 4.0.10) - framework: dnxcore50 - System.IO.FileSystem (>= 4.0) - framework: dnxcore50 - System.IO.FileSystem.Primitives (>= 4.0) - framework: dnxcore50 - System.Resources.ResourceManager (>= 4.0) - framework: dnxcore50 - System.Runtime (>= 4.0.20) - framework: dnxcore50 - System.Runtime.Extensions (>= 4.0.10) - framework: dnxcore50 - System.Runtime.InteropServices (>= 4.0.20) - framework: dnxcore50 - System.Text.Encoding (>= 4.0.10) - framework: dnxcore50 - System.Text.Encoding.Extensions (>= 4.0.10) - framework: dnxcore50 - System.Text.RegularExpressions (>= 4.0.10) - framework: dnxcore50 - System.Threading.Tasks (>= 4.0.10) - framework: dnxcore50 - System.Xml.XDocument (4.0.10) - framework: dnxcore50 - System.Collections (>= 4.0.10) - framework: dnxcore50 - System.Diagnostics.Debug (>= 4.0.10) - framework: dnxcore50 - System.Globalization (>= 4.0.10) - framework: dnxcore50 - System.IO (>= 4.0.10) - framework: dnxcore50 - System.Reflection (>= 4.0.10) - framework: dnxcore50 - System.Resources.ResourceManager (>= 4.0) - framework: dnxcore50 - System.Runtime (>= 4.0.20) - framework: dnxcore50 - System.Runtime.Extensions (>= 4.0.10) - framework: dnxcore50 - System.Text.Encoding (>= 4.0.10) - framework: dnxcore50 - System.Threading (>= 4.0.10) - framework: dnxcore50 - System.Xml.ReaderWriter (>= 4.0.10) - framework: dnxcore50 - xunit (2.0) - xunit.assert (2.0) - xunit.core (2.0) - xunit.abstractions (2.0) - xunit.assert (2.0) - xunit.core (2.0) - xunit.extensibility.core (2.0) - xunit.extensibility.core (2.0) - xunit.abstractions (2.0) - xunit.runner.console (2.0) - xunit.runner.visualstudio (2.1) - version_in_path: true -GITHUB - remote: gsscoder/CSharpx - src/CSharpx/EnumerableExtensions.cs (1ae4e7b6ac1b19493f333e23c9ae636a01b23862) - src/CSharpx/Maybe.cs (1ae4e7b6ac1b19493f333e23c9ae636a01b23862) - remote: gsscoder/railwaysharp - src/RailwaySharp/ErrorHandling.cs (fa4f11173e470d558df80987e69e6ab10f977fcd) \ No newline at end of file diff --git a/src/CommandLine/BaseAttribute.cs b/src/CommandLine/BaseAttribute.cs index e390e88c..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,16 +92,8 @@ public object Default /// public string HelpText { - get { return helpText; } - set - { - if (value == null) - { - throw new ArgumentNullException("value"); - } - - helpText = value; - } + get => helpText.Value??string.Empty; + set => helpText.Value = value ?? throw new ArgumentNullException("value"); } /// @@ -127,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 8f829c9f..04496eb8 100644 --- a/src/CommandLine/CommandLine.csproj +++ b/src/CommandLine/CommandLine.csproj @@ -1,210 +1,56 @@ - - - - - Debug - AnyCPU - {E1BD3C65-49C3-49E7-BABA-C60980CB3F20} - Library - Properties - CommandLine - CommandLine - v4.0 - 512 - 12.0.0 - 2.0 - ..\..\ - true - - - true - full - false - bin\Debug\ - TRACE;DEBUG;CSX_EITHER_INTERNAL; CSX_REM_EITHER_BEYOND_2; CSX_ENUM_INTERNAL; ERRH_INTERNAL; ERRH_DISABLE_INLINE_METHODS; CSX_MAYBE_INTERNAL; CSX_REM_EITHER_FUNC;NET40;SKIP_FSHARP - prompt - 4 - true - bin\Debug\CommandLine.XML - - - pdbonly - true - bin\Release\ - TRACE;CSX_EITHER_INTERNAL; CSX_REM_EITHER_BEYOND_2; CSX_ENUM_INTERNAL; ERRH_INTERNAL; ERRH_DISABLE_INLINE_METHODS; CSX_MAYBE_INTERNAL; CSX_REM_EITHER_FUNC;NET40;SKIP_FSHARP - prompt - 4 - true - bin\Release\CommandLine.XML - - - true - - - ..\..\CommandLine.snk - - - - True - Infrastructure/ErrorHandling.cs - - - True - Infrastructure/EnumerableExtensions.cs - - - True - Infrastructure/Maybe.cs - - - - - - - - - - - Properties\SharedAssemblyInfo.cs - - - - - - - - - - - - - - Code - - - - - - - - - - - - - - - - - Code - - - - - - Code - - - - - - - - - - - - - - - - - - - - Code - - - Code - - - Code - - - - Code - - - - Code - - - - - - - - - - - - - - - - - - - - - - - ..\..\packages\FSharp.Core\lib\net20\FSharp.Core.dll - True - True - - - - - - - - ..\..\packages\FSharp.Core\lib\portable-net45+netcore45\FSharp.Core.dll - True - True - - - - - - - ..\..\packages\FSharp.Core\lib\portable-net45+netcore45+wp8\FSharp.Core.dll - True - True - - - - - - - ..\..\packages\FSharp.Core\lib\portable-net45+netcore45+wpa81+wp8\FSharp.Core.dll - True - True - - - - - - - ..\..\packages\FSharp.Core\lib\portable-net45+sl5+netcore45\FSharp.Core.dll - True - True - - - - - \ No newline at end of file + + + + 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 + + + + + + + + + + + + + + + + + diff --git a/src/CommandLine/CommandLine.csproj.DotSettings b/src/CommandLine/CommandLine.csproj.DotSettings deleted file mode 100644 index 7634a78e..00000000 --- a/src/CommandLine/CommandLine.csproj.DotSettings +++ /dev/null @@ -1,3 +0,0 @@ - - True - True \ No newline at end of file diff --git a/src/CommandLine/CommandLine.project.json b/src/CommandLine/CommandLine.project.json deleted file mode 100644 index 8a7aa92e..00000000 --- a/src/CommandLine/CommandLine.project.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - - "frameworks": { - "net40": { - } - }, - - "runtimes": { "win": { } } -} \ 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 0c24fddd..f48127b1 100644 --- a/src/CommandLine/Core/InstanceBuilder.cs +++ b/src/CommandLine/Core/InstanceBuilder.cs @@ -4,9 +4,6 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; -#if PLATFORM_DOTNET -using System.Reflection; -#endif using CommandLine.Infrastructure; using CSharpx; using RailwaySharp.ErrorHandling; @@ -23,53 +20,82 @@ public static ParserResult Build( StringComparer nameComparer, bool ignoreValueCase, CultureInfo parsingCulture, + bool autoHelp, + 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())); + Specification.FromProperty(pi), pi, Maybe.Nothing())) + .Memoize(); var specs = from pt in specProps select pt.Specification; var optionSpecs = specs .ThrowingValidate(SpecificationGuards.Lookup) - .OfType(); + .OfType() + .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.Memoize(); Func> buildUp = () => { - var tokenizerResult = tokenizer(arguments, optionSpecs); + var tokenizerResult = tokenizer(argumentsList, optionSpecs); - var tokens = tokenizerResult.SucceededWith(); + var tokens = tokenizerResult.SucceededWith().Memoize(); var partitions = TokenPartitioner.Partition( tokens, name => TypeLookup.FindTypeDescriptorAndSibling(name, optionSpecs, nameComparer)); - var optionsPartition = partitions.Item1; - var valuesPartition = partitions.Item2; - var errorsPartition = partitions.Item3; + 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)); + valuesPartition, + (vals, type, isScalar) => TypeConverter.ChangeType(vals, type, isScalar, false, parsingCulture, ignoreValueCase)); var missingValueErrors = from token in errorsPartition select @@ -78,61 +104,44 @@ public static ParserResult Build( .FromOptionSpecification()); var specPropsWithValue = - optionSpecPropsResult.SucceededWith().Concat(valueSpecPropsResult.SucceededWith()); + optionSpecPropsResult.SucceededWith().Concat(valueSpecPropsResult.SucceededWith()).Memoize(); + + 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()); - mutable = - mutable.SetProperties(specPropsWithValue, sp => sp.Value.IsJust(), sp => sp.Value.FromJustOrFail()) - .SetProperties( - specPropsWithValue, - sp => sp.Value.IsNothing() && sp.Specification.DefaultValue.IsJust(), - sp => sp.Specification.DefaultValue.FromJustOrFail()) - .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() - select - 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) - .Memorize(); + .Concat(setPropertyErrors) + .Memoize(); var warnings = from e in allErrors where nonFatalErrors.Contains(e.Tag) select e; return allErrors.Except(warnings).ToParserResult(instance); }; - var preprocessorErrors = arguments.Any() - ? arguments.Preprocess(PreprocessorGuards.Lookup(nameComparer)) - : Enumerable.Empty(); + var preprocessorErrors = ( + argumentsList.Any() + ? arguments.Preprocess(PreprocessorGuards.Lookup(nameComparer, autoHelp, autoVersion)) + : Enumerable.Empty() + ).Memoize(); - var result = arguments.Any() + var result = argumentsList.Any() ? preprocessorErrors.Any() ? notParsed(preprocessorErrors) : buildUp() @@ -140,5 +149,83 @@ join sp in specPropsWithValue on prms.Name.ToLower() equals sp.Property.Name.ToL 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 f68216b1..72307bf2 100644 --- a/src/CommandLine/Core/InstanceChooser.cs +++ b/src/CommandLine/Core/InstanceChooser.cs @@ -17,55 +17,134 @@ public static ParserResult Choose( IEnumerable types, IEnumerable arguments, StringComparer nameComparer, + bool ignoreValueCase, CultureInfo parsingCulture, + bool autoHelp, + 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 preprocCompare("help") + return (autoHelp && preprocCompare("help")) ? MakeNotParsed(types, MakeHelpVerbRequestedError(verbs, arguments.Skip(1).FirstOrDefault() ?? string.Empty, nameComparer)) - : preprocCompare("version") + : (autoVersion && preprocCompare("version")) ? MakeNotParsed(types, new VersionRequestedError()) - : MatchVerb(tokenizer, verbs, arguments, nameComparer, parsingCulture, 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, CultureInfo parsingCulture, + bool autoHelp, + 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, - false, + ignoreValueCase, parsingCulture, + autoHelp, + autoVersion, nonFatalErrors) : MakeNotParsed(verbs.Select(v => v.Item2), new BadVerbSelectedError(arguments.First())); } + private static ParserResult MatchVerb( + Func, IEnumerable, Result, Error>> tokenizer, + IEnumerable> verbs, + Tuple defaultVerb, + IEnumerable arguments, + StringComparer nameComparer, + bool ignoreValueCase, + 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 528ef34a..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; } @@ -33,6 +33,5 @@ public static Maybe HavingSeparator(string name, IEnumerable Maybe.Just(spec.Separator), Maybe.Nothing()); } - } } diff --git a/src/CommandLine/Core/OptionMapper.cs b/src/CommandLine/Core/OptionMapper.cs index 0d89b134..ded42c4f 100644 --- a/src/CommandLine/Core/OptionMapper.cs +++ b/src/CommandLine/Core/OptionMapper.cs @@ -15,38 +15,45 @@ 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()); } - ); + ).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/PreprocessorGuards.cs b/src/CommandLine/Core/PreprocessorGuards.cs index 30ffe4a0..8d6fb5be 100644 --- a/src/CommandLine/Core/PreprocessorGuards.cs +++ b/src/CommandLine/Core/PreprocessorGuards.cs @@ -9,13 +9,14 @@ namespace CommandLine.Core static class PreprocessorGuards { public static IEnumerable, IEnumerable>> - Lookup(StringComparer nameComparer) + Lookup(StringComparer nameComparer, bool autoHelp, bool autoVersion) { - return new List, IEnumerable>> - { - HelpCommand(nameComparer), - VersionCommand(nameComparer) - }; + var list = new List, IEnumerable>>(); + if (autoHelp) + list.Add(HelpCommand(nameComparer)); + if (autoVersion) + list.Add(VersionCommand(nameComparer)); + return list; } public static Func, IEnumerable> HelpCommand(StringComparer nameComparer) diff --git a/src/CommandLine/Core/ReflectionExtensions.cs b/src/CommandLine/Core/ReflectionExtensions.cs index 068b3034..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; @@ -40,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(); @@ -80,51 +79,38 @@ public static TargetType ToTargetType(this Type type) : TargetType.Scalar; } - public static T SetProperties( + public static IEnumerable SetProperties( this T instance, IEnumerable specProps, Func predicate, Func selector) { - return specProps.Where(predicate).Aggregate( - instance, - (current, specProp) => - { - specProp.Property.SetValue(current, selector(specProp)); - return instance; - }); + return specProps.Where(predicate).SelectMany(specProp => specProp.SetValue(instance, selector(specProp))); } - private static T SetValue(this PropertyInfo property, T instance, object value) + private static IEnumerable SetValue(this SpecificationProperty specProp, T instance, object value) { - Action fail = inner => { - throw new InvalidOperationException("Cannot set value to target instance.", inner); - }; - try { - property.SetValue(instance, value, null); + specProp.Property.SetValue(instance, value, null); + return Enumerable.Empty(); } -#if !PLATFORM_DOTNET - catch (TargetException e) - { - fail(e); - } -#endif - catch (TargetParameterCountException e) + catch (TargetInvocationException e) { - fail(e); + return new[] { new SetValueExceptionError(specProp.Specification.FromSpecification(), e.InnerException, value) }; } - catch (MethodAccessException e) + catch (ArgumentException e) { - fail(e); + var argEx = new ArgumentException(InvalidAttributeConfigurationError.ErrorMessage, e); + + return new[] { new SetValueExceptionError(specProp.Specification.FromSpecification(), argEx, value) }; } - catch (TargetInvocationException e) + + catch (Exception e) { - fail(e); + return new[] { new SetValueExceptionError(specProp.Specification.FromSpecification(), e, value) }; } - return instance; } public static object CreateEmptyArray(this Type type) @@ -134,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) @@ -179,47 +173,32 @@ public static TypeInfo ToTypeInfo(this Type type) public static object StaticMethod(this Type type, string name, params object[] args) { -#if NETSTANDARD1_5 - MethodInfo method = type.GetTypeInfo().GetDeclaredMethod(name); - return method.Invoke(null, args); -#else return type.GetTypeInfo().InvokeMember( name, BindingFlags.InvokeMethod | BindingFlags.Public | BindingFlags.Static, null, null, args); -#endif } public static object StaticProperty(this Type type, string name) { -#if NETSTANDARD1_5 - PropertyInfo property = type.GetTypeInfo().GetDeclaredProperty(name); - return property.GetValue(null); -#else return type.GetTypeInfo().InvokeMember( name, BindingFlags.GetProperty | BindingFlags.Public | BindingFlags.Static, null, null, new object[] { }); -#endif } public static object InstanceProperty(this Type type, string name, object target) { -#if NETSTANDARD1_5 - PropertyInfo property = type.GetTypeInfo().GetDeclaredProperty(name); - return property.GetValue(target); -#else return type.GetTypeInfo().InvokeMember( name, BindingFlags.GetProperty | BindingFlags.Public | BindingFlags.Instance, null, target, new object[] { }); -#endif } public static bool IsPrimitiveEx(this Type type) @@ -237,22 +216,12 @@ public static bool IsPrimitiveEx(this Type type) || Convert.GetTypeCode(type) != TypeCode.Object; } - -#if NET40 - public static Type GetTypeInfo(this Type type) - { - return type; - } -#else - public static Attribute[] GetCustomAttributes(this Type type, Type attributeType, bool inherit) - { - return type.GetTypeInfo().GetCustomAttributes(attributeType, inherit).OfType().ToArray(); - } - - public static Attribute[] GetCustomAttributes(this Assembly assembly, Type attributeType, bool inherit) + public static bool IsCustomStruct(this Type type) { - return assembly.GetCustomAttributes(attributeType).ToArray(); + 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; } -#endif } -} \ 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/SpecificationGuards.cs b/src/CommandLine/Core/SpecificationGuards.cs index 1de07da2..b6d7d122 100644 --- a/src/CommandLine/Core/SpecificationGuards.cs +++ b/src/CommandLine/Core/SpecificationGuards.cs @@ -13,7 +13,7 @@ static class SpecificationGuards Tuple.Create(GuardAgainstScalarWithRange(), "Scalar option specifications do not support range specification."), Tuple.Create(GuardAgainstSequenceWithWrongRange(), "Bad range in sequence option specifications."), Tuple.Create(GuardAgainstSequenceWithZeroRange(), "Zero is not allowed in range of sequence option specifications."), - Tuple.Create(GuardAgainstOneCharLongName(), "Long name should be longest than one character.") + Tuple.Create(GuardAgainstOneCharLongName(), "Long name should be longer than one character.") }; private static Func GuardAgainstScalarWithRange() diff --git a/src/CommandLine/Core/SpecificationPropertyExtensions.cs b/src/CommandLine/Core/SpecificationPropertyExtensions.cs index faf48e16..03245f56 100644 --- a/src/CommandLine/Core/SpecificationPropertyExtensions.cs +++ b/src/CommandLine/Core/SpecificationPropertyExtensions.cs @@ -3,9 +3,6 @@ using System; using System.Collections.Generic; using System.Linq; -#if PLATFORM_DOTNET -using System.Reflection; -#endif using CSharpx; using System.Reflection; @@ -15,14 +12,14 @@ static class SpecificationPropertyExtensions { public static SpecificationProperty WithSpecification(this SpecificationProperty specProp, Specification newSpecification) { - if (newSpecification == null) throw new ArgumentNullException("newSpecification"); + if (newSpecification == null) throw new ArgumentNullException(nameof(newSpecification)); return SpecificationProperty.Create(newSpecification, specProp.Property, specProp.Value); } public static SpecificationProperty WithValue(this SpecificationProperty specProp, Maybe newValue) { - if (newValue == null) throw new ArgumentNullException("newValue"); + if (newValue == null) throw new ArgumentNullException(nameof(newValue)); return SpecificationProperty.Create(specProp.Specification, specProp.Property, newValue); } 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 ebe6c0ca..4dc25f7f 100644 --- a/src/CommandLine/Core/TokenPartitioner.cs +++ b/src/CommandLine/Core/TokenPartitioner.cs @@ -11,24 +11,21 @@ namespace CommandLine.Core static class TokenPartitioner { public static - Tuple< - IEnumerable>>, // options - IEnumerable, // values - IEnumerable // errors - > Partition( + Tuple>>, IEnumerable, IEnumerable> Partition( IEnumerable tokens, Func> typeLookup) { - var tokenList = tokens.Memorize(); - var switches = Switch.Partition(tokenList, typeLookup).Memorize(); - var scalars = Scalar.Partition(tokenList, typeLookup).Memorize(); - var sequences = Sequence.Partition(tokenList, typeLookup).Memorize(); - var nonOptions = tokenList - .Where(t => !switches.Contains(t, ReferenceEqualityComparer.Default)) - .Where(t => !scalars.Contains(t, ReferenceEqualityComparer.Default)) - .Where(t => !sequences.Contains(t, ReferenceEqualityComparer.Default)).Memorize(); - var values = nonOptions.Where(v => v.IsValue()).Memorize(); - var errors = nonOptions.Except(values, (IEqualityComparer)ReferenceEqualityComparer.Default).Memorize(); + IEqualityComparer tokenComparer = ReferenceEqualityComparer.Default; + + 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) @@ -37,5 +34,151 @@ public static values.Select(t => 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 e0e09fd3..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); + 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(); - - 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); - - 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 ad2b7d81..2e27af40 100644 --- a/src/CommandLine/Core/TypeConverter.cs +++ b/src/CommandLine/Core/TypeConverter.cs @@ -4,9 +4,6 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; -#if PLATFORM_DOTNET -using System.Reflection; -#endif using CommandLine.Infrastructure; using CSharpx; using RailwaySharp.ErrorHandling; @@ -16,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) @@ -49,6 +48,27 @@ 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 + { + return Convert.ChangeType(value, type, conversionCulture); + } + catch (InvalidCastException) + { + // Required for converting from string to TimeSpan because Convert.ChangeType can't + return System.ComponentModel.TypeDescriptor.GetConverter(type).ConvertFrom(null, conversionCulture, value); + } + } + private static Result ChangeTypeScalarImpl(string value, Type conversionType, CultureInfo conversionCulture, bool ignoreValueCase) { Func changeType = () => @@ -71,10 +91,9 @@ private static Result ChangeTypeScalarImpl(string value, Type () => #if !SKIP_FSHARP isFsOption - ? FSharpOptionHelper.Some(type, Convert.ChangeType(value, type, conversionCulture)) : + ? FSharpOptionHelper.Some(type, ConvertString(value, type, conversionCulture)) : #endif - Convert.ChangeType(value, type, conversionCulture); - + ConvertString(value, type, conversionCulture); #if !SKIP_FSHARP Func empty = () => isFsOption ? FSharpOptionHelper.None(type) : null; #else @@ -102,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 @@ -119,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 880d4d4e..48e9427c 100644 --- a/src/CommandLine/Core/Verb.cs +++ b/src/CommandLine/Core/Verb.cs @@ -3,49 +3,42 @@ using System; using System.Collections.Generic; using System.Linq; -#if !NET40 using System.Reflection; -#endif 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) { - if (name == null) throw new ArgumentNullException("name"); - if (helpText == null) throw new ArgumentNullException("helpText"); - - this.name = name; - this.helpText = 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 ); } @@ -59,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 475ac8e3..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 { @@ -60,7 +62,29 @@ public enum ErrorType /// /// Value of type. /// - VersionRequestedError + VersionRequestedError, + /// + /// Value of type. + /// + + SetValueExceptionError, + /// + /// Value of type. + /// + InvalidAttributeConfigurationError, + /// + /// Value of type. + /// + MissingGroupOptionError, + /// + /// Value of type. + /// + GroupOptionAmbiguityError, + /// + /// Value of type. + /// + MultipleDefaultVerbsError + } /// @@ -200,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(); } /// @@ -279,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(); } /// @@ -471,4 +495,118 @@ internal VersionRequestedError() { } } -} \ No newline at end of file + + /// + /// Models as error generated when exception is thrown at Property.SetValue + /// + public sealed class SetValueExceptionError : NamedError + { + private readonly Exception exception; + private readonly object value; + + internal SetValueExceptionError(NameInfo nameInfo, Exception exception, object value) + : base(ErrorType.SetValueExceptionError, nameInfo) + { + this.exception = exception; + this.value = value; + } + + /// + /// The expection thrown from Property.SetValue + /// + public Exception Exception + { + get { return exception; } + } + + /// + /// The value that had to be set to the property + /// + 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) + { } + } +} 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/CSharpx/Either.cs b/src/CommandLine/Infrastructure/CSharpx/Either.cs new file mode 100644 index 00000000..3d985948 --- /dev/null +++ b/src/CommandLine/Infrastructure/CSharpx/Either.cs @@ -0,0 +1,315 @@ +//#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; + +namespace CSharpx +{ + #region Either Type +#if !CSX_EITHER_INTERNAL + public +#endif + enum EitherType + { + /// + /// Failed computation case. + /// + Left, + /// + /// Sccessful computation case. + /// + Right + } + +#if !CSX_EITHER_INTERNAL + public +#endif + abstract class Either + { + private readonly EitherType tag; + + protected Either(EitherType tag) + { + this.tag = tag; + } + + public EitherType Tag + { + get { return this.tag; } + } + + #region Basic Match Methods + public bool MatchLeft(out TLeft value) + { + value = Tag == EitherType.Left ? ((Left)this).Value : default(TLeft); + return Tag == EitherType.Left; + } + + public bool MatchRight(out TRight value) + { + value = Tag == EitherType.Right ? ((Right)this).Value : default(TRight); + return Tag == EitherType.Right; + } + #endregion + } + +#if !CSX_EITHER_INTERNAL + public +#endif + sealed class Left : Either + { + private readonly TLeft value; + + internal Left(TLeft value) + : base(EitherType.Left) + { + this.value = value; + } + + public TLeft Value + { + get { return value; } + } + } + +#if !CSX_EITHER_INTERNAL + public +#endif + sealed class Right : Either + { + private readonly TRight value; + + internal Right(TRight value) + : base(EitherType.Right) + { + this.value = value; + } + + public TRight Value + { + get { return value; } + } + } + #endregion + +#if !CSX_EITHER_INTERNAL + public +#endif + static class Either + { + #region Value Case Constructors + public static Either Left(TLeft value) + { + return new Left(value); + } + + public static Either Right(TRight value) + { + return new Right(value); + } + #endregion + + #region Monad + /// + /// Inject a value into the Either type, returning Right case. + /// + public static Either Return(TRight value) + { + return Either.Right(value); + } + + /// + /// Fail with a message. Not part of mathematical definition of a monad. + /// + public static Either Fail(string message) + { + throw new Exception(message); + } + + /// + /// Monadic bind. + /// + public static Either Bind(Either either, Func> func) + { + TRight right; + if (either.MatchRight(out right)) { + return func(right); + } + return Either.Left(either.GetLeft()); + } + #endregion + + #region Functor + /// + /// Transforms a Either's right value by using a specified mapping function. + /// + public static Either Map(Either either, Func func) + { + TRight right; + if (either.MatchRight(out right)) { + return Either.Right(func(right)); + } + return Either.Left(either.GetLeft()); + } + #endregion + + #region Bifunctor + /// + /// Maps both parts of a Either type. Applies the first function if Either is Left. + /// Otherwise applies the second function. + /// + public static Either Bimap(Either either, Func mapLeft, Func mapRight) + { + TRight right; + if (either.MatchRight(out right)) { + return Either.Right(mapRight(right)); + } + return Either.Left(mapLeft(either.GetLeft())); + } + #endregion + + #region Linq Operators + /// + /// Map operation compatible with Linq. + /// + public static Either Select( + this Either either, + Func selector) + { + return Either.Map(either, selector); + } + + public static Either SelectMany(this Either result, + Func> func) + { + return Either.Bind(result, func); + } + #endregion + + /// + /// Returns a Either Right or fail with an exception. + /// + public static TRight GetOrFail(Either either) + { + TRight value; + if (either.MatchRight(out value)) { + return value; + } + throw new ArgumentException(nameof(either), string.Format("The either value was Left {0}", either)); + } + + /// + /// Returns a Either Left or a defualt value. + /// + public static TLeft GetLeftOrDefault(Either either, TLeft @default) + { + TLeft value; + return either.MatchLeft(out value) ? value : @default; + } + + /// + /// Returns a Either Right or a defualt value. + /// + public static TRight GetRightOrDefault(Either either, TRight @default) + { + TRight value; + return either.MatchRight(out value) ? value : @default; + } + + /// + /// Wraps a function, encapsulates any exception thrown within to a Either. + /// + public static Either Try(Func func) + { + try { + return new Right(func()); + } + catch (Exception ex) { + return new Left(ex); + } + } + + /// + /// Attempts to cast an object. + /// Stores the cast value in 1Of2 if successful, otherwise stores the exception in 2Of2 + /// + public static Either Cast(object obj) + { + return Either.Try(() => (TRight)obj); + } + +#if !CSX_REM_MAYBE_FUNC + public static Either FromMaybe(Maybe maybe, TLeft left) + { + if (maybe.Tag == MaybeType.Just) { + return Either.Right(((Just)maybe).Value); + } + return Either.Left(left); + } +#endif + + private static TLeft GetLeft(this Either either) + { + return ((Left)either).Value; + } + } + +#if !CSX_EITHER_INTERNAL + public +#endif + static class EitherExtensions + { + #region Alternative Match Methods + public static void Match(this Either either, Action ifLeft, Action ifRight) + { + TLeft left; + if (either.MatchLeft(out left)) { + ifLeft(left); + return; + } + ifRight(((Right)either).Value); + } + #endregion + + /// + /// Equivalent to monadic operation. + /// Builds a value in case by default. + /// + public static Either ToEither(this TRight value) + { + return Either.Return(value); + } + + public static Either Bind( + this Either either, + Func> func) + { + return Either.Bind(either, func); + } + + public static Either Map( + this Either either, + Func func) + { + return Either.Map(either, func); + } + + public static Either Bimap( + this Either either, + Func mapLeft, + Func mapRight) + { + return Either.Bimap(either, mapLeft, mapRight); + } + + public static bool IsLeft(this Either either) + { + return either.Tag == EitherType.Left; + } + + public static bool IsRight(this Either either) + { + return either.Tag == EitherType.Right; + } + } +} 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/CSharpx/Maybe.cs b/src/CommandLine/Infrastructure/CSharpx/Maybe.cs new file mode 100644 index 00000000..044bb681 --- /dev/null +++ b/src/CommandLine/Infrastructure/CSharpx/Maybe.cs @@ -0,0 +1,394 @@ +//#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; +using System.Linq; + +namespace CSharpx +{ + #region Maybe Type + /// + /// Discriminator for . + /// +#if !CSX_MAYBE_INTERNAL + public +#endif + enum MaybeType + { + Just, + Nothing + } + + /// + /// The Maybe type models an optional value. A value of type Maybe a either contains a value of type a (represented as Just a), + /// or it is empty (represented as Nothing). + /// +#if !CSX_MAYBE_INTERNAL + public +#endif + abstract class Maybe + { + private readonly MaybeType tag; + + protected Maybe(MaybeType tag) + { + this.tag = tag; + } + + /// + /// Type discriminator. + /// + public MaybeType Tag { get { return tag; } } + + #region Basic Match Methods + /// + /// Matches a value returning true and value itself via output parameter. + /// + public bool MatchJust(out T value) + { + value = Tag == MaybeType.Just ? ((Just)this).Value : default(T); + return Tag == MaybeType.Just; + } + + /// + /// Matches an empty value returning true. + /// + public bool MatchNothing() + { + return Tag == MaybeType.Nothing; + } + #endregion + } + #endregion + + /// + /// Models a when in empty state. + /// +#if !CSX_MAYBE_INTERNAL + public +#endif + sealed class Nothing : Maybe + { + internal Nothing() + : base(MaybeType.Nothing) + { + } + } + + /// + /// Models a when contains a value. + /// +#if !CSX_MAYBE_INTERNAL + public +#endif + sealed class Just : Maybe + { + private readonly T value; + + internal Just(T value) + : base(MaybeType.Just) + { + this.value = value; + } + + /// + /// The wrapped value. + /// + public T Value + { + get { return value; } + } + } + + /// + /// Provides static methods for manipulating . + /// +#if !CSX_MAYBE_INTERNAL + public +#endif + static class Maybe + { + #region Value Case Constructors + /// + /// Builds the empty case of . + /// + public static Maybe Nothing() + { + return new Nothing(); + } + + /// + /// Builds the case when contains a value. + /// + public static Just Just(T value) + { + return new Just(value); + } + #endregion + + #region Monad + /// + /// Inject a value into the monadic type. + /// + public static Maybe Return(T value) + { + return Equals(value, default(T)) ? Maybe.Nothing() : Maybe.Just(value); + } + + /// + /// Sequentially compose two actions, passing any value produced by the first as an argument to the second. + /// + public static Maybe Bind(Maybe maybe, Func> func) + { + T1 value1; + return maybe.MatchJust(out value1) ? func(value1) : Maybe.Nothing(); + } + #endregion + + #region Functor + /// + /// Transforms an maybe value by using a specified mapping function. + /// + public static Maybe Map(Maybe maybe, Func func) + { + T1 value1; + return maybe.MatchJust(out value1) ? Maybe.Just(func(value1)) : Maybe.Nothing(); + } + #endregion + + /// + /// If both maybes contain a value, it merges them into a maybe with a tupled value. + /// + public static Maybe> Merge(Maybe first, Maybe second) + { + T1 value1; + T2 value2; + if (first.MatchJust(out value1) && second.MatchJust(out value2)) { + return Maybe.Just(Tuple.Create(value1, value2)); + } + return Maybe.Nothing>(); + } + +#if !CSX_REM_EITHER_FUNC + /// + /// Maps Either Right value to Maybe Just, otherwise Maybe Nothing. + /// + public static Maybe FromEither(Either either) + { + if (either.Tag == EitherType.Right) { + return Maybe.Just(((Right)either).Value); + } + return Maybe.Nothing(); + } +#endif + } + + /// + /// Provides convenience extension methods for . + /// +#if !CSX_MAYBE_INTERNAL + public +#endif + static class MaybeExtensions + { + #region Alternative Match Methods + /// + /// Provides pattern matching using delegates. + /// + public static void Match(this Maybe maybe, Action ifJust, Action ifNothing) + { + T value; + if (maybe.MatchJust(out value)) { + ifJust(value); + return; + } + ifNothing(); + } + + /// + /// Provides pattern matching using delegates over maybe with tupled wrapped value. + /// + public static void Match(this Maybe> maybe, Action ifJust, Action ifNothing) + { + T1 value1; + T2 value2; + if (maybe.MatchJust(out value1, out value2)) { + ifJust(value1, value2); + return; + } + ifNothing(); + } + + /// + /// Matches a value returning true and tupled value itself via two output parameters. + /// + public static bool MatchJust(this Maybe> maybe, out T1 value1, out T2 value2) + { + Tuple value; + if (maybe.MatchJust(out value)) { + value1 = value.Item1; + value2 = value.Item2; + return true; + } + value1 = default(T1); + value2 = default(T2); + return false; + } + #endregion + + /// + /// Equivalent to monadic operation. + /// Builds a value in case is different from its default. + /// + public static Maybe ToMaybe(this T value) + { + return Maybe.Return(value); + } + + /// + /// Invokes a function on this maybe value that itself yields a maybe. + /// + public static Maybe Bind(this Maybe maybe, Func> func) + { + return Maybe.Bind(maybe, func); + } + + /// + /// Transforms this maybe value by using a specified mapping function. + /// + public static Maybe Map(this Maybe maybe, Func func) + { + return Maybe.Map(maybe, func); + } + + #region Linq Operators + /// + /// Map operation compatible with Linq. + /// + public static Maybe Select( + this Maybe maybe, + Func selector) + { + return Maybe.Map(maybe, selector); + } + + /// + /// Bind operation compatible with Linq. + /// + public static Maybe SelectMany( + this Maybe maybe, + Func> valueSelector, + Func resultSelector) + { + return maybe + .Bind(sourceValue => + valueSelector(sourceValue) + .Map(resultValue => resultSelector(sourceValue, resultValue))); + } + #endregion + + #region Do Semantic + /// + /// 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)) { + action(value); + } + } + + /// + /// If contans a value executes an delegate over it. + /// + public static void Do(this Maybe> maybe, Action action) + { + T1 value1; + T2 value2; + if (maybe.MatchJust(out value1, out value2)) { + action(value1, value2); + } + } + #endregion + + /// + /// Returns true iffits argument is of the form . + /// + public static bool IsJust(this Maybe maybe) + { + return maybe.Tag == MaybeType.Just; + } + + /// + /// Returns true iffits argument is of the form . + /// + public static bool IsNothing(this Maybe maybe) + { + return maybe.Tag == MaybeType.Nothing; + } + + /// + /// Extracts the element out of a and returns a default value if its argument is . + /// + public static T FromJust(this Maybe maybe) + { + T value; + if (maybe.MatchJust(out value)) { + return value; + } + return default(T); + } + + /// + /// Extracts the element out of a and throws an error if its argument is . + /// + public static T FromJustOrFail(this Maybe maybe, Exception exceptionToThrow = null) + { + T value; + if (maybe.MatchJust(out value)) { + return value; + } + throw exceptionToThrow ?? new ArgumentException("Value empty."); + } + + /// + /// If contains a values returns it, otherwise returns . + /// + public static T GetValueOrDefault(this Maybe maybe, T noneValue) + { + T value; + return maybe.MatchJust(out value) ? value : noneValue; + } + + /// + /// If contains a values executes a mapping function over it, otherwise returns . + /// + public static T2 MapValueOrDefault(this Maybe maybe, Func func, T2 noneValue) + { + T1 value1; + 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)) { + return Enumerable.Empty().Concat(new[] { value }); + } + return Enumerable.Empty(); + } + } +} \ No newline at end of file diff --git a/src/CommandLine/Infrastructure/EnumerableExtensions`1.cs b/src/CommandLine/Infrastructure/EnumerableExtensions.cs similarity index 96% rename from src/CommandLine/Infrastructure/EnumerableExtensions`1.cs rename to src/CommandLine/Infrastructure/EnumerableExtensions.cs index 056fa152..bdada5de 100644 --- a/src/CommandLine/Infrastructure/EnumerableExtensions`1.cs +++ b/src/CommandLine/Infrastructure/EnumerableExtensions.cs @@ -1,68 +1,68 @@ -// 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; - } - } - } - } +// 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 new file mode 100644 index 00000000..8aee4bac --- /dev/null +++ b/src/CommandLine/Infrastructure/ErrorHandling.cs @@ -0,0 +1,664 @@ +//Use project level define(s) when referencing with Paket. +//#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; +#if ERRH_ADD_MAYBE_METHODS +using CSharpx; +#endif + +namespace RailwaySharp.ErrorHandling +{ +#if !ERRH_INTERNAL + public +#endif + enum ResultType + { + Ok, + Bad + } + + /// + /// Represents the result of a computation. + /// + /// Type that models the result of a successful computation. + /// Type that model a message related to a computation. +#if !ERRH_INTERNAL + public +#endif + abstract class Result + { + private readonly ResultType _tag; + + protected Result(ResultType tag) + { + _tag = tag; + } + + public ResultType Tag + { + get { return _tag; } + } + + public override string ToString() + { + 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()))); + case ResultType.Bad: + var bad = (Bad)this; + return string.Format( + "Error: {0}", + string.Join(Environment.NewLine, bad.Messages.Select(v => v.ToString()))); + } + } + } + + /// + /// Represents the result of a successful computation. + /// + /// Type that models the result of a successful computation. + /// Type that model a message related to a computation. +#if !ERRH_INTERNAL + public +#endif + sealed class Ok : Result + { + private readonly Tuple> _value; + + public Ok(TSuccess success, IEnumerable messages) + : base(ResultType.Ok) + { + if (messages == null) throw new ArgumentNullException(nameof(messages)); + + _value = Tuple.Create(success, messages); + } + + public TSuccess Success + { + get { return _value.Item1; } + } + + public IEnumerable Messages + { + get { return _value.Item2; } + } + } + + /// + /// Represents the result of a failed computation. + /// + /// Type that models the result of a successful computation. + /// Type that model a message related to a computation. +#if !ERRH_INTERNAL + public +#endif + sealed class Bad : Result + { + private readonly IEnumerable _messages; + + public Bad(IEnumerable messages) + : base(ResultType.Bad) + { + if (messages == null) throw new ArgumentException(nameof(messages)); + + _messages = messages; + } + + public IEnumerable Messages + { + get { return _messages; } + } + } + +#if !ERRH_INTERNAL + public +#endif + static class Result + { + /// + /// Creates a Failure result with the given messages. + /// + public static Result FailWith(IEnumerable messages) + { + if (messages == null) throw new ArgumentException(nameof(messages)); + + return new Bad(messages); + } + + /// + /// Creates a Failure result with the given message. + /// + public static Result FailWith(TMessage message) + { + if (message == null) throw new ArgumentException(nameof(message)); + + return new Bad(new[] { message }); + } + + /// + /// Creates a Success result with the given value. + /// + public static Result Succeed(TSuccess value) + { + return new Ok(value, Enumerable.Empty()); + } + + /// + /// Creates a Success result with the given value and the given message. + /// + public static Result Succeed(TSuccess value, TMessage message) + { + if (message == null) throw new ArgumentException(nameof(message)); + + return new Ok(value, new[] { message }); + } + + /// + /// Creates a Success result with the given value and the given messages. + /// + public static Result Succeed(TSuccess value, IEnumerable messages) + { + if (messages == null) throw new ArgumentException(nameof(messages)); + + return new Ok(value, messages); + } + + /// + /// Executes the given function on a given success or captures the failure. + /// + public static Result Try(Func func) + { + if (func == null) throw new ArgumentException(nameof(func)); + + try { + return new Ok( + func(), Enumerable.Empty()); + } + catch (Exception ex) { + return new Bad( + new[] { ex }); + } + } + } + +#if !ERRH_INTERNAL + public +#endif + static class Trial + { + /// + /// Wraps a value in a Success. + /// +#if ERRH_ENABLE_INLINE_METHODS + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + public static Result Ok(TSuccess value) + { + return new Ok(value, Enumerable.Empty()); + } + + /// + /// Wraps a value in a Success. + /// +#if ERRH_ENABLE_INLINE_METHODS + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + public static Result Pass(TSuccess value) + { + return new Ok(value, Enumerable.Empty()); + } + + /// + /// Wraps a value in a Success and adds a message. + /// +#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_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_ENABLE_INLINE_METHODS + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + public static bool Failed(Result result) + { + return result.Tag == ResultType.Bad; + } + + /// + /// Takes a Result and maps it with successFunc if it is a Success otherwise it maps it with failureFunc. + /// +#if ERRH_ENABLE_INLINE_METHODS + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + public static TResult Either( + Func, TResult> successFunc, + 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) { + return successFunc(ok.Success, ok.Messages); + } + var bad = (Bad)trialResult; + return failureFunc(bad.Messages); + } + + /// + /// 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_ENABLE_INLINE_METHODS + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + public static TSuccess ReturnOrFail(Result result) + { + Func, TSuccess> raiseExn = msgs => + { + throw new Exception( + string.Join( + Environment.NewLine, msgs.Select(m => m.ToString()))); + }; + + return Either((succ, _) => succ, raiseExn, result); + } + + /// + /// Appends the given messages with the messages in the given result. + /// +#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( + succ, messages.Concat(msgs)); + + Func, Result> failureFunc = + errors => new Bad(errors.Concat(messages)); + + return Either(successFunc, failureFunc, result); + } + + /// + /// If the result is a Success it executes the given function on the value. + /// Otherwise the exisiting failure is propagated. + /// +#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)); + + Func, Result> failureFunc = + messages => new Bad(messages); + + return Either(successFunc, failureFunc, result); + } + + /// + /// Flattens a nested result given the Failure types are equal. + /// +#if ERRH_ENABLE_INLINE_METHODS + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + public static Result Flatten( + Result, TMessage> result) + { + 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_ENABLE_INLINE_METHODS + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + public static Result Apply( + Result, TMessage> wrappedFunction, + Result result) + { + 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) { + return new Bad(((Bad)result).Messages); + } + 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_ENABLE_INLINE_METHODS + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + public static Result Lift( + Func func, + Result result) + { + return Apply(Ok, TMessage>(func), result); + } + + /// + /// Promote a function to a monad/applicative, scanning the monadic/applicative arguments from left to right. + /// +#if ERRH_ENABLE_INLINE_METHODS + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + public static Result Lift2( + Func> func, + Result first, + Result second) + { + 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_ENABLE_INLINE_METHODS + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + public static Result, TMessage> Collect( + IEnumerable> xs) + { + return Lift(Enumerable.Reverse, + xs.Aggregate, Result, TMessage>, Result, TMessage>>( + 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), + ok1.Messages.Concat(ok2.Messages)); + } + if ((result.Tag == ResultType.Ok && next.Tag == ResultType.Bad) + || (result.Tag == ResultType.Bad && next.Tag == ResultType.Ok)) { + var m1 = result.Tag == ResultType.Ok + ? ((Ok, TMessage>)result).Messages + : ((Bad)next).Messages; + var m2 = result.Tag == ResultType.Bad + ? ((Bad, TMessage>)result).Messages + : ((Ok)next).Messages; + return new Bad, TMessage>(m1.Concat(m2)); + } + + var bad1 = (Bad, TMessage>)result; + var bad2 = (Bad)next; + return new Bad, TMessage>(bad1.Messages.Concat(bad2.Messages)); + }, x => x)); + } + } + + /// + /// Extensions methods for easier usage. + /// +#if !ERRH_INTERNAL + public +#endif + static class ResultExtensions + { + /// + /// Allows pattern matching on Results. + /// +#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) { + ifSuccess(ok.Success, ok.Messages); + return; + } + + var bad = (Bad)result; + ifFailure(bad.Messages); + } + + /// + /// Allows pattern matching on Results. + /// +#if ERRH_ENABLE_INLINE_METHODS + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + public static TResult Either(this Result result, + Func, TResult> ifSuccess, + Func, TResult> ifFailure) + { + return Trial.Either(ifSuccess, ifFailure, result); + } + + /// + /// Lifts a Func into a Result and applies it on the given result. + /// +#if ERRH_ENABLE_INLINE_METHODS + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + public static Result Map(this Result result, + Func func) + { + return Trial.Lift(func, result); + } + + /// + /// Collects a sequence of Results and accumulates their values. + /// If the sequence contains an error the error will be propagated. + /// +#if ERRH_ENABLE_INLINE_METHODS + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + public static Result, TMessage> Collect( + this IEnumerable> values) + { + return Trial.Collect(values); + } + + /// + /// Collects a sequence of Results and accumulates their values. + /// If the sequence contains an error the error will be propagated. + /// +#if ERRH_ENABLE_INLINE_METHODS + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + public static Result, TMessage> Flatten(this Result>, TMessage> result) + { + if (result.Tag == ResultType.Ok) { + var ok = (Ok>, TMessage>)result; + var values = ok.Success; + var result1 = Collect(values); + if (result1.Tag == ResultType.Ok) { + var ok1 = (Ok, TMessage>)result1; + return new Ok, TMessage>(ok1.Success, ok1.Messages); + } + var bad1 = (Bad, TMessage>)result1; + return new Bad, TMessage>(bad1.Messages); + } + var bad = (Bad>, TMessage>)result; + return new Bad, TMessage>(bad.Messages); + } + + /// + /// If the result is a Success it executes the given Func on the value. + /// Otherwise the exisiting failure is propagated. + /// +#if ERRH_ENABLE_INLINE_METHODS + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + public static Result SelectMany(this Result result, + Func> func) + { + return Trial.Bind(func, result); + } + + /// + /// If the result is a Success it executes the given Func on the value. + /// If the result of the Func is a Success it maps it using the given Func. + /// Otherwise the exisiting failure is propagated. + /// +#if ERRH_ENABLE_INLINE_METHODS + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + public static Result SelectMany( + this Result result, + Func> 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, + Result, + Result + > liftedMapper = (a, b) => Trial.Lift2(curriedMapper, a, b); + var v = Trial.Bind(func, result); + return liftedMapper(result, v); + } + + /// + /// Lifts a Func into a Result and applies it on the given result. + /// +#if ERRH_ENABLE_INLINE_METHODS + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + public static Result Select(this Result result, + Func func) + { + return Trial.Lift(func, result); + } + + /// + /// Returns the error messages or fails if the result was a success. + /// +#if ERRH_ENABLE_INLINE_METHODS + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + public static IEnumerable FailedWith(this Result result) + { + if (result.Tag == ResultType.Ok) { + var ok = (Ok)result; + throw new Exception( + string.Format("Result was a success: {0} - {1}", + ok.Success, + string.Join(Environment.NewLine, ok.Messages.Select(m => m.ToString())))); + } + var bad = (Bad)result; + return bad.Messages; + } + + /// + /// Returns the result or fails if the result was an error. + /// +#if ERRH_ENABLE_INLINE_METHODS + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + public static TSuccess SucceededWith(this Result result) + { + if (result.Tag == ResultType.Ok) { + var ok = (Ok)result; + return ok.Success; + } + var bad = (Bad)result; + throw new Exception( + string.Format("Result was an error: {0}", + string.Join(Environment.NewLine, bad.Messages.Select(m => 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/ExceptionExtensions.cs b/src/CommandLine/Infrastructure/ExceptionExtensions.cs index 270b260b..d7284e63 100644 --- a/src/CommandLine/Infrastructure/ExceptionExtensions.cs +++ b/src/CommandLine/Infrastructure/ExceptionExtensions.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text; namespace CommandLine.Infrastructure { diff --git a/src/CommandLine/Infrastructure/FSharpOptionHelper.cs b/src/CommandLine/Infrastructure/FSharpOptionHelper.cs index 783cfa83..3cfd3882 100644 --- a/src/CommandLine/Infrastructure/FSharpOptionHelper.cs +++ b/src/CommandLine/Infrastructure/FSharpOptionHelper.cs @@ -1,8 +1,6 @@ -#if !SKIP_FSHARP +// Copyright 2005-2015 Giacomo Stelluti Scala & Contributors. All rights reserved. See License.md in the project root for license information. + using System; -#if PLATFORM_DOTNET -using System.Reflection; -#endif using CommandLine.Core; using Microsoft.FSharp.Core; @@ -13,9 +11,6 @@ static class FSharpOptionHelper public static Type GetUnderlyingType(Type type) { return type -#if NETSTANDARD1_5 - .GetTypeInfo() -#endif .GetGenericArguments()[0]; } @@ -51,5 +46,4 @@ public static bool IsSome(object value) "get_IsSome", value); } } -} -#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/ReferenceEqualityComparer.cs b/src/CommandLine/Infrastructure/ReferenceEqualityComparer.cs index 2f6f4c2c..1db71787 100644 --- a/src/CommandLine/Infrastructure/ReferenceEqualityComparer.cs +++ b/src/CommandLine/Infrastructure/ReferenceEqualityComparer.cs @@ -1,6 +1,5 @@ // 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.Runtime.CompilerServices; diff --git a/src/CommandLine/Infrastructure/ReflectionHelper.cs b/src/CommandLine/Infrastructure/ReflectionHelper.cs index c054f83c..47fe70ea 100644 --- a/src/CommandLine/Infrastructure/ReflectionHelper.cs +++ b/src/CommandLine/Infrastructure/ReflectionHelper.cs @@ -45,10 +45,10 @@ 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(); @@ -85,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); @@ -101,13 +103,19 @@ public static object CreateDefaultImmutableInstance(Type type, Type[] constructo private static Assembly GetExecutingOrEntryAssembly() { - var assembly = Assembly.GetEntryAssembly(); - -#if !NETSTANDARD1_5 - assembly = assembly ?? Assembly.GetExecutingAssembly(); -#endif + //resolve issues of null EntryAssembly in Xunit Test #392,424,389 + //return Assembly.GetEntryAssembly(); + return Assembly.GetEntryAssembly() ?? Assembly.GetCallingAssembly(); + } - return assembly; + 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 8ac38ec4..00000000 --- a/src/CommandLine/Infrastructure/ResultExtensions.cs +++ /dev/null @@ -1,36 +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 System.Text; - -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 a1c5cbdf..4301aa52 100644 --- a/src/CommandLine/Parser.cs +++ b/src/CommandLine/Parser.cs @@ -99,6 +99,9 @@ public ParserResult ParseArguments(IEnumerable args) settings.NameComparer, settings.CaseInsensitiveEnumValues, settings.ParsingCulture, + settings.AutoHelp, + settings.AutoVersion, + settings.AllowMultiInstance, HandleUnknownArguments(settings.IgnoreUnknownArguments)), settings); } @@ -114,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"); @@ -128,6 +130,9 @@ public ParserResult ParseArguments(Func factory, IEnumerable ar settings.NameComparer, settings.CaseInsensitiveEnumValues, settings.ParsingCulture, + settings.AutoHelp, + settings.AutoVersion, + settings.AllowMultiInstance, HandleUnknownArguments(settings.IgnoreUnknownArguments)), settings); } @@ -156,7 +161,11 @@ public ParserResult ParseArguments(IEnumerable args, params Type types, args, settings.NameComparer, + settings.CaseInsensitiveEnumValues, settings.ParsingCulture, + settings.AutoHelp, + settings.AutoVersion, + settings.AllowMultiInstance, HandleUnknownArguments(settings.IgnoreUnknownArguments)), settings); } @@ -176,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); @@ -222,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 5ec4c073..c7c9c833 100644 --- a/src/CommandLine/ParserResult.cs +++ b/src/CommandLine/ParserResult.cs @@ -6,10 +6,10 @@ namespace CommandLine { - sealed class TypeInfo + public sealed class TypeInfo { private readonly Type current; - private readonly IEnumerable choices; + private readonly IEnumerable choices; private TypeInfo(Type current, IEnumerable choices) { @@ -27,12 +27,12 @@ public IEnumerable Choices get { return this.choices; } } - public static TypeInfo Create(Type current) + internal static TypeInfo Create(Type current) { return new TypeInfo(current, Enumerable.Empty()); } - public static TypeInfo Create(Type current, IEnumerable choices) + internal static TypeInfo Create(Type current, IEnumerable choices) { return new TypeInfo(current, 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]; } /// @@ -78,10 +88,20 @@ public ParserResultType Tag get { return this.tag; } } - internal TypeInfo TypeInfo + 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 @@ internal 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); } @@ -216,4 +215,4 @@ public bool Equals(NotParsed other) && Errors.SequenceEqual(other.Errors); } } -} \ No newline at end of file +} 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 93962566..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 { @@ -20,9 +21,14 @@ public class ParserSettings : IDisposable private bool caseInsensitiveEnumValues; private TextWriter helpWriter; private bool ignoreUnknownArguments; + 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. @@ -31,15 +37,36 @@ public ParserSettings() { caseSensitive = true; caseInsensitiveEnumValues = false; + 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; - } - catch (IOException) + width = Console.WindowWidth; + if (width < 1) + { + width = DefaultMaximumLength; + } + } + catch (Exception e) when (e is IOException || e is PlatformNotSupportedException || e is ArgumentOutOfRangeException) { - maximumDisplayWidth = DefaultMaximumLength; + width = DefaultMaximumLength; } + return width; } /// @@ -118,14 +145,33 @@ public bool IgnoreUnknownArguments set { PopsicleSetter.Set(Consumed, ref ignoreUnknownArguments, value); } } + /// + /// Gets or sets a value indicating whether implicit option or verb 'help' should be supported. + /// + public bool AutoHelp + { + get { return autoHelp; } + set { PopsicleSetter.Set(Consumed, ref autoHelp, value); } + } + + /// + /// Gets or sets a value indicating whether implicit option or verb 'version' should be supported. + /// + public bool AutoVersion + { + get { return autoVersion; } + set { PopsicleSetter.Set(Consumed, ref autoVersion, value); } + } + /// /// 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)); } /// @@ -137,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 5f156a44..1dc94d20 100644 --- a/src/CommandLine/Properties/AssemblyInfo.cs +++ b/src/CommandLine/Properties/AssemblyInfo.cs @@ -1,37 +1,5 @@ // Copyright 2005-2015 Giacomo Stelluti Scala & Contributors. All rights reserved. See License.md in the project root for license information. -using System; -using System.Reflection; using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -[assembly: AssemblyTitle("CommandLine.dll")] -[assembly: AssemblyDescription("Command Line Parser Library allows CLR applications to define a syntax for parsing command line arguments.")] -[assembly: AssemblyCulture("")] -[assembly: InternalsVisibleTo("CommandLine.Tests, PublicKey=" + - "002400000480000094000000060200000024000052534131000400000100010015eb7571d696c0" + - "75627830f9468969103bc35764467bdbccfc0850f2fbe6913ee233d5d7cf3bbcb870fd42e6a8cc" + - "846d706b5cef35389e5b90051991ee8b6ed73ee1e19f108e409be69af6219b2e31862405f4b8ba" + - "101662fbbb54ba92a35d97664fe65c90c2bebd07aef530b01b709be5ed01b7e4d67a6b01c8643e" + - "42a20fb4")] -#if PLATFORM_DOTNET -[assembly: InternalsVisibleTo("CommandLine.DotNet.Tests, PublicKey=" + - "002400000480000094000000060200000024000052534131000400000100010015eb7571d696c0" + - "75627830f9468969103bc35764467bdbccfc0850f2fbe6913ee233d5d7cf3bbcb870fd42e6a8cc" + - "846d706b5cef35389e5b90051991ee8b6ed73ee1e19f108e409be69af6219b2e31862405f4b8ba" + - "101662fbbb54ba92a35d97664fe65c90c2bebd07aef530b01b709be5ed01b7e4d67a6b01c8643e" + - "42a20fb4")] -#endif -[assembly: InternalsVisibleTo("CommandLine.FSharp, PublicKey=" + - "002400000480000094000000060200000024000052534131000400000100010015eb7571d696c0" + - "75627830f9468969103bc35764467bdbccfc0850f2fbe6913ee233d5d7cf3bbcb870fd42e6a8cc" + - "846d706b5cef35389e5b90051991ee8b6ed73ee1e19f108e409be69af6219b2e31862405f4b8ba" + - "101662fbbb54ba92a35d97664fe65c90c2bebd07aef530b01b709be5ed01b7e4d67a6b01c8643e" + - "42a20fb4")] -#if DEBUG -[assembly: AssemblyConfiguration("Debug")] -#else -[assembly: AssemblyConfiguration("Release")] -#endif -[assembly: ComVisible(false)] -[assembly: CLSCompliant(true)] \ No newline at end of file +[assembly: InternalsVisibleTo("CommandLine.Tests, PublicKey=002400000480000094000000060200000024000052534131000400000100010009ab24ef889cd26bf46f7eaeda28e0fa5c04c50c93c6e121337b154bca0a1fd58ac6cb86195b709c2120f482730ced04a0e167a5758e56d3464bfabafe022b31510c39a61968fde795480dd60f6a396015c5f69a942074a3f4654b6dd66d0c63608bea78bdf96b35b1b48bb75741c2caad1f70579f286f1dbc2c560511c648d2")] diff --git a/src/CommandLine/Text/CopyrightInfo.cs b/src/CommandLine/Text/CopyrightInfo.cs index 3df9e630..c8bc3593 100644 --- a/src/CommandLine/Text/CopyrightInfo.cs +++ b/src/CommandLine/Text/CopyrightInfo.cs @@ -1,7 +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.Dynamic; using System.Globalization; using System.Reflection; using System.Text; @@ -28,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); } } @@ -116,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); + } } } @@ -193,4 +193,4 @@ protected virtual string FormatYears(int[] years) return yearsPart.ToString(); } } -} \ No newline at end of file +} diff --git a/src/CommandLine/Text/HeadingInfo.cs b/src/CommandLine/Text/HeadingInfo.cs index 286a1872..41e264f7 100644 --- a/src/CommandLine/Text/HeadingInfo.cs +++ b/src/CommandLine/Text/HeadingInfo.cs @@ -57,8 +57,9 @@ public static HeadingInfo Default { var title = ReflectionHelper.GetAttribute() .MapValueOrDefault( - titleAttribute => Path.GetFileNameWithoutExtension(titleAttribute.Title), + titleAttribute => titleAttribute.Title, ReflectionHelper.GetAssemblyName()); + var version = ReflectionHelper.GetAttribute() .MapValueOrDefault( versionAttribute => versionAttribute.InformationalVersion, diff --git a/src/CommandLine/Text/HelpText.cs b/src/CommandLine/Text/HelpText.cs index eaa6ade5..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; @@ -31,6 +109,9 @@ public class HelpText private StringBuilder optionsHelp; private bool addDashesToOption; private bool addEnumValuesToHelpText; + private bool autoHelp; + private bool autoVersion; + private bool addNewLineBetweenHelpSections; /// /// Initializes a new instance of the class. @@ -105,6 +186,10 @@ public HelpText(SentenceBuilder sentenceBuilder, string heading, string copyrigh try { maximumDisplayWidth = Console.WindowWidth; + if (maximumDisplayWidth < 1) + { + maximumDisplayWidth = DefaultMaximumLength; + } } catch (IOException) { @@ -113,6 +198,8 @@ public HelpText(SentenceBuilder sentenceBuilder, string heading, string copyrigh this.sentenceBuilder = sentenceBuilder; this.heading = heading; this.copyright = copyright; + this.autoHelp = true; + this.autoVersion = true; } /// @@ -174,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. /// @@ -183,6 +279,24 @@ public bool AddEnumValuesToHelpText set { addEnumValuesToHelpText = value; } } + /// + /// Gets or sets a value indicating whether implicit option or verb 'help' should be supported. + /// + public bool AutoHelp + { + get { return autoHelp; } + set { autoHelp = value; } + } + + /// + /// Gets or sets a value indicating whether implicit option or verb 'version' should be supported. + /// + public bool AutoVersion + { + get { return autoVersion; } + set { autoVersion = value; } + } + /// /// Gets the instance specified in constructor. /// @@ -231,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); } @@ -249,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( @@ -271,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. @@ -282,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"); @@ -289,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); } /// @@ -313,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, @@ -417,6 +563,7 @@ public HelpText AddOptions(ParserResult result) return AddOptionsImpl( GetSpecificationsFromType(result.TypeInfo.Current), SentenceBuilder.RequiredWord(), + SentenceBuilder.OptionGroupWord(), MaximumDisplayWidth); } @@ -434,6 +581,7 @@ public HelpText AddVerbs(params Type[] types) return AddOptionsImpl( AdaptVerbsToSpecifications(types), SentenceBuilder.RequiredWord(), + SentenceBuilder.OptionGroupWord(), MaximumDisplayWidth); } @@ -450,6 +598,7 @@ public HelpText AddOptions(int maximumLength, ParserResult result) return AddOptionsImpl( GetSpecificationsFromType(result.TypeInfo.Current), SentenceBuilder.RequiredWord(), + SentenceBuilder.OptionGroupWord(), maximumLength); } @@ -468,6 +617,7 @@ public HelpText AddVerbs(int maximumLength, params Type[] types) return AddOptionsImpl( AdaptVerbsToSpecifications(types), SentenceBuilder.RequiredWord(), + SentenceBuilder.OptionGroupWord(), maximumLength); } @@ -511,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()) @@ -582,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, @@ -591,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(); } @@ -604,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) @@ -636,48 +808,21 @@ internal static void AddLine(StringBuilder builder, string value, int maximumLen throw new ArgumentOutOfRangeException(nameof(value)); } - value = value.Trim(); + 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) { var specs = type.GetSpecifications(Specification.FromProperty); var optionSpecs = specs - .OfType() - .Concat(new[] { MakeHelpEntry(), MakeVersionEntry() }); + .OfType(); + if (autoHelp) + optionSpecs = optionSpecs.Concat(new[] { MakeHelpEntry() }); + if (autoVersion) + optionSpecs = optionSpecs.Concat(new[] { MakeVersionEntry() }); var valueSpecs = specs .OfType() .OrderBy(v => v.Index); @@ -694,12 +839,8 @@ private static Maybe>> GetUsageFromTy var prop = tuple.Item1; var attr = tuple.Item2; -#if NETSTANDARD1_5 - var examples = (IEnumerable)prop.GetValue(null); -#else var examples = (IEnumerable)prop .GetValue(null, BindingFlags.Public | BindingFlags.Static | BindingFlags.GetProperty, null, null, null); -#endif return Tuple.Create(attr, examples); }); @@ -707,31 +848,60 @@ private static Maybe>> GetUsageFromTy private IEnumerable AdaptVerbsToSpecifications(IEnumerable types) { - return (from verbTuple in Verb.SelectFromTypes(types) - select - OptionSpecification.NewSwitch( - string.Empty, - verbTuple.Item1.Name, - false, - verbTuple.Item1.HelpText, - string.Empty, - verbTuple.Item1.Hidden)).Concat(new[] { MakeHelpEntry(), MakeVersionEntry() }); + var optionSpecs = from verbTuple in Verb.SelectFromTypes(types) + 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() }); + if (autoVersion) + 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); + + 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)); - specifications.ForEach( - option => - AddOption(requiredWord, maxLength, option, remainingSpace)); + } return this; } @@ -765,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; @@ -779,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; @@ -789,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); @@ -914,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; } @@ -967,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 1c28e959..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: @@ -137,6 +148,22 @@ public override Func FormatError case ErrorType.RepeatedOptionError: return "Option '".JoinTo(((RepeatedOptionError)error).NameInfo.NameText, "' is defined multiple times."); + 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(); }; @@ -150,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 => @@ -166,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 20122a7b..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; @@ -18,6 +19,8 @@ public class UnParserSettings private bool preferShortName; 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. @@ -46,6 +49,22 @@ public bool UseEqualToken set { PopsicleSetter.Set(Consumed, ref useEqualToken, value); } } + /// + /// Gets or sets a value indicating whether unparsing process shall expose hidden options. + /// + public bool ShowHidden + { + get { return 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. /// @@ -81,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(); } /// @@ -110,37 +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)) - 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 => @@ -157,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(); @@ -181,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) @@ -199,23 +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) { - var longName = - optionSpec.LongName.Length > 0 - && !settings.PreferShortName; + // 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) + || 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) @@ -231,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 @@ -242,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/ValueAttribute.cs b/src/CommandLine/ValueAttribute.cs index db1ea405..ba2c0ef9 100644 --- a/src/CommandLine/ValueAttribute.cs +++ b/src/CommandLine/ValueAttribute.cs @@ -38,9 +38,7 @@ public string MetaName get { return metaName; } set { - if (value == null) throw new ArgumentNullException("value"); - - metaName = value; + metaName = value ?? throw new ArgumentNullException("value"); } } } diff --git a/src/CommandLine/VerbAttribute.cs b/src/CommandLine/VerbAttribute.cs index af66ac12..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,16 +52,26 @@ public bool Hidden /// public string HelpText { - get { return helpText; } - set - { - if (value == null) - { - throw new ArgumentNullException("value"); - } - - helpText = 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/src/CommandLine/paket.references b/src/CommandLine/paket.references deleted file mode 100644 index 21448920..00000000 --- a/src/CommandLine/paket.references +++ /dev/null @@ -1,4 +0,0 @@ -FSharp.Core -File:Maybe.cs Infrastructure -File:EnumerableExtensions.cs Infrastructure -File:ErrorHandling.cs Infrastructure diff --git a/src/CommandLine/project.json b/src/CommandLine/project.json deleted file mode 100644 index 9a68f725..00000000 --- a/src/CommandLine/project.json +++ /dev/null @@ -1,61 +0,0 @@ -{ - "version": "2.0.275-*", - "authors": [ - "Giacomo Stelluti Scala" - ], - "title": "Command Line Parser Library", - "description": "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.", - "packOptions": { - "licenseUrl": "https://raw.githubusercontent.com/gsscoder/commandline/master/doc/LICENSE", - "owners": [ - "gimmemoore", - "gsscoder" - ], - "repository": { - "type": "git", - "url": "https://github.com/gsscoder/commandline" - } - }, - "buildOptions": { - "keyFile": "../../CommandLine.snk", - "strongName": true, - "define": [ - "CSX_REM_EITHER_FUNC", "SKIP_FSHARP" - ], - "xmlDoc": true, - "compile": { - "include": [ - "../CommandLine/**/*.cs", - "../../paket-files/**/*.cs" - ], - "exclude": [ - "../CommandLine/obj/**" - ] - } - }, - "frameworks": { - "netstandard1.5": { - "dependencies": { - "System.Runtime.Extensions": "4.1.0", - "System.Globalization": "4.0.11", - "System.Linq.Expressions": "4.1.0", - "System.Linq": "4.1.0", - "System.Linq.Dynamic.Core": "1.0.6.8", - "System.Diagnostics.Debug": "4.0.11", - "System.Console": "4.0.0", - "System.Reflection.Extensions": "4.0.1", - "System.Text.RegularExpressions": "4.1.0", - }, - "imports": [ "netcore45" ] - }, - "net40": { - "frameworkAssemblies": {}, - "buildOptions": { - "define": [ "ERRH_DISABLE_INLINE_METHODS", "NET40" ] - } - }, - "net45": { - "frameworkAssemblies": { } - } - } -} diff --git a/src/SharedAssemblyInfo.cs b/src/SharedAssemblyInfo.cs deleted file mode 100644 index 078b4537..00000000 --- a/src/SharedAssemblyInfo.cs +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright 2005-2018 Giacomo Stelluti Scala & Contributors. All rights reserved. See License.md in the project root for license information. - -using System.Reflection; -using System.Resources; - -[assembly: AssemblyProduct("Command Line Parser Library")] -[assembly: AssemblyCopyright("Copyright (c) 2005 - 2018 Giacomo Stelluti Scala & Contributors")] -[assembly: NeutralResourcesLanguage("en-US")] - -// versions are replaced during CI build -[assembly: AssemblyVersion("2.2.0.0")] -[assembly: AssemblyFileVersion("2.2.0.0")] -[assembly: AssemblyInformationalVersion("2.2.0.0")] diff --git a/tests/CommandLine.Tests.Properties/CommandLine.Tests.Properties.csproj b/tests/CommandLine.Tests.Properties/CommandLine.Tests.Properties.csproj deleted file mode 100644 index cb2b6b96..00000000 --- a/tests/CommandLine.Tests.Properties/CommandLine.Tests.Properties.csproj +++ /dev/null @@ -1,395 +0,0 @@ - - - - - Debug - AnyCPU - {A917F20D-FC6D-4EB1-BDF0-F6C71300C5BD} - Library - Properties - CommandLine.Tests.Properties - CommandLine.Tests.Properties - v4.5 - 512 - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - - - - - - - - - - - Properties\SharedAssemblyInfo.cs - - - - - - - - {e1bd3c65-49c3-49e7-baba-c60980cb3f20} - CommandLine - - - - - - - - - - <__paket__xunit_runner_visualstudio_props>net20\xunit.runner.visualstudio - - - - - <__paket__xunit_runner_visualstudio_props>portable-net45+win8+wp8+wpa81\xunit.runner.visualstudio - - - - - <__paket__xunit_runner_visualstudio_props>uap10.0\xunit.runner.visualstudio - <__paket__xunit_runner_visualstudio_targets>uap10.0\xunit.runner.visualstudio - - - - - <__paket__xunit_runner_visualstudio_props>win81\xunit.runner.visualstudio - <__paket__xunit_runner_visualstudio_targets>win81\xunit.runner.visualstudio - - - - - <__paket__xunit_runner_visualstudio_props>wpa81\xunit.runner.visualstudio - <__paket__xunit_runner_visualstudio_targets>wpa81\xunit.runner.visualstudio - - - - - - - - <__paket__xunit_core_props>Xamarin.iOS\xunit.core - - - - - <__paket__xunit_core_props>monoandroid\xunit.core - - - - - <__paket__xunit_core_props>monotouch\xunit.core - - - - - <__paket__xunit_core_props>portable-net45+win+wpa81+wp80+monotouch+monoandroid+Xamarin.iOS\xunit.core - - - - - <__paket__xunit_core_props>portable-win81+wpa81\xunit.core - <__paket__xunit_core_targets>portable-win81+wpa81\xunit.core - - - - - <__paket__xunit_core_props>win8\xunit.core - <__paket__xunit_core_targets>win8\xunit.core - - - - - <__paket__xunit_core_props>wp8\xunit.core - <__paket__xunit_core_targets>wp8\xunit.core - - - - - - - - - - ..\..\packages\FluentAssertions\lib\monoandroid\FluentAssertions.Core.dll - True - True - - - - - - - ..\..\packages\FluentAssertions\lib\monotouch\FluentAssertions.Core.dll - True - True - - - - - - - ..\..\packages\FluentAssertions\lib\net40\FluentAssertions.dll - True - True - - - ..\..\packages\FluentAssertions\lib\net40\FluentAssertions.Core.dll - True - True - - - - - - - ..\..\packages\FluentAssertions\lib\net45\FluentAssertions.dll - True - True - - - ..\..\packages\FluentAssertions\lib\net45\FluentAssertions.Core.dll - True - True - - - - - - - ..\..\packages\FluentAssertions\lib\portable-net40+sl5+win8+wp8+wpa81\FluentAssertions.dll - True - True - - - ..\..\packages\FluentAssertions\lib\portable-net40+sl5+win8+wp8+wpa81\FluentAssertions.Core.dll - True - True - - - - - - - ..\..\packages\FluentAssertions\lib\portable-win81+wpa81\FluentAssertions.dll - True - True - - - ..\..\packages\FluentAssertions\lib\portable-win81+wpa81\FluentAssertions.Core.dll - True - True - - - - - - - ..\..\packages\FluentAssertions\lib\sl5\FluentAssertions.dll - True - True - - - ..\..\packages\FluentAssertions\lib\sl5\FluentAssertions.Core.dll - True - True - - - ..\..\packages\FluentAssertions\lib\sl5\Microsoft.VisualStudio.QualityTools.UnitTesting.Silverlight.dll - True - True - - - - - - - ..\..\packages\FluentAssertions\lib\wp8\FluentAssertions.dll - True - True - - - ..\..\packages\FluentAssertions\lib\wp8\FluentAssertions.Core.dll - True - True - - - - - - - ..\..\packages\FluentAssertions\lib\xamarin.ios\FluentAssertions.Core.dll - True - True - - - - - - - - - ..\..\packages\FsCheck\lib\net45\FsCheck.dll - True - True - - - - - - - ..\..\packages\FsCheck\lib\portable-net45+netcore45\FsCheck.dll - True - True - - - - - - - ..\..\packages\FsCheck\lib\portable-net45+netcore45+wp8\FsCheck.dll - True - True - - - - - - - ..\..\packages\FsCheck\lib\portable-net45+netcore45+wpa81+wp8\FsCheck.dll - True - True - - - - - - - - - ..\..\packages\FSharp.Core\lib\net20\FSharp.Core.dll - True - True - - - - - - - ..\..\packages\FSharp.Core\lib\net40\FSharp.Core.dll - True - True - - - - - - - ..\..\packages\FSharp.Core\lib\portable-net45+netcore45\FSharp.Core.dll - True - True - - - - - - - ..\..\packages\FSharp.Core\lib\portable-net45+netcore45+wp8\FSharp.Core.dll - True - True - - - - - - - ..\..\packages\FSharp.Core\lib\portable-net45+netcore45+wpa81+wp8\FSharp.Core.dll - True - True - - - - - - - ..\..\packages\FSharp.Core\lib\portable-net45+sl5+netcore45\FSharp.Core.dll - True - True - - - - - - - - - ..\..\packages\xunit.abstractions\lib\net35\xunit.abstractions.dll - True - True - - - - - - - ..\..\packages\xunit.abstractions\lib\portable-net45+win+wpa81+wp80+monotouch+monoandroid+Xamarin.iOS\xunit.abstractions.dll - True - True - - - - - - - - - ..\..\packages\xunit.assert\lib\portable-net45+win+wpa81+wp80+monotouch+monoandroid+Xamarin.iOS\xunit.assert.dll - True - True - - - - - - - - - ..\..\packages\xunit.extensibility.core\lib\portable-net45+win+wpa81+wp80+monotouch+monoandroid+Xamarin.iOS\xunit.core.dll - True - True - - - - - - - \ No newline at end of file diff --git a/tests/CommandLine.Tests.Properties/Fakes/Scalar_String_Mutable.cs b/tests/CommandLine.Tests.Properties/Fakes/Scalar_String_Mutable.cs deleted file mode 100644 index 5b45b5d2..00000000 --- a/tests/CommandLine.Tests.Properties/Fakes/Scalar_String_Mutable.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace CommandLine.Tests.Properties.Fakes -{ - class Scalar_String_Mutable - { - [Option] - public string StringValue { get; set; } - } -} diff --git a/tests/CommandLine.Tests.Properties/ParserProperties.cs b/tests/CommandLine.Tests.Properties/ParserProperties.cs deleted file mode 100644 index d6a957ec..00000000 --- a/tests/CommandLine.Tests.Properties/ParserProperties.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -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.ShouldBeEquivalentTo(value); - }).QuickCheckThrowOnFailure(); - } - } -} diff --git a/tests/CommandLine.Tests.Properties/Properties/AssemblyInfo.cs b/tests/CommandLine.Tests.Properties/Properties/AssemblyInfo.cs deleted file mode 100644 index c934a297..00000000 --- a/tests/CommandLine.Tests.Properties/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright 2005-2015 Giacomo Stelluti Scala & Contributors. All rights reserved. See License.md in the project root for license information. - -using System.Reflection; -using System.Runtime.InteropServices; -using CommandLine.Text; - -[assembly: AssemblyTitle("CommandLine.Tests.Properties.dll")] -[assembly: AssemblyDescription("Command Line Parser Library allows CLR applications to define a syntax for parsing command line arguments.")] -[assembly: AssemblyCulture("")] - -[assembly: AssemblyLicense( - "This is free software. You may redistribute copies of it under the terms of", - "the MIT License .")] -[assembly: AssemblyUsage( - "[no usage, this is a dll]")] - -#if DEBUG -[assembly: AssemblyConfiguration("Debug")] -#else -//[assembly: AssemblyConfiguration("Release")] -#endif -[assembly: ComVisible(false)] -//[assembly: CLSCompliant(true)] -//[assembly: AssemblyCompany("")] -//[assembly: AssemblyTrademark("")] \ No newline at end of file diff --git a/tests/CommandLine.Tests.Properties/packages.config b/tests/CommandLine.Tests.Properties/packages.config deleted file mode 100644 index 27432dad..00000000 --- a/tests/CommandLine.Tests.Properties/packages.config +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/tests/CommandLine.Tests.Properties/paket.references b/tests/CommandLine.Tests.Properties/paket.references deleted file mode 100644 index ffc4ed3c..00000000 --- a/tests/CommandLine.Tests.Properties/paket.references +++ /dev/null @@ -1,5 +0,0 @@ -FSharp.Core -FsCheck -FluentAssertions -xunit -xunit.runner.visualstudio diff --git a/tests/CommandLine.Tests/CommandLine.Tests.csproj b/tests/CommandLine.Tests/CommandLine.Tests.csproj index 5914a3a9..d4dbcab0 100644 --- a/tests/CommandLine.Tests/CommandLine.Tests.csproj +++ b/tests/CommandLine.Tests/CommandLine.Tests.csproj @@ -1,429 +1,38 @@ - - - - + + - Debug - AnyCPU - {0A15C4D2-B3E9-43AB-8155-1B39F7AC8A5E} Library - Properties - CommandLine.Tests - CommandLine.Tests - v4.5 - 512 - 12.0.0 - 2.0 - ..\..\ - true - - f62f3018 - - - true - full - false - bin\Debug\ - TRACE;DEBUG;SKIP_FSHARP - prompt - 4 - false - - - pdbonly - true - bin\Release\ - TRACE;SKIP_FSHARP - prompt - 4 - false - - - true - - + 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 - - - - - - - - - - - - Properties\SharedAssemblyInfo.cs - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Code - - - - - - - - - - - - - - Designer - + + + + + $(DefineConstants);PLATFORM_DOTNET + - - {E1BD3C65-49C3-49E7-BABA-C60980CB3F20} - CommandLine - + + - + + + + + + + - - - - - <__paket__xunit_runner_visualstudio_props>net20\xunit.runner.visualstudio - - - - - <__paket__xunit_runner_visualstudio_props>portable-net45+win8+wp8+wpa81\xunit.runner.visualstudio - - - - - <__paket__xunit_runner_visualstudio_props>uap10.0\xunit.runner.visualstudio - <__paket__xunit_runner_visualstudio_targets>uap10.0\xunit.runner.visualstudio - - - - - <__paket__xunit_runner_visualstudio_props>win81\xunit.runner.visualstudio - <__paket__xunit_runner_visualstudio_targets>win81\xunit.runner.visualstudio - - - - - <__paket__xunit_runner_visualstudio_props>wpa81\xunit.runner.visualstudio - <__paket__xunit_runner_visualstudio_targets>wpa81\xunit.runner.visualstudio - - - - - - - - <__paket__xunit_core_props>Xamarin.iOS\xunit.core - - - - - <__paket__xunit_core_props>monoandroid\xunit.core - - - - - <__paket__xunit_core_props>monotouch\xunit.core - - - - - <__paket__xunit_core_props>portable-net45+win+wpa81+wp80+monotouch+monoandroid+Xamarin.iOS\xunit.core - - - - - <__paket__xunit_core_props>portable-win81+wpa81\xunit.core - <__paket__xunit_core_targets>portable-win81+wpa81\xunit.core - - - - - <__paket__xunit_core_props>win8\xunit.core - <__paket__xunit_core_targets>win8\xunit.core - - - - - <__paket__xunit_core_props>wp8\xunit.core - <__paket__xunit_core_targets>wp8\xunit.core - - - - - - - - - - ..\..\packages\FluentAssertions\lib\monoandroid\FluentAssertions.Core.dll - True - True - - - - - - - ..\..\packages\FluentAssertions\lib\monotouch\FluentAssertions.Core.dll - True - True - - - - - - - ..\..\packages\FluentAssertions\lib\net40\FluentAssertions.dll - True - True - - - ..\..\packages\FluentAssertions\lib\net40\FluentAssertions.Core.dll - True - True - - - - - - - ..\..\packages\FluentAssertions\lib\net45\FluentAssertions.dll - True - True - - - ..\..\packages\FluentAssertions\lib\net45\FluentAssertions.Core.dll - True - True - - - - - - - ..\..\packages\FluentAssertions\lib\portable-net40+sl5+win8+wp8+wpa81\FluentAssertions.dll - True - True - - - ..\..\packages\FluentAssertions\lib\portable-net40+sl5+win8+wp8+wpa81\FluentAssertions.Core.dll - True - True - - - - - - - ..\..\packages\FluentAssertions\lib\portable-win81+wpa81\FluentAssertions.dll - True - True - - - ..\..\packages\FluentAssertions\lib\portable-win81+wpa81\FluentAssertions.Core.dll - True - True - - - - - - - ..\..\packages\FluentAssertions\lib\sl5\FluentAssertions.dll - True - True - - - ..\..\packages\FluentAssertions\lib\sl5\FluentAssertions.Core.dll - True - True - - - ..\..\packages\FluentAssertions\lib\sl5\Microsoft.VisualStudio.QualityTools.UnitTesting.Silverlight.dll - True - True - - - - - - - ..\..\packages\FluentAssertions\lib\wp8\FluentAssertions.dll - True - True - - - ..\..\packages\FluentAssertions\lib\wp8\FluentAssertions.Core.dll - True - True - - - - - - - ..\..\packages\FluentAssertions\lib\xamarin.ios\FluentAssertions.Core.dll - True - True - - - - - - - - - ..\..\packages\FSharp.Core\lib\net20\FSharp.Core.dll - True - True - - - - - - - - ..\..\packages\FSharp.Core\lib\portable-net45+netcore45\FSharp.Core.dll - True - True - - - - - - - ..\..\packages\FSharp.Core\lib\portable-net45+netcore45+wp8\FSharp.Core.dll - True - True - - - - - - - ..\..\packages\FSharp.Core\lib\portable-net45+netcore45+wpa81+wp8\FSharp.Core.dll - True - True - - - - - - - ..\..\packages\FSharp.Core\lib\portable-net45+sl5+netcore45\FSharp.Core.dll - True - True - - - - - - - - - ..\..\packages\xunit.abstractions\lib\net35\xunit.abstractions.dll - True - True - - - - - - - ..\..\packages\xunit.abstractions\lib\portable-net45+win+wpa81+wp80+monotouch+monoandroid+Xamarin.iOS\xunit.abstractions.dll - True - True - - - - - - - - - ..\..\packages\xunit.assert\lib\portable-net45+win+wpa81+wp80+monotouch+monoandroid+Xamarin.iOS\xunit.assert.dll - True - True - - - - - - - - - ..\..\packages\xunit.extensibility.core\lib\portable-net45+win+wpa81+wp80+monotouch+monoandroid+Xamarin.iOS\xunit.core.dll - True - True - - - - - - + + \ No newline at end of file diff --git a/tests/CommandLine.Tests/CultureInfoExtensions.cs b/tests/CommandLine.Tests/CultureInfoExtensions.cs index f3d6a01a..c49357ed 100644 --- a/tests/CommandLine.Tests/CultureInfoExtensions.cs +++ b/tests/CommandLine.Tests/CultureInfoExtensions.cs @@ -6,7 +6,6 @@ namespace CommandLine.Tests { -#if !PLATFORM_DOTNET struct CultureHandlers { public Action ChangeCulture; @@ -26,5 +25,4 @@ public static CultureHandlers MakeCultureHandlers(this CultureInfo newCulture) return new CultureHandlers { ChangeCulture = changer, ResetCulture = resetter }; } } -#endif } 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 new file mode 100644 index 00000000..b9a87cd3 --- /dev/null +++ b/tests/CommandLine.Tests/Fakes/Hidden_Option.cs @@ -0,0 +1,8 @@ +namespace CommandLine.Tests.Fakes +{ + public class Hidden_Option + { + [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_Custom_Help_Option.cs b/tests/CommandLine.Tests/Fakes/Options_With_Custom_Help_Option.cs new file mode 100644 index 00000000..0215d392 --- /dev/null +++ b/tests/CommandLine.Tests/Fakes/Options_With_Custom_Help_Option.cs @@ -0,0 +1,8 @@ +namespace CommandLine.Tests.Fakes +{ + class Options_With_Custom_Help_Option : Simple_Options + { + [Option('h', "help")] + public bool Help { get; set; } + } +} diff --git a/tests/CommandLine.Tests/Fakes/Options_With_Custom_Version_Option.cs b/tests/CommandLine.Tests/Fakes/Options_With_Custom_Version_Option.cs new file mode 100644 index 00000000..df392ff5 --- /dev/null +++ b/tests/CommandLine.Tests/Fakes/Options_With_Custom_Version_Option.cs @@ -0,0 +1,8 @@ +namespace CommandLine.Tests.Fakes +{ + class Options_With_Custom_Version_Option : Simple_Options + { + [Option('v', "version")] + public bool MyVersion { 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_FSharpOption.cs b/tests/CommandLine.Tests/Fakes/Options_With_FSharpOption.cs index 7cf66998..0bc67b08 100644 --- a/tests/CommandLine.Tests/Fakes/Options_With_FSharpOption.cs +++ b/tests/CommandLine.Tests/Fakes/Options_With_FSharpOption.cs @@ -1,6 +1,5 @@ // Copyright 2005-2015 Giacomo Stelluti Scala & Contributors. All rights reserved. See License.md in the project root for license information. -#if !SKIP_FSHARP using Microsoft.FSharp.Core; namespace CommandLine.Tests.Fakes @@ -13,5 +12,4 @@ public class Options_With_FSharpOption [Value(0)] public FSharpOption Offset { get; set; } } -} -#endif +} \ No newline at end of file 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_InvalidDefaults.cs b/tests/CommandLine.Tests/Fakes/Options_With_InvalidDefaults.cs new file mode 100644 index 00000000..cedb1136 --- /dev/null +++ b/tests/CommandLine.Tests/Fakes/Options_With_InvalidDefaults.cs @@ -0,0 +1,11 @@ + +namespace CommandLine.Tests.Fakes +{ + class Options_With_InvalidDefaults + { + // Default of string and integer type property will also throw. + + [Option(Default = false)] + public string FileName { 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_Property_Throwing_Exception.cs b/tests/CommandLine.Tests/Fakes/Options_With_Property_Throwing_Exception.cs new file mode 100644 index 00000000..031dcb6c --- /dev/null +++ b/tests/CommandLine.Tests/Fakes/Options_With_Property_Throwing_Exception.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CommandLine.Tests.Fakes +{ + class Options_With_Property_Throwing_Exception + { + private string optValue; + + [Option('e')] + public string OptValue + { + get + { + return optValue; + } + set + { + if (value != "good") + throw new ArgumentException("Invalid value, only accept 'good' value"); + + optValue = value; + } + } + } +} 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_TimeSpan.cs b/tests/CommandLine.Tests/Fakes/Options_With_TimeSpan.cs new file mode 100644 index 00000000..0e88896d --- /dev/null +++ b/tests/CommandLine.Tests/Fakes/Options_With_TimeSpan.cs @@ -0,0 +1,12 @@ +// 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 Options_With_TimeSpan + { + [Option('d', "duration")] + public TimeSpan Duration { 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/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/Properties/AssemblyInfo.cs b/tests/CommandLine.Tests/Properties/AssemblyInfo.cs deleted file mode 100644 index a526d3a8..00000000 --- a/tests/CommandLine.Tests/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright 2005-2015 Giacomo Stelluti Scala & Contributors. All rights reserved. See License.md in the project root for license information. - -using System.Reflection; -using System.Runtime.InteropServices; -using CommandLine.Text; - -[assembly: AssemblyTitle("CommandLine.Tests.dll")] -[assembly: AssemblyDescription("Command Line Parser Library allows CLR applications to define a syntax for parsing command line arguments.")] -[assembly: AssemblyCulture("")] - -[assembly: AssemblyLicense( - "This is free software. You may redistribute copies of it under the terms of", - "the MIT License .")] -[assembly: AssemblyUsage( - "[no usage, this is a dll]")] - -#if DEBUG -[assembly: AssemblyConfiguration("Debug")] -#else -//[assembly: AssemblyConfiguration("Release")] -#endif -[assembly: ComVisible(false)] -//[assembly: CLSCompliant(true)] -//[assembly: AssemblyCompany("")] -//[assembly: AssemblyTrademark("")] \ No newline at end of file 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 7d7544e6..2f8d02b7 100644 --- a/tests/CommandLine.Tests/Unit/Core/InstanceBuilderTests.cs +++ b/tests/CommandLine.Tests/Unit/Core/InstanceBuilderTests.cs @@ -1,25 +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; -#if !SKIP_FSHARP using Microsoft.FSharp.Core; -#endif 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; namespace CommandLine.Tests.Unit.Core { public class InstanceBuilderTests { - private static ParserResult InvokeBuild(string[] arguments) + private static ParserResult InvokeBuild(string[] arguments, bool autoHelp = true, bool autoVersion = true, bool multiInstance = false) where T : new() { return InstanceBuilder.Build( @@ -29,6 +29,9 @@ private static ParserResult InvokeBuild(string[] arguments) StringComparer.Ordinal, false, CultureInfo.InvariantCulture, + autoHelp, + autoVersion, + multiInstance, Enumerable.Empty()); } @@ -42,6 +45,8 @@ private static ParserResult InvokeBuildEnumValuesCaseIgnore(string[] argum StringComparer.Ordinal, true, CultureInfo.InvariantCulture, + true, + true, Enumerable.Empty()); } @@ -54,6 +59,8 @@ private static ParserResult InvokeBuildImmutable(string[] arguments) StringComparer.Ordinal, false, CultureInfo.InvariantCulture, + true, + true, Enumerable.Empty()); } @@ -69,13 +76,11 @@ public void Explicit_help_request_generates_help_requested_error() new[] { "--help" }); // Verify outcome - result.ShouldBeEquivalentTo(expectedResult); - - // Teardown + result.Should().BeEquivalentTo(expectedResult); } [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) @@ -87,9 +92,7 @@ public void Parse_negative_long_value(string[] arguments, long expected) arguments); // Verify outcome - ((Parsed)result).Value.LongValue.ShouldBeEquivalentTo(expected); - - // Teardown + ((Parsed)result).Value.LongValue.Should().Be(expected); } [Theory] @@ -107,9 +110,7 @@ public void Parse_double_value(string[] arguments, double expected) arguments); // Verify outcome - ((Parsed)result).Value.DoubleValue.ShouldBeEquivalentTo(expected); - - // Teardown + ((Parsed)result).Value.DoubleValue.Should().Be(expected); } [Theory] @@ -128,9 +129,7 @@ public void Parse_int_sequence(string[] arguments, int[] expected) arguments); // Verify outcome - ((Parsed)result).Value.IntSequence.ShouldBeEquivalentTo(expected); - - // Teardown + ((Parsed)result).Value.IntSequence.Should().BeEquivalentTo(expected); } [Theory] @@ -147,15 +146,13 @@ public void Parse_int_sequence_with_range(string[] arguments, int[] expected) arguments); // Verify outcome - ((Parsed)result).Value.IntSequence.ShouldBeEquivalentTo(expected); - - // Teardown + ((Parsed)result).Value.IntSequence.Should().BeEquivalentTo(expected); } [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) @@ -167,9 +164,7 @@ public void Parse_string_sequence_with_only_min_constraint(string[] arguments, s arguments); // Verify outcome - ((Parsed)result).Value.StringSequence.ShouldBeEquivalentTo(expected); - - // Teardown + ((Parsed)result).Value.StringSequence.Should().BeEquivalentTo(expected); } [Theory] @@ -186,13 +181,11 @@ public void Parse_string_sequence_with_only_max_constraint(string[] arguments, s arguments); // Verify outcome - ((Parsed)result).Value.StringSequence.ShouldBeEquivalentTo(expected); - - // Teardown + ((Parsed)result).Value.StringSequence.Should().BeEquivalentTo(expected); } [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")) }; @@ -202,13 +195,11 @@ public void Breaking_min_constraint_in_string_sequence_gererates_MissingValueOpt new[] { "-s" }); // Verify outcome - ((NotParsed)result).Errors.ShouldBeEquivalentTo(expectedResult); - - // Teardown + ((NotParsed)result).Errors.Should().BeEquivalentTo(expectedResult); } [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) }; @@ -218,29 +209,26 @@ public void Breaking_min_constraint_in_string_sequence_as_value_gererates_Sequen new string[] { }); // Verify outcome - ((NotParsed)result).Errors.ShouldBeEquivalentTo(expectedResult); - - // Teardown + ((NotParsed)result).Errors.Should().BeEquivalentTo(expectedResult); } [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.ShouldBeEquivalentTo(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) }; @@ -250,9 +238,7 @@ public void Breaking_max_constraint_in_string_sequence_as_value_gererates_Sequen new[] { "one", "two", "three", "this-is-too-much" }); // Verify outcome - ((NotParsed)result).Errors.ShouldBeEquivalentTo(expectedResult); - - // Teardown + ((NotParsed)result).Errors.Should().BeEquivalentTo(expectedResult); } [Theory] @@ -271,9 +257,7 @@ public void Parse_enum_value(string[] arguments, Colors expected) arguments); // Verify outcome - expected.ShouldBeEquivalentTo(((Parsed)result).Value.Colors); - - // Teardown + expected.Should().BeEquivalentTo(((Parsed)result).Value.Colors); } [Theory] @@ -292,9 +276,7 @@ public void Parse_enum_value_ignore_case(string[] arguments, Colors expected) arguments); // Verify outcome - expected.ShouldBeEquivalentTo(((Parsed)result).Value.Colors); - - // Teardown + expected.Should().BeEquivalentTo(((Parsed)result).Value.Colors); } [Fact] @@ -308,9 +290,7 @@ public void Parse_enum_value_with_wrong_index_generates_BadFormatConversionError new[] { "--colors", "3" }); // Verify outcome - ((NotParsed)result).Errors.ShouldBeEquivalentTo(expectedResult); - - // Teardown + ((NotParsed)result).Errors.Should().BeEquivalentTo(expectedResult); } [Fact] @@ -324,9 +304,7 @@ public void Parse_enum_value_with_wrong_item_name_generates_BadFormatConversionE new[] { "--colors", "Yellow" }); // Verify outcome - ((NotParsed)result).Errors.ShouldBeEquivalentTo(expectedResult); - - // Teardown + ((NotParsed)result).Errors.Should().BeEquivalentTo(expectedResult); } [Fact] @@ -340,9 +318,7 @@ public void Parse_enum_value_with_wrong_item_name_case_generates_BadFormatConver new[] { "--colors", "RED" }); // Verify outcome - ((NotParsed)result).Errors.ShouldBeEquivalentTo(expectedResult); - - // Teardown + ((NotParsed)result).Errors.Should().BeEquivalentTo(expectedResult); } [Fact] @@ -350,21 +326,19 @@ 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( new[] { "10", "a", "b", "c", "20" }); // Verify outcome - expectedResult.ShouldBeEquivalentTo(((Parsed)result).Value); - - // Teardown + expectedResult.Should().BeEquivalentTo(((Parsed)result).Value); } [Theory] @@ -382,9 +356,7 @@ public void Parse_sequence_value_without_range_constraints(string[] arguments, l arguments); // Verify outcome - expected.ShouldBeEquivalentTo(((Parsed)result).Value.LongSequence); - - // Teardown + expected.Should().BeEquivalentTo(((Parsed)result).Value.LongSequence); } [Theory] @@ -401,9 +373,7 @@ public void Parse_long_sequence_with_separator(string[] arguments, long[] expect arguments); // Verify outcome - expected.ShouldBeEquivalentTo(((Parsed)result).Value.LongSequence); - - // Teardown + expected.Should().BeEquivalentTo(((Parsed)result).Value.LongSequence); } [Theory] @@ -420,9 +390,7 @@ public void Parse_string_sequence_with_separator(string[] arguments, string[] ex arguments); // Verify outcome - expected.ShouldBeEquivalentTo(((Parsed)result).Value.StringSequence); - - // Teardown + expected.Should().BeEquivalentTo(((Parsed)result).Value.StringSequence); } /// @@ -433,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 @@ -451,16 +419,16 @@ public void Double_dash_force_subsequent_arguments_as_values() StringComparer.Ordinal, false, CultureInfo.InvariantCulture, + true, + true, Enumerable.Empty()); // Verify outcome - expectedResult.ShouldBeEquivalentTo(((Parsed)result).Value); - - // Teardown + expectedResult.Should().BeEquivalentTo(((Parsed)result).Value); } [Fact] - public void Parse_option_from_different_sets_gererates_MutuallyExclusiveSetError() + public void Parse_option_from_different_sets_generates_MutuallyExclusiveSetError() { // Fixture setup var expectedResult = new[] @@ -474,15 +442,15 @@ public void Parse_option_from_different_sets_gererates_MutuallyExclusiveSetError new[] { "--weburl", "http://mywebsite.org/", "--ftpurl", "fpt://ftpsite.org/" }); // Verify outcome - ((NotParsed)result).Errors.ShouldBeEquivalentTo(expectedResult); - - // Teardown + ((NotParsed)result).Errors.Should().BeEquivalentTo(expectedResult); } [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" }; @@ -491,12 +459,13 @@ public void Two_required_options_at_the_same_set_and_both_are_true() { new[] { "--ftpurl", "str1", "--weburl", "str2" }); // Verify outcome - expectedResult.ShouldBeEquivalentTo(((Parsed)result).Value); + expectedResult.Should().BeEquivalentTo(((Parsed)result).Value); // Teardown } [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[] { @@ -508,13 +477,11 @@ public void Two_required_options_at_the_same_set_and_none_are_true() { new string[] { }); // Verify outcome - ((NotParsed)result).Errors.ShouldBeEquivalentTo(expectedResult); - - // Teardown + ((NotParsed)result).Errors.Should().BeEquivalentTo(expectedResult); } [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")) }; @@ -524,13 +491,11 @@ public void Omitting_required_option_gererates_MissingRequiredOptionError() new string[] { }); // Verify outcome - ((NotParsed)result).Errors.ShouldBeEquivalentTo(expectedResult); - - // Teardown + ((NotParsed)result).Errors.Should().BeEquivalentTo(expectedResult); } [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", "")) }; @@ -540,13 +505,11 @@ public void Wrong_range_in_sequence_gererates_SequenceOutOfRangeError() new[] { "-i", "10" }); // Verify outcome - ((NotParsed)result).Errors.ShouldBeEquivalentTo(expectedResult); - - // Teardown + ((NotParsed)result).Errors.Should().BeEquivalentTo(expectedResult); } [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") }; @@ -556,13 +519,11 @@ public void Parse_unknown_long_option_gererates_UnknownOptionError() new[] { "--stringvalue", "abc", "--xyz" }); // Verify outcome - ((NotParsed)result).Errors.ShouldBeEquivalentTo(expectedResult); - - // Teardown + ((NotParsed)result).Errors.Should().BeEquivalentTo(expectedResult); } [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") }; @@ -572,13 +533,11 @@ public void Parse_unknown_short_option_gererates_UnknownOptionError() new[] { "-z", "-x" }); // Verify outcome - ((NotParsed)result).Errors.ShouldBeEquivalentTo(expectedResult); - - // Teardown + ((NotParsed)result).Errors.Should().BeEquivalentTo(expectedResult); } [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") }; @@ -588,14 +547,12 @@ public void Parse_unknown_short_option_in_option_group_gererates_UnknownOptionEr new[] { "-zx" }); // Verify outcome - ((NotParsed)result).Errors.ShouldBeEquivalentTo(expectedResult); - - // Teardown + ((NotParsed)result).Errors.Should().BeEquivalentTo(expectedResult); } [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 @@ -605,9 +562,7 @@ public void Omitting_names_assumes_identifier_as_long_name(string[] arguments, s arguments); // Verify outcome - ((Parsed)result).Value.StringValue.ShouldBeEquivalentTo(expected); - - // Teardown + ((Parsed)result).Value.StringValue.Should().BeEquivalentTo(expected); } [Fact] @@ -621,9 +576,7 @@ public void Breaking_required_constraint_in_string_scalar_as_value_generates_Mis new string[] { }); // Verify outcome - ((NotParsed)result).Errors.ShouldBeEquivalentTo(expectedResult); - - // Teardown + ((NotParsed)result).Errors.Should().BeEquivalentTo(expectedResult); } [Theory] @@ -640,13 +593,11 @@ public void Parse_utf8_string_correctly(string[] arguments, string expected) arguments); // Verify outcome - expected.ShouldBeEquivalentTo(((Parsed)result).Value.StringValue); - - // Teardown + expected.Should().BeEquivalentTo(((Parsed)result).Value.StringValue); } [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) }; @@ -656,9 +607,7 @@ public void Breaking_equal_min_max_constraint_in_string_sequence_as_value_gerera new[] { "one", "two", "this-is-too-much" }); // Verify outcome - ((NotParsed)result).Errors.ShouldBeEquivalentTo(expectedResult); - - // Teardown + ((NotParsed)result).Errors.Should().BeEquivalentTo(expectedResult); } [Theory] @@ -675,9 +624,7 @@ public void Parse_nullable_int(string[] arguments, int? expected) arguments); // Verify outcome - expected.ShouldBeEquivalentTo(((Parsed)result).Value.NullableInt); - - // Teardown + expected.Should().Be(((Parsed)result).Value.NullableInt); } [Theory] @@ -694,9 +641,7 @@ public void Parse_nullable_long(string[] arguments, long? expected) arguments); // Verify outcome - expected.ShouldBeEquivalentTo(((Parsed)result).Value.NullableLong); - - // Teardown + expected.Should().Be(((Parsed)result).Value.NullableLong); } #if !SKIP_FSHARP @@ -714,11 +659,9 @@ public void Parse_fsharp_option_string(string[] arguments, string expectedValue, // Verify outcome if (((Parsed)result).Value.FileName != null) { - expectedValue.ShouldBeEquivalentTo(((Parsed)result).Value.FileName.Value); + expectedValue.Should().BeEquivalentTo(((Parsed)result).Value.FileName.Value); } - expectedSome.ShouldBeEquivalentTo(FSharpOption.get_IsSome(((Parsed)result).Value.FileName)); - - // Teardown + expectedSome.Should().Be(FSharpOption.get_IsSome(((Parsed)result).Value.FileName)); } [Theory] @@ -735,11 +678,9 @@ public void Parse_fsharp_option_int(string[] arguments, int expectedValue, bool // Verify outcome if (((Parsed)result).Value.Offset != null) { - expectedValue.ShouldBeEquivalentTo(((Parsed)result).Value.Offset.Value); + expectedValue.Should().Be(((Parsed)result).Value.Offset.Value); } - expectedSome.ShouldBeEquivalentTo(FSharpOption.get_IsSome(((Parsed)result).Value.Offset)); - - // Teardown + expectedSome.Should().Be(FSharpOption.get_IsSome(((Parsed)result).Value.Offset)); } #endif @@ -778,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)] @@ -848,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")] @@ -858,7 +813,7 @@ public void Can_define_options_on_interface_properties(string[] arguments, strin arguments); // Verify outcome - expected.ShouldBeEquivalentTo(((Parsed)result).Value.InputFile); + expected.Should().BeEquivalentTo(((Parsed)result).Value.InputFile); } [Theory] @@ -887,7 +842,7 @@ public void Enforce_required_within_mutually_exclusive_set_only(string[] argumen } [Theory] - [MemberData("RequiredValueStringData")] + [MemberData(nameof(RequiredValueStringData))] public void Parse_string_scalar_with_required_constraint_as_value(string[] arguments, Options_With_Required_Set_To_True_For_Values expected) { // Fixture setup in attributes @@ -897,13 +852,11 @@ public void Parse_string_scalar_with_required_constraint_as_value(string[] argum arguments); // Verify outcome - expected.ShouldBeEquivalentTo(((Parsed)result).Value); - - // Teardown + expected.Should().BeEquivalentTo(((Parsed)result).Value); } [Theory] - [MemberData("ScalarSequenceStringAdjacentData")] + [MemberData(nameof(ScalarSequenceStringAdjacentData))] public void Parse_string_scalar_and_sequence_adjacent(string[] arguments, Options_With_Scalar_Value_And_Adjacent_SequenceString expected) { // Fixture setup in attributes @@ -913,34 +866,30 @@ public void Parse_string_scalar_and_sequence_adjacent(string[] arguments, Option arguments); // Verify outcome - expected.ShouldBeEquivalentTo(((Parsed)result).Value); - - // Teardown + expected.Should().BeEquivalentTo(((Parsed)result).Value); } [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( new[] { "--stringvalue=strval0", "-i", "9", "7", "8", "-x", "9876543210" }); // Verify outcome - expectedResult.ShouldBeEquivalentTo(((Parsed)result).Value); - - // Teardown + expectedResult.Should().BeEquivalentTo(((Parsed)result).Value); } [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 @@ -953,7 +902,7 @@ public void Breaking_required_constraint_generate_MissingRequiredOptionError(str } [Theory] - [MemberData("ImmutableInstanceData")] + [MemberData(nameof(ImmutableInstanceData))] public void Parse_to_immutable_instance(string[] arguments, Immutable_Simple_Options expected) { // Fixture setup in attributes @@ -963,9 +912,24 @@ public void Parse_to_immutable_instance(string[] arguments, Immutable_Simple_Opt arguments); // Verify outcome - expected.ShouldBeEquivalentTo(((Parsed)result).Value); + 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] @@ -979,11 +943,40 @@ public void Parse_to_type_with_single_string_ctor_builds_up_correct_instance() new[] { "--endpoint=http://localhost/test/", "custom-value" }); // Verify outcome - expectedResult.ShouldBeEquivalentTo(((Parsed)result).Value); + expectedResult.Should().BeEquivalentTo(((Parsed)result).Value); + } - // Teardown + [Fact] + public void Parse_option_with_exception_thrown_from_setter_generates_SetValueExceptionError() + { + // Fixture setup + var expectedResult = new[] { new SetValueExceptionError(new NameInfo("e", ""), new ArgumentException(), "bad") }; + + // Exercize system + var result = InvokeBuild( + new[] { "-e", "bad" }); + + // Verify outcome + ((NotParsed)result).Errors.Should().BeEquivalentTo(expectedResult); } + [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); + } + + [Theory] [InlineData(new[] { "--stringvalue", "x-" }, "x-")] [InlineData(new[] { "--stringvalue", "x--" }, "x--")] @@ -1002,13 +995,73 @@ public void Parse_string_with_dashes_except_in_beginning(string[] arguments, str arguments); // Verify outcome - expected.ShouldBeEquivalentTo(((Parsed)result).Value.StringValue); + expected.Should().BeEquivalentTo(((Parsed)result).Value.StringValue); + } - // Teardown + [Theory] + [InlineData(new[] { "--help" }, ErrorType.UnknownOptionError)] + public void Parse_without_auto_help_should_not_recognize_help_option(string[] arguments, ErrorType errorType) + { + // Fixture setup in attributes + + // Exercize system + var result = InvokeBuild(arguments, autoHelp: false); + + // Verify outcome + result.Should().BeOfType>() + .Which.Errors.Should().ContainSingle() + .Which.Tag.Should().Be(errorType); } [Theory] - [MemberData("GuidData")] + [InlineData(new[] { "--help" }, true)] + [InlineData(new[] { "-h" }, true)] + [InlineData(new[] { "-x" }, false)] + public void Parse_with_custom_help_option(string[] arguments, bool isHelp) + { + // Fixture setup in attributes + + // Exercize system + var result = InvokeBuild(arguments, autoHelp: false); + + // Verify outcome + result.Should().BeOfType>() + .Which.Value.Help.Should().Be(isHelp); + } + + [Theory] + [InlineData(new[] { "--version" }, ErrorType.UnknownOptionError)] + public void Parse_without_auto_version_should_not_recognize_version_option(string[] arguments, ErrorType errorType) + { + // Fixture setup in attributes + + // Exercize system + var result = InvokeBuild(arguments, autoVersion: false); + + // Verify outcome + result.Should().BeOfType>() + .Which.Errors.Should().ContainSingle() + .Which.Tag.Should().Be(errorType); + } + + [Theory] + [InlineData(new[] { "--version" }, true)] + [InlineData(new[] { "-v" }, true)] + [InlineData(new[] { "-s", "s" }, false)] + public void Parse_with_custom_version_option(string[] arguments, bool isVersion) + { + // Fixture setup in attributes + + // Exercize system + var result = InvokeBuild(arguments, autoVersion: false); + + // Verify outcome + result.Should().BeOfType>() + .Which.Value.MyVersion.Should().Be(isVersion); + } + + [Theory] + [MemberData(nameof(GuidData))] public void Parse_Guid(string[] arguments, Options_With_Guid expected) { // Fixture setup in attributes @@ -1018,12 +1071,243 @@ public void Parse_Guid(string[] arguments, Options_With_Guid expected) arguments); // Verify outcome - expected.ShouldBeEquivalentTo(((Parsed)result).Value); + expected.Should().BeEquivalentTo(((Parsed)result).Value); + } - // Teardown + [Fact] + public void Parse_TimeSpan() + { + // Fixture setup + var expectedResult = new Options_With_TimeSpan { Duration = TimeSpan.FromMinutes(42) }; + + // Exercize system + var result = InvokeBuild( + new[] { "--duration=00:42:00" }); + + // Verify outcome + expectedResult.Should().BeEquivalentTo(((Parsed)result).Value); + } + + #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); } - public static IEnumerable RequiredValueStringData + [Fact] + public void Options_In_Group_With_No_Values_Generates_MissingGroupOptionErrors() + { + // Fixture setup + var optionNames1 = new List + { + new NameInfo("", "option11"), + new NameInfo("", "option12") + }; + var optionNames2 = new List + { + new NameInfo("", "option21"), + new NameInfo("", "option22") + }; + var expectedResult = new[] + { + new MissingGroupOptionError("err-group", optionNames1), + new MissingGroupOptionError("err-group2", optionNames2) + }; + + // Exercize system + var result = InvokeBuild( + new[] { "-v 10.42" }); + + // Verify outcome + ((NotParsed)result).Errors.Should().BeEquivalentTo(expectedResult); + } + + [Theory] + [InlineData("-v", "10.5", "--option1", "test1", "--option2", "test2")] + [InlineData("-v", "10.5", "--option1", "test1")] + [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>(); + } + + [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 { @@ -1035,11 +1319,11 @@ public static IEnumerable RequiredValueStringData } } - public static IEnumerable ScalarSequenceStringAdjacentData + 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" } } }; @@ -1048,7 +1332,7 @@ public static IEnumerable ScalarSequenceStringAdjacentData } } - public static IEnumerable ImmutableInstanceData + public static IEnumerable ImmutableInstanceData { get { @@ -1060,8 +1344,20 @@ 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 + public static IEnumerable GuidData { get { diff --git a/tests/CommandLine.Tests/Unit/Core/InstanceChooserTests.cs b/tests/CommandLine.Tests/Unit/Core/InstanceChooserTests.cs index 96d175b1..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,14 +15,19 @@ 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), types, arguments, StringComparer.Ordinal, + false, CultureInfo.InvariantCulture, + true, + true, + multiInstance, Enumerable.Empty()); } @@ -38,7 +43,7 @@ public void Parse_empty_array_returns_NullInstance() new string[] { }); // Verify outcome - ((NotParsed)result).Errors.ShouldBeEquivalentTo(expectedErrors); + ((NotParsed)result).Errors.Should().BeEquivalentTo(expectedErrors); // Teardown } @@ -55,7 +60,7 @@ public void Explicit_help_request_generates_HelpVerbRequestedError() new[] { "help" }); // Verify outcome - ((NotParsed)result).Errors.ShouldBeEquivalentTo(expectedErrors); + ((NotParsed)result).Errors.Should().BeEquivalentTo(expectedErrors); // Teardown } @@ -72,7 +77,7 @@ public void Explicit_help_request_for_a_valid_verb_generates_HelpVerbRequestedEr new[] { "help", "commit" }); // Verify outcome - ((NotParsed)result).Errors.ShouldBeEquivalentTo(expectedErrors); + ((NotParsed)result).Errors.Should().BeEquivalentTo(expectedErrors); // Teardown } @@ -89,7 +94,7 @@ public void Explicit_help_request_for_an_invalid_verb_generates_HelpVerbRequeste new[] { "help", "earthunderalienattack" }); // Verify outcome - ((NotParsed)result).Errors.ShouldBeEquivalentTo(expectedErrors); + ((NotParsed)result).Errors.Should().BeEquivalentTo(expectedErrors); // Teardown } @@ -107,7 +112,7 @@ public void Parse_existing_verb_returns_verb_instance() // Verify outcome Assert.IsType(((Parsed)result).Value); - expected.ShouldBeEquivalentTo(((Parsed)result).Value); + expected.Should().BeEquivalentTo(((Parsed)result).Value); // Teardown } @@ -124,7 +129,7 @@ public void Parse_existing_verb_returns_verb_immutable_instance() // Verify outcome Assert.IsType(((Parsed)result).Value); - expected.ShouldBeEquivalentTo(((Parsed)result).Value); + expected.Should().BeEquivalentTo(((Parsed)result).Value); // Teardown } @@ -141,7 +146,7 @@ public void Parse_sequence_verb_returns_verb_instance() // Verify outcome Assert.IsType(((Parsed)result).Value); - expected.ShouldBeEquivalentTo(((Parsed)result).Value); + expected.Should().BeEquivalentTo(((Parsed)result).Value); // Teardown } @@ -162,8 +167,21 @@ public void Parse_sequence_verb_with_separator_returns_verb_instance(string[] ar // Verify outcome Assert.IsType(((Parsed)result).Value); - expected.ShouldBeEquivalentTo(((Parsed)result).Value); + 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 3dc79fe4..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,12 +17,12 @@ 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); // Verify outcome - expected.ShouldBeEquivalentTo(result); + expected.Should().BeEquivalentTo(result); // Teardown } @@ -35,13 +35,13 @@ 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(); // Verify outcome - expected.ShouldBeEquivalentTo(result); + expected.Should().BeEquivalentTo(result); // Teardown } 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 570d07a2..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,14 +15,15 @@ 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.ShouldBeEquivalentTo(result); + expected.Should().BeEquivalentTo(result); } [Fact] @@ -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,8 +41,9 @@ 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.ShouldBeEquivalentTo(result); + expected.Should().BeEquivalentTo(result); } } } diff --git a/tests/CommandLine.Tests/Unit/Core/SequenceTests.cs b/tests/CommandLine.Tests/Unit/Core/SequenceTests.cs index 4aa0fe44..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,14 +15,15 @@ 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.ShouldAllBeEquivalentTo(result); + expected.Should().AllBeEquivalentTo(result); } [Fact] @@ -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,8 +45,9 @@ 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.ShouldBeEquivalentTo(result); + expected.Should().BeEquivalentTo(result); } [Fact] @@ -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,8 +71,9 @@ 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.ShouldBeEquivalentTo(result); + expected.Should().BeEquivalentTo(result); } [Fact] @@ -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 - expected.ShouldBeEquivalentTo(result); + // 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 14c5c62c..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,14 +15,15 @@ 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.ShouldBeEquivalentTo(result); + expected.Should().BeEquivalentTo(result); } [Fact] @@ -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,8 +41,9 @@ 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.ShouldBeEquivalentTo(result); + 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 08aae057..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 = @@ -35,7 +32,7 @@ public void Explode_scalar_with_separator_in_odd_args_input_returns_sequence() Enumerable.Empty()), optionName => NameLookup.HavingSeparator(optionName, specs, StringComparer.Ordinal)); // Verify outcome - ((Ok, Error>)result).Success.ShouldBeEquivalentTo(expectedTokens); + ((Ok, Error>)result).Success.Should().BeEquivalentTo(expectedTokens); // Teardown } @@ -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 = @@ -59,35 +56,63 @@ public void Explode_scalar_with_separator_in_even_args_input_returns_sequence() optionName => NameLookup.HavingSeparator(optionName, specs, StringComparer.Ordinal)); // Verify outcome - ((Ok, Error>)result).Success.ShouldBeEquivalentTo(expectedTokens); + ((Ok, Error>)result).Success.Should().BeEquivalentTo(expectedTokens); // Teardown } [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.ShouldBeEquivalentTo(expectedTokens); + result.Should().BeEquivalentTo(expectedTokens); // Teardown } @@ -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 7642a80a..c3e93781 100644 --- a/tests/CommandLine.Tests/Unit/Core/TypeConverterTests.cs +++ b/tests/CommandLine.Tests/Unit/Core/TypeConverterTests.cs @@ -1,15 +1,10 @@ using System; -using System.CodeDom; using System.Collections.Generic; -using System.Dynamic; using System.Globalization; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using CommandLine.Core; -using CSharpx; -using FluentAssertions; using Xunit; +using FluentAssertions; +using CSharpx; +using CommandLine.Core; namespace CommandLine.Tests.Unit.Core { @@ -21,11 +16,18 @@ enum TestEnum ValueB = 2 } + [Flags] + enum TestFlagEnum + { + ValueA = 0x1, + ValueB = 0x2 + } + [Theory] - [MemberData("ChangeType_scalars_source")] + [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) { @@ -33,9 +35,7 @@ public void ChangeType_scalars(string testValue, Type destinationType, bool expe } else { - object matchedValue; - - result.MatchJust(out matchedValue).Should().BeTrue("should parse successfully"); + result.MatchJust(out object matchedValue).Should().BeTrue("should parse successfully"); Assert.Equal(matchedValue, expectedResult); } } @@ -57,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}, @@ -101,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/Infrastructure/FSharpOptionHelperTests.cs b/tests/CommandLine.Tests/Unit/Infrastructure/FSharpOptionHelperTests.cs index 38e0e86f..80f0fc61 100644 --- a/tests/CommandLine.Tests/Unit/Infrastructure/FSharpOptionHelperTests.cs +++ b/tests/CommandLine.Tests/Unit/Infrastructure/FSharpOptionHelperTests.cs @@ -1,6 +1,5 @@ // Copyright 2005-2015 Giacomo Stelluti Scala & Contributors. All rights reserved. See License.md in the project root for license information. -#if !SKIP_FSHARP using System.Reflection; using CommandLine.Infrastructure; using CommandLine.Tests.Fakes; @@ -23,7 +22,7 @@ public void Match_type_returns_true_if_FSharpOption() public void Get_underlying_type() { FSharpOptionHelper.GetUnderlyingType(TestData.PropertyType).FullName - .ShouldBeEquivalentTo("System.String"); + .Should().BeEquivalentTo("System.String"); } [Fact] @@ -49,4 +48,3 @@ private PropertyInfo TestData } } } -#endif 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 26170d03..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 { @@ -18,7 +17,17 @@ public static void Invoke_parsed_lambda_when_parsed() Parser.Default.ParseArguments(new[] { "--stringvalue", "value" }) .WithParsed(opts => expected = opts.StringValue); - "value".ShouldBeEquivalentTo(expected); + "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] @@ -31,7 +40,21 @@ public static void Invoke_parsed_lambda_when_parsed_for_verbs() .WithParsed(opts => expected = "wrong2") .WithParsed(opts => expected = opts.Urls.First()); - "https://value.org/user/file.git".ShouldBeEquivalentTo(expected); + "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] @@ -41,7 +64,17 @@ public static void Invoke_not_parsed_lambda_when_not_parsed() Parser.Default.ParseArguments(new[] { "-i", "aaa" }) .WithNotParsed(_ => expected = "changed"); - "changed".ShouldBeEquivalentTo(expected); + "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] @@ -54,7 +87,21 @@ public static void Invoke_not_parsed_lambda_when_parsed_for_verbs() .WithParsed(opts => expected = "wrong3") .WithNotParsed(_ => expected = "changed"); - "changed".ShouldBeEquivalentTo(expected); + "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] @@ -65,7 +112,19 @@ public static void Invoke_proper_lambda_when_parsed() .WithParsed(opts => expected = opts.StringValue) .WithNotParsed(_ => expected = "changed"); - "value".ShouldBeEquivalentTo(expected); + "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] @@ -76,7 +135,19 @@ public static void Invoke_proper_lambda_when_not_parsed() .WithParsed(opts => expected = opts.StringValue) .WithNotParsed(_ => expected = "changed"); - "changed".ShouldBeEquivalentTo(expected); + "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] @@ -85,7 +156,7 @@ public static void Turn_sucessful_parsing_into_exit_code() var expected = Parser.Default.ParseArguments(new[] { "--stringvalue", "value" }) .MapResult(_ => 0, _ => -1); - 0.ShouldBeEquivalentTo(expected); + 0.Should().Be(expected); } [Fact] @@ -99,7 +170,7 @@ public static void Turn_sucessful_parsing_into_exit_code_for_verbs() (Clone_Verb opts) => 2, errs => 3); - 2.ShouldBeEquivalentTo(expected); + 2.Should().Be(expected); } [Fact] @@ -108,7 +179,7 @@ public static void Turn_failed_parsing_into_exit_code() var expected = Parser.Default.ParseArguments(new[] { "-i", "aaa" }) .MapResult(_ => 0, _ => -1); - (-1).ShouldBeEquivalentTo(expected); + (-1).Should().Be(expected); } [Fact] @@ -122,7 +193,7 @@ public static void Turn_failed_parsing_into_exit_code_for_verbs() (Clone_Verb opts) => 2, errs => 3); - 3.ShouldBeEquivalentTo(expected); + 3.Should().Be(expected); } [Fact] @@ -136,7 +207,22 @@ public static void Invoke_parsed_lambda_when_parsed_for_base_verbs() .WithParsed(opts => expected = "wrong3") .WithParsed(opts => expected = opts.FileName); - "dummy.bin".ShouldBeEquivalentTo(expected); + "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] @@ -148,7 +234,7 @@ public static void Turn_sucessful_parsing_into_exit_code_for_single_base_verbs() (Base_Class_For_Verb opts) => 1, errs => 2); - 1.ShouldBeEquivalentTo(expected); + 1.Should().Be(expected); } [Fact] @@ -164,7 +250,7 @@ public static void Turn_sucessful_parsing_into_exit_code_for_multiple_base_verbs (Derived_Verb opts) => 3, errs => 5); - 4.ShouldBeEquivalentTo(expected); + 4.Should().Be(expected); } } } 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 43b44e27..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 { @@ -72,7 +72,7 @@ public void Parse_options() var result = sut.ParseArguments(new[] { "--stringvalue=strvalue", "-i1", "2", "3" }); // Verify outcome - ((Parsed)result).Value.ShouldBeEquivalentTo(expectedOptions); + ((Parsed)result).Value.Should().BeEquivalentTo(expectedOptions); // Teardown } @@ -91,7 +91,37 @@ public void Parse_options_with_short_name(string outputFile, string[] args) var result = sut.ParseArguments(args); // Verify outcome - ((Parsed)result).Value.ShouldBeEquivalentTo(expectedOptions); + ((Parsed)result).Value.Should().BeEquivalentTo(expectedOptions); + // 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 } @@ -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 } @@ -128,10 +159,45 @@ public void Parse_options_with_double_dash() new[] { "--stringvalue", "astring", "--", "20", "--aaa", "-b", "--ccc", "30" }); // Verify outcome - ((Parsed)result).Value.ShouldBeEquivalentTo(expectedOptions); + ((Parsed)result).Value.Should().BeEquivalentTo(expectedOptions); // 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() { @@ -148,7 +214,7 @@ public void Parse_options_with_double_dash_in_verbs_scenario() // Verify outcome Assert.IsType(((Parsed)result).Value); - ((Parsed)result).Value.ShouldBeEquivalentTo(expectedOptions, o => o.RespectingRuntimeTypes()); + ((Parsed)result).Value.Should().BeEquivalentTo(expectedOptions, o => o.RespectingRuntimeTypes()); // Teardown } @@ -164,7 +230,7 @@ public void Parse_options_with_single_dash() var result = sut.ParseArguments(args); // Verify outcome - ((Parsed)result).Value.ShouldBeEquivalentTo(expectedOptions); + ((Parsed)result).Value.Should().BeEquivalentTo(expectedOptions); // Teardown } @@ -194,7 +260,7 @@ public void Parse_verbs() // Verify outcome Assert.IsType(((Parsed)result).Value); - ((Parsed)result).Value.ShouldBeEquivalentTo(expectedOptions, o => o.RespectingRuntimeTypes()); + ((Parsed)result).Value.Should().BeEquivalentTo(expectedOptions, o => o.RespectingRuntimeTypes()); // Teardown } @@ -216,7 +282,7 @@ public void Parse_options_with_short_name_in_verbs_scenario(string message, stri // Verify outcome Assert.IsType(((Parsed)result).Value); - ((Parsed)result).Value.ShouldBeEquivalentTo(expectedOptions, o => o.RespectingRuntimeTypes()); + ((Parsed)result).Value.Should().BeEquivalentTo(expectedOptions, o => o.RespectingRuntimeTypes()); // Teardown } @@ -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 } @@ -259,7 +326,7 @@ public void Parse_verbs_using_generic_overload() // Verify outcome Assert.IsType(((Parsed)result).Value); - ((Parsed)result).Value.ShouldBeEquivalentTo(expectedOptions, o => o.RespectingRuntimeTypes()); + ((Parsed)result).Value.Should().BeEquivalentTo(expectedOptions, o => o.RespectingRuntimeTypes()); // Teardown } @@ -274,7 +341,7 @@ public void Parse_to_immutable_instance() var result = sut.ParseArguments(new[] { "--stringvalue=strvalue", "-i1", "2", "3" }); // Verify outcome - ((Parsed)result).Value.ShouldBeEquivalentTo(expectedOptions); + ((Parsed)result).Value.Should().BeEquivalentTo(expectedOptions); // Teardown } @@ -340,13 +407,8 @@ 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 } @@ -364,24 +426,41 @@ 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].ShouldBeEquivalentTo("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[2].ShouldBeEquivalentTo("ERROR(S):"); - lines[3].ShouldBeEquivalentTo("No verb selected."); - lines[4].ShouldBeEquivalentTo("add Add file contents to the index."); - lines[5].ShouldBeEquivalentTo("commit Record changes to the repository."); - lines[6].ShouldBeEquivalentTo("clone Clone a repository into a new directory."); - lines[7].ShouldBeEquivalentTo("help Display more information on a specific command."); - lines[8].ShouldBeEquivalentTo("version Display version information."); + 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."); + lines[5].Should().BeEquivalentTo("commit Record changes to the repository."); + lines[6].Should().BeEquivalentTo("clone Clone a repository into a new directory."); + lines[7].Should().BeEquivalentTo("help Display more information on a specific command."); + 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); + // 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() { @@ -395,19 +474,13 @@ 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].ShouldBeEquivalentTo("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[2].ShouldBeEquivalentTo("add Add file contents to the index."); - lines[3].ShouldBeEquivalentTo("commit Record changes to the repository."); - lines[4].ShouldBeEquivalentTo("clone Clone a repository into a new directory."); - lines[5].ShouldBeEquivalentTo("help Display more information on a specific command."); - lines[6].ShouldBeEquivalentTo("version Display version information."); + 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."); + lines[5].Should().BeEquivalentTo("help Display more information on a specific command."); + lines[6].Should().BeEquivalentTo("version Display version information."); // Teardown } @@ -428,12 +501,7 @@ 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 } @@ -450,22 +518,16 @@ 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].ShouldBeEquivalentTo("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[2].ShouldBeEquivalentTo("ERROR(S):"); - lines[3].ShouldBeEquivalentTo("Option: 'weburl' is not compatible with: 'ftpurl'."); - lines[4].ShouldBeEquivalentTo("Option: 'ftpurl' is not compatible with: 'weburl'."); - lines[5].ShouldBeEquivalentTo("--weburl Required."); - lines[6].ShouldBeEquivalentTo("--ftpurl Required."); - lines[7].ShouldBeEquivalentTo("-a"); - lines[8].ShouldBeEquivalentTo("--help Display this help screen."); - lines[9].ShouldBeEquivalentTo("--version Display version information."); + 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'."); + lines[5].Should().BeEquivalentTo("--weburl Required."); + lines[6].Should().BeEquivalentTo("--ftpurl Required."); + lines[7].Should().BeEquivalentTo("-a"); + lines[8].Should().BeEquivalentTo("--help Display this help screen."); + lines[9].Should().BeEquivalentTo("--version Display version information."); // Teardown } @@ -490,7 +552,11 @@ public void Properly_formatted_help_screen_is_displayed_when_usage_is_defined_in { // Fixture setup var help = new StringWriter(); - var sut = new Parser(config => config.HelpWriter = help); + var sut = new Parser(config => + { + config.HelpWriter = help; + config.MaximumDisplayWidth = 80; + }); // Exercize system sut.ParseArguments( @@ -499,29 +565,23 @@ 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].ShouldBeEquivalentTo("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[2].ShouldBeEquivalentTo("ERROR(S):"); - lines[3].ShouldBeEquivalentTo("Option 'badoption' is unknown."); - lines[4].ShouldBeEquivalentTo("USAGE:"); - lines[5].ShouldBeEquivalentTo("Basic cloning:"); - lines[6].ShouldBeEquivalentTo("git clone https://github.com/gsscoder/csharpx"); - lines[7].ShouldBeEquivalentTo("Cloning quietly:"); - lines[8].ShouldBeEquivalentTo("git clone --quiet https://github.com/gsscoder/railwaysharp"); - lines[9].ShouldBeEquivalentTo("Cloning without hard links:"); - lines[10].ShouldBeEquivalentTo("git clone --no-hardlinks https://github.com/gsscoder/csharpx"); - lines[11].ShouldBeEquivalentTo("--no-hardlinks Optimize the cloning process from a repository on a local"); - lines[12].ShouldBeEquivalentTo("filesystem by copying files."); - lines[13].ShouldBeEquivalentTo("-q, --quiet Suppress summary message."); - lines[14].ShouldBeEquivalentTo("--help Display this help screen."); - lines[15].ShouldBeEquivalentTo("--version Display version information."); - lines[16].ShouldBeEquivalentTo("URLS (pos. 0) A list of url(s) to clone."); + 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:"); + lines[5].Should().BeEquivalentTo("Basic cloning:"); + lines[6].Should().BeEquivalentTo("git clone https://github.com/gsscoder/csharpx"); + lines[7].Should().BeEquivalentTo("Cloning quietly:"); + lines[8].Should().BeEquivalentTo("git clone --quiet https://github.com/gsscoder/railwaysharp"); + lines[9].Should().BeEquivalentTo("Cloning without hard links:"); + lines[10].Should().BeEquivalentTo("git clone --no-hardlinks https://github.com/gsscoder/csharpx"); + lines[11].Should().BeEquivalentTo("--no-hardlinks Optimize the cloning process from a repository on a local"); + lines[12].Should().BeEquivalentTo("filesystem by copying files."); + lines[13].Should().BeEquivalentTo("-q, --quiet Suppress summary message."); + lines[14].Should().BeEquivalentTo("--help Display this help screen."); + lines[15].Should().BeEquivalentTo("--version Display version information."); + lines[16].Should().BeEquivalentTo("URLS (pos. 0) A list of url(s) to clone."); // Teardown } @@ -539,19 +599,13 @@ 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].ShouldBeEquivalentTo("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[2].ShouldBeEquivalentTo("ERROR(S):"); - lines[3].ShouldBeEquivalentTo("No verb selected."); - lines[4].ShouldBeEquivalentTo("add Add file contents to the index."); - lines[5].ShouldBeEquivalentTo("help Display more information on a specific command."); - lines[6].ShouldBeEquivalentTo("version Display version information."); + 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."); + lines[5].Should().BeEquivalentTo("help Display more information on a specific command."); + lines[6].Should().BeEquivalentTo("version Display version information."); // Teardown } @@ -569,17 +623,11 @@ 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].ShouldBeEquivalentTo("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[2].ShouldBeEquivalentTo("-f, --force Allow adding otherwise ignored files."); - lines[3].ShouldBeEquivalentTo("--help Display this help screen."); - lines[4].ShouldBeEquivalentTo("--version Display version information."); + 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."); // Teardown } @@ -597,10 +645,10 @@ public void Parse_options_when_given_hidden_verb() // Verify outcome - result.Tag.ShouldBeEquivalentTo(ParserResultType.Parsed); + result.Tag.Should().BeEquivalentTo(ParserResultType.Parsed); result.GetType().Should().Be>(); result.TypeInfo.Current.Should().Be(); - ((Parsed)result).Value.ShouldBeEquivalentTo(expectedOptions, o => o.RespectingRuntimeTypes()); + ((Parsed)result).Value.Should().BeEquivalentTo(expectedOptions, o => o.RespectingRuntimeTypes()); // Teardown } @@ -616,10 +664,10 @@ public void Parse_options_when_given_hidden_verb_with_hidden_option() var result = sut.ParseArguments(new string[] { "secert", "--force", "--secert-option", "shhh" }); // Verify outcome - result.Tag.ShouldBeEquivalentTo(ParserResultType.Parsed); + result.Tag.Should().BeEquivalentTo(ParserResultType.Parsed); result.GetType().Should().Be>(); result.TypeInfo.Current.Should().Be(); - ((Parsed)result).Value.ShouldBeEquivalentTo(expectedOptions, o => o.RespectingRuntimeTypes()); + ((Parsed)result).Value.Should().BeEquivalentTo(expectedOptions, o => o.RespectingRuntimeTypes()); // Teardown } @@ -628,7 +676,11 @@ public void Specific_verb_help_screen_should_be_displayed_regardless_other_argum { // Fixture setup var help = new StringWriter(); - var sut = new Parser(config => config.HelpWriter = help); + var sut = new Parser(config => + { + config.HelpWriter = help; + config.MaximumDisplayWidth = 80; + }); // Exercize system sut.ParseArguments( @@ -637,26 +689,20 @@ 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].ShouldBeEquivalentTo("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[2].ShouldBeEquivalentTo("--no-hardlinks Optimize the cloning process from a repository on a local"); - lines[3].ShouldBeEquivalentTo("filesystem by copying files."); - lines[4].ShouldBeEquivalentTo("-q, --quiet Suppress summary message."); - lines[5].ShouldBeEquivalentTo("--help Display this help screen."); - lines[6].ShouldBeEquivalentTo("--version Display version information."); - lines[7].ShouldBeEquivalentTo("value pos. 0"); + 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."); + lines[5].Should().BeEquivalentTo("--help Display this help screen."); + lines[6].Should().BeEquivalentTo("--version Display version information."); + lines[7].Should().BeEquivalentTo("value pos. 0"); // Teardown } [Theory] - [MemberData("IgnoreUnknownArgumentsData")] + [MemberData(nameof(IgnoreUnknownArgumentsData))] public void When_IgnoreUnknownArguments_is_set_valid_unknown_arguments_avoid_a_failure_parsing( string[] arguments, Simple_Options expected) @@ -668,14 +714,14 @@ public void When_IgnoreUnknownArguments_is_set_valid_unknown_arguments_avoid_a_f var result = sut.ParseArguments(arguments); // Verify outcome - result.Tag.ShouldBeEquivalentTo(ParserResultType.Parsed); - result.WithParsed(opts => opts.ShouldBeEquivalentTo(expected)); + result.Tag.Should().BeEquivalentTo(ParserResultType.Parsed); + result.WithParsed(opts => opts.Should().BeEquivalentTo(expected)); // Teardown } [Theory] - [MemberData("IgnoreUnknownArgumentsForVerbsData")] + [MemberData(nameof(IgnoreUnknownArgumentsForVerbsData))] public void When_IgnoreUnknownArguments_is_set_valid_unknown_arguments_avoid_a_failure_parsing_for_verbs( string[] arguments, Commit_Verb expected) @@ -687,8 +733,8 @@ public void When_IgnoreUnknownArguments_is_set_valid_unknown_arguments_avoid_a_f var result = sut.ParseArguments(arguments); // Verify outcome - result.Tag.ShouldBeEquivalentTo(ParserResultType.Parsed); - result.WithParsed(opts => opts.ShouldBeEquivalentTo(expected)); + result.Tag.Should().BeEquivalentTo(ParserResultType.Parsed); + result.WithParsed(opts => opts.Should().BeEquivalentTo(expected)); // Teardown } @@ -698,7 +744,11 @@ public void Properly_formatted_help_screen_excludes_help_as_unknown_option() { // Fixture setup var help = new StringWriter(); - var sut = new Parser(config => config.HelpWriter = help); + var sut = new Parser(config => + { + config.HelpWriter = help; + config.MaximumDisplayWidth = 80; + }); // Exercize system sut.ParseArguments( @@ -707,35 +757,30 @@ 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].ShouldBeEquivalentTo("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[2].ShouldBeEquivalentTo("ERROR(S):"); - lines[3].ShouldBeEquivalentTo("Option 'bad-arg' is unknown."); - lines[4].ShouldBeEquivalentTo("--no-hardlinks Optimize the cloning process from a repository on a local"); - lines[5].ShouldBeEquivalentTo("filesystem by copying files."); - lines[6].ShouldBeEquivalentTo("-q, --quiet Suppress summary message."); - lines[7].ShouldBeEquivalentTo("--help Display this help screen."); - lines[8].ShouldBeEquivalentTo("--version Display version information."); - lines[9].ShouldBeEquivalentTo("value pos. 0"); + 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"); + lines[5].Should().BeEquivalentTo("filesystem by copying files."); + lines[6].Should().BeEquivalentTo("-q, --quiet Suppress summary message."); + lines[7].Should().BeEquivalentTo("--help Display this help screen."); + lines[8].Should().BeEquivalentTo("--version Display version information."); + lines[9].Should().BeEquivalentTo("value pos. 0"); // Teardown } [Fact] - public static void Breaking_mutually_exclusive_set_constraint_with_set_name_with_partial_string_right_side_equality_gererates_MissingValueOptionError() + 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 @@ -743,9 +788,8 @@ public static void Breaking_mutually_exclusive_set_constraint_with_set_name_with new[] { "--weburl", "value", "--somethingelse", "othervalue" }); // Verify outcome - ((NotParsed)result).Errors.ShouldBeEquivalentTo(expectedResult); - - // Teardown + ((NotParsed)result).Errors.Should().BeEquivalentTo(expectedResult); + } [Fact] @@ -768,7 +812,7 @@ public static void Arguments_with_the_different_values_when_unknown_arguments_ar result.MapResult(_ => true, _ => false).Should().BeTrue(); } - public static IEnumerable IgnoreUnknownArgumentsData + public static IEnumerable IgnoreUnknownArgumentsData { get { @@ -778,7 +822,7 @@ public static IEnumerable IgnoreUnknownArgumentsData } } - public static IEnumerable IgnoreUnknownArgumentsForVerbsData + public static IEnumerable IgnoreUnknownArgumentsForVerbsData { get { @@ -794,7 +838,7 @@ public static void Null_default() parser.ParseArguments("".Split()) .WithParsed(r => { - Assert.Equal(null, r.User); + Assert.Null(r.User); }); } @@ -847,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 90ca41b5..9811f7be 100644 --- a/tests/CommandLine.Tests/Unit/Text/HelpTextTests.cs +++ b/tests/CommandLine.Tests/Unit/Text/HelpTextTests.cs @@ -5,72 +5,98 @@ 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.ShouldBeEquivalentTo(new HelpText().ToString()); + 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].ShouldBeEquivalentTo("Unit-tests 2.0"); - lines[1].ShouldBeEquivalentTo("Copyright (C) 2005 - 2013 Author"); - lines[2].ShouldBeEquivalentTo("pre-options line 1"); - lines[3].ShouldBeEquivalentTo("pre-options line 2"); - lines[4].ShouldBeEquivalentTo("post-options line 1"); - lines[5].ShouldBeEquivalentTo("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].ShouldBeEquivalentTo("pre-options"); - lines[1].ShouldBeEquivalentTo("--stringvalue Define a string value here."); - lines[2].ShouldBeEquivalentTo("-s, --shortandlong Example with both short and long name."); - lines[3].ShouldBeEquivalentTo("-i Define a int sequence here."); - lines[4].ShouldBeEquivalentTo("-x Define a boolean or switch value here."); - lines[5].ShouldBeEquivalentTo("--help Display this help screen."); - lines[6].ShouldBeEquivalentTo("--version Display version information."); - lines[7].ShouldBeEquivalentTo("value pos. 0 Define a long value here."); - lines[8].ShouldBeEquivalentTo("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] @@ -78,7 +104,7 @@ public void Create_instance_with_enum_options_enabled() { // Fixture setup // Exercize system - var sut = new HelpText { AddDashesToOption = true, AddEnumValuesToHelpText = true } + 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"); @@ -86,13 +112,13 @@ public void Create_instance_with_enum_options_enabled() // Verify outcome var lines = sut.ToString().ToNotEmptyLines().TrimStringArray(); - lines[0].ShouldBeEquivalentTo("pre-options"); - lines[1].ShouldBeEquivalentTo("--stringvalue Define a string value here."); - lines[2].ShouldBeEquivalentTo("--shape Define a enum value here. Valid values: Circle, Square,"); - lines[3].ShouldBeEquivalentTo("Triangle"); - lines[4].ShouldBeEquivalentTo("--help Display this help screen."); - lines[5].ShouldBeEquivalentTo("--version Display version information."); - lines[6].ShouldBeEquivalentTo("post-options"); + 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 } @@ -109,12 +135,12 @@ public void Create_instance_with_enum_options_disabled() // Verify outcome var lines = sut.ToString().ToNotEmptyLines().TrimStringArray(); - lines[0].ShouldBeEquivalentTo("pre-options"); - lines[1].ShouldBeEquivalentTo("--stringvalue Define a string value here."); - lines[2].ShouldBeEquivalentTo("--shape Define a enum value here."); - lines[3].ShouldBeEquivalentTo("--help Display this help screen."); - lines[4].ShouldBeEquivalentTo("--version Display version information."); - lines[5].ShouldBeEquivalentTo("post-options"); + 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."); + lines[3].Should().BeEquivalentTo("--help Display this help screen."); + lines[4].Should().BeEquivalentTo("--version Display version information."); + lines[5].Should().BeEquivalentTo("post-options"); // Teardown } @@ -130,7 +156,7 @@ public void When_defined_MetaValue_should_be_rendered() // Verify outcome var lines = sut.ToString().ToNotEmptyLines().TrimStringArray(); - lines[2].ShouldBeEquivalentTo("i FILE, input-file=FILE Required. Specify input FILE to be processed."); + lines[2].Should().BeEquivalentTo("i FILE, input-file=FILE Required. Specify input FILE to be processed."); // Teardown } @@ -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( @@ -148,23 +174,21 @@ 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].ShouldBeEquivalentTo(" v, verbose This is the description"); //"The first line should have the arguments and the start of the Help Text."); + 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].ShouldBeEquivalentTo(" of the verbosity to "); - lines[4].ShouldBeEquivalentTo(" test out the wrapping "); - lines[5].ShouldBeEquivalentTo(" capabilities of the "); - lines[6].ShouldBeEquivalentTo(" 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,9 +196,9 @@ 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].ShouldBeEquivalentTo(" 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].ShouldBeEquivalentTo(" the Help Text."); + lines[3].Should().BeEquivalentTo(" the Help Text."); // Teardown } @@ -183,7 +207,8 @@ public void When_help_text_has_hidden_option_it_should_not_be_added_to_help_text { // 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( TypeInfo.Create(typeof(Simple_Options_With_HelpText_Set_To_Long_Description)), @@ -191,9 +216,9 @@ 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].ShouldBeEquivalentTo(" 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].ShouldBeEquivalentTo(" wrapping capabilities of the Help Text."); + lines[3].Should().BeEquivalentTo(" wrapping capabilities of the Help Text."); // Teardown } @@ -202,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( @@ -211,12 +236,12 @@ public void Long_help_text_without_spaces() // Verify outcome var lines = sut.ToString().ToNotEmptyLines(); - lines[1].ShouldBeEquivalentTo(" v, verbose Before "); - lines[2].ShouldBeEquivalentTo(" 012345678901234567890123"); - lines[3].ShouldBeEquivalentTo(" After"); - lines[4].ShouldBeEquivalentTo(" input-file Before "); - lines[5].ShouldBeEquivalentTo(" 012345678901234567890123"); - lines[6].ShouldBeEquivalentTo(" 456789 After"); + 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[5].Should().BeEquivalentTo(" 012345678901234567890123"); + lines[6].Should().BeEquivalentTo(" 456789 After"); // Teardown } @@ -233,20 +258,25 @@ public void Long_pre_and_post_lines_without_spaces() // Verify outcome var lines = sut.ToString().ToNotEmptyLines(); - lines[1].ShouldBeEquivalentTo("Before "); - lines[2].ShouldBeEquivalentTo("0123456789012345678901234567890123456789"); - lines[3].ShouldBeEquivalentTo("012 After"); - lines[lines.Length - 3].ShouldBeEquivalentTo("Before "); - lines[lines.Length - 2].ShouldBeEquivalentTo("0123456789012345678901234567890123456789"); - lines[lines.Length - 1].ShouldBeEquivalentTo(" After"); + 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 - 2].Should().BeEquivalentTo("0123456789012345678901234567890123456789"); + 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[] @@ -259,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; @@ -291,14 +327,14 @@ public void Invoking_RenderParsingErrorsText_returns_appropriate_formatted_text( // Verify outcome var lines = errorsText.ToNotEmptyLines(); - - lines[0].ShouldBeEquivalentTo(" ERR badtoken"); - lines[1].ShouldBeEquivalentTo(" ERR x, switch"); - lines[2].ShouldBeEquivalentTo(" ERR unknown"); - lines[3].ShouldBeEquivalentTo(" ERR missing"); - lines[4].ShouldBeEquivalentTo(" ERR s, sequence"); - lines[5].ShouldBeEquivalentTo(" ERR no-verb-selected"); - lines[6].ShouldBeEquivalentTo(" ERR badverb"); + lines[0].Should().BeEquivalentTo(" ERR badtoken"); + lines[1].Should().BeEquivalentTo(" ERR x, switch"); + lines[2].Should().BeEquivalentTo(" ERR unknown"); + lines[3].Should().BeEquivalentTo(" ERR missing"); + 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 } @@ -318,23 +354,23 @@ 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].ShouldBeEquivalentTo("ERROR(S):"); - lines[3].ShouldBeEquivalentTo("Token 'badtoken' is not recognized."); - lines[4].ShouldBeEquivalentTo("A sequence option 'i' is defined with fewer or more items than required."); - lines[5].ShouldBeEquivalentTo("--stringvalue Define a string value here."); - lines[6].ShouldBeEquivalentTo("-s, --shortandlong Example with both short and long name."); - lines[7].ShouldBeEquivalentTo("-i Define a int sequence here."); - lines[8].ShouldBeEquivalentTo("-x Define a boolean or switch value here."); - lines[9].ShouldBeEquivalentTo("--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 } @@ -353,21 +389,19 @@ 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].ShouldBeEquivalentTo("-p, --patch Use the interactive patch selection interface to chose which"); - lines[3].ShouldBeEquivalentTo("changes to commit."); - lines[4].ShouldBeEquivalentTo("--amend Used to amend the tip of the current branch."); - lines[5].ShouldBeEquivalentTo("-m, --message Use the given message as the commit message."); - lines[6].ShouldBeEquivalentTo("--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 } @@ -383,23 +417,16 @@ 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].ShouldBeEquivalentTo("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[2].ShouldBeEquivalentTo("-p, --patch Use the interactive patch selection interface to chose which changes to commit."); - lines[3].ShouldBeEquivalentTo("--amend Used to amend the tip of the current branch."); - lines[4].ShouldBeEquivalentTo("-m, --message Use the given message as the commit message."); - lines[5].ShouldBeEquivalentTo("--help Display this help screen."); + 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."); + lines[5].Should().BeEquivalentTo("--help Display this help screen."); // Teardown } @@ -420,19 +447,13 @@ 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[2].ShouldBeEquivalentTo("add Add file contents to the index."); - lines[3].ShouldBeEquivalentTo("commit Record changes to the repository."); - lines[4].ShouldBeEquivalentTo("clone Clone a repository into a new directory."); - lines[5].ShouldBeEquivalentTo("help Display more information on a specific command."); - lines[6].ShouldBeEquivalentTo("version Display version information."); + 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."); + lines[5].Should().BeEquivalentTo("help Display more information on a specific command."); + lines[6].Should().BeEquivalentTo("version Display version information."); // Teardown } @@ -441,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].ShouldBeEquivalentTo("pre-options"); - lines[1].ShouldBeEquivalentTo("--stringvalue=STR Define a string value here."); - lines[2].ShouldBeEquivalentTo("-i INTSEQ Define a int sequence here."); - lines[3].ShouldBeEquivalentTo("-x Define a boolean or switch value here."); - lines[4].ShouldBeEquivalentTo("--help Display this help screen."); - lines[5].ShouldBeEquivalentTo("--version Display version information."); - lines[6].ShouldBeEquivalentTo("number (pos. 0) NUM Define a long value here."); - lines[7].ShouldBeEquivalentTo("paintcolor (pos. 1) COLOR Define a color value here."); - lines[8].ShouldBeEquivalentTo("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 } @@ -468,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); @@ -476,21 +500,23 @@ public static void RenderUsageText_returns_properly_formatted_text() var lines = text.ToNotEmptyLines(); // Teardown - lines[0].ShouldBeEquivalentTo("Normal scenario:"); - lines[1].ShouldBeEquivalentTo(" mono testapp.exe --input file.bin --output out.bin"); - lines[2].ShouldBeEquivalentTo("Logging warnings:"); - lines[3].ShouldBeEquivalentTo(" mono testapp.exe -w --input file.bin"); - lines[4].ShouldBeEquivalentTo("Logging errors:"); - lines[5].ShouldBeEquivalentTo(" mono testapp.exe -e --input file.bin"); - lines[6].ShouldBeEquivalentTo(" mono testapp.exe --errs --input=file.bin"); - lines[7].ShouldBeEquivalentTo("List:"); - lines[8].ShouldBeEquivalentTo(" mono testapp.exe -l 1,2"); - lines[9].ShouldBeEquivalentTo("Value:"); - lines[10].ShouldBeEquivalentTo(" mono testapp.exe value"); + lines[0].Should().BeEquivalentTo("Normal scenario:"); + lines[1].Should().BeEquivalentTo(" mono testapp.exe --input file.bin --output out.bin"); + lines[2].Should().BeEquivalentTo("Logging warnings:"); + lines[3].Should().BeEquivalentTo(" mono testapp.exe -w --input file.bin"); + lines[4].Should().BeEquivalentTo("Logging errors:"); + lines[5].Should().BeEquivalentTo(" mono testapp.exe -e --input file.bin"); + lines[6].Should().BeEquivalentTo(" mono testapp.exe --errs --input=file.bin"); + lines[7].Should().BeEquivalentTo("List:"); + lines[8].Should().BeEquivalentTo(" mono testapp.exe -l 1,2"); + lines[9].Should().BeEquivalentTo("Value:"); + 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( @@ -501,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 text = helpText.ToString(); - var lines = text.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].ShouldBeEquivalentTo("ERROR(S):"); - lines[3].ShouldBeEquivalentTo("Token 'badtoken' is not recognized."); - lines[4].ShouldBeEquivalentTo("USAGE:"); - lines[5].ShouldBeEquivalentTo("Normal scenario:"); - lines[6].ShouldBeEquivalentTo("mono testapp.exe --input file.bin --output out.bin"); - lines[7].ShouldBeEquivalentTo("Logging warnings:"); - lines[8].ShouldBeEquivalentTo("mono testapp.exe -w --input file.bin"); - lines[9].ShouldBeEquivalentTo("Logging errors:"); - lines[10].ShouldBeEquivalentTo("mono testapp.exe -e --input file.bin"); - lines[11].ShouldBeEquivalentTo("mono testapp.exe --errs --input=file.bin"); - lines[12].ShouldBeEquivalentTo("List:"); - lines[13].ShouldBeEquivalentTo("mono testapp.exe -l 1,2"); - lines[14].ShouldBeEquivalentTo("Value:"); - lines[15].ShouldBeEquivalentTo("mono testapp.exe value"); - lines[16].ShouldBeEquivalentTo("-i, --input Set input file."); - lines[17].ShouldBeEquivalentTo("-i, --output Set output file."); - lines[18].ShouldBeEquivalentTo("--verbose Set verbosity level."); - lines[19].ShouldBeEquivalentTo("-w, --warns Log warnings."); - lines[20].ShouldBeEquivalentTo("-e, --errs Log errors."); - lines[21].ShouldBeEquivalentTo("-l List."); - lines[22].ShouldBeEquivalentTo("--help Display this help screen."); - lines[23].ShouldBeEquivalentTo("--version Display version information."); - lines[24].ShouldBeEquivalentTo("value pos. 0 Value."); + 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, ""); - // Teardown + var text = helpText.ToString(); + var lines = text.ToLines().TrimStringArray(); + + 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() { @@ -567,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"; - - 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); + string expectedTitle = "Title"; + string expectedVersion = "1.2.3.4"; - 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] @@ -669,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 3d15e2d0..7e878f32 100644 --- a/tests/CommandLine.Tests/Unit/UnParserExtensionsTests.cs +++ b/tests/CommandLine.Tests/Unit/UnParserExtensionsTests.cs @@ -1,53 +1,100 @@ // 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 { [Theory] - [MemberData("UnParseData")] + [MemberData(nameof(UnParseData))] public static void UnParsing_instance_returns_command_line(Simple_Options options, string result) { new Parser() .FormatCommandLine(options) - .ShouldBeEquivalentTo(result); + .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("UnParseDataVerbs")] + [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) { new Parser() .FormatCommandLine(verb) - .ShouldBeEquivalentTo(result); + .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("UnParseDataImmutable")] + [MemberData(nameof(UnParseDataImmutable))] public static void UnParsing_immutable_instance_returns_command_line(Immutable_Simple_Options options, string result) { new Parser() .FormatCommandLine(options) - .ShouldBeEquivalentTo(result); + .Should().BeEquivalentTo(result); + } + + [Theory] + [MemberData(nameof(UnParseDataHidden))] + public static void Unparsing_hidden_option_returns_command_line(Hidden_Option options, bool showHidden, string result) + { + new Parser() + .FormatCommandLine(options, config => config.ShowHidden = showHidden) + .Should().BeEquivalentTo(result); } #if !SKIP_FSHARP [Theory] - [MemberData("UnParseDataFSharpOption")] + [MemberData(nameof(UnParseDataFSharpOption))] public static void UnParsing_instance_with_fsharp_option_returns_command_line(Options_With_FSharpOption options, string result) { new Parser() .FormatCommandLine(options) - .ShouldBeEquivalentTo(result); + .Should().BeEquivalentTo(result); } #endif @@ -57,7 +104,7 @@ public static void UnParsing_instance_with_group_switches_returns_command_line_w var options = new Options_With_Switches { InputFile = "input.bin", HumanReadable = true, IgnoreWarnings = true }; new Parser() .FormatCommandLine(options, config => config.GroupSwitches = true) - .ShouldBeEquivalentTo("-hi --input input.bin"); + .Should().BeEquivalentTo("-hi --input input.bin"); } [Fact] @@ -66,7 +113,7 @@ public static void UnParsing_instance_with_equal_token_returns_command_line_with var options = new Simple_Options { BoolValue = true, IntSequence = new[] { 1, 2, 3 }, StringValue = "nospaces", LongValue = 123456789 }; new Parser() .FormatCommandLine(options, config => config.UseEqualToken = true) - .ShouldBeEquivalentTo("-i 1 2 3 --stringvalue=nospaces -x 123456789"); + .Should().BeEquivalentTo("-i 1 2 3 --stringvalue=nospaces -x 123456789"); } [Fact] @@ -75,7 +122,7 @@ public static void UnParsing_instance_with_dash_in_value_and_dashdash_enabled_re var options = new Simple_Options_With_Values { StringSequence = new List { "-something", "with", "dash" } }; new Parser((setting) => setting.EnableDashDash = true) .FormatCommandLine(options) - .ShouldBeEquivalentTo("-- -something with dash"); + .Should().BeEquivalentTo("-- -something with dash"); } [Fact] @@ -84,7 +131,7 @@ public static void UnParsing_instance_with_no_values_and_dashdash_enabled_return var options = new Simple_Options_With_Values(); new Parser((setting) => setting.EnableDashDash = true) .FormatCommandLine(options) - .ShouldBeEquivalentTo(""); + .Should().BeEquivalentTo(""); } [Fact] @@ -93,10 +140,231 @@ public static void UnParsing_instance_with_dash_in_value_and_dashdash_disabled_r var options = new Simple_Options_With_Values { StringSequence = new List { "-something", "with", "dash" } }; new Parser() .FormatCommandLine(options) - .ShouldBeEquivalentTo("-something with dash"); + .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\".+\""); } - public static IEnumerable UnParseData + [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 { @@ -113,8 +381,17 @@ 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 + public static IEnumerable UnParseDataVerbs { get { @@ -124,25 +401,33 @@ public static IEnumerable UnParseDataVerbs } } - public static IEnumerable UnParseDataImmutable + 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" }; } } + 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, "" }; + } + } #if !SKIP_FSHARP - public static IEnumerable UnParseDataFSharpOption + public static IEnumerable UnParseDataFSharpOption { get { 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 + } + } + } +} diff --git a/tests/CommandLine.Tests/packages.config b/tests/CommandLine.Tests/packages.config deleted file mode 100644 index d6c88f8b..00000000 --- a/tests/CommandLine.Tests/packages.config +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/tests/CommandLine.Tests/paket.references b/tests/CommandLine.Tests/paket.references deleted file mode 100644 index 76fe9421..00000000 --- a/tests/CommandLine.Tests/paket.references +++ /dev/null @@ -1,4 +0,0 @@ -FSharp.Core -FluentAssertions -xunit -xunit.runner.visualstudio diff --git a/tests/CommandLine.Tests/project.json b/tests/CommandLine.Tests/project.json deleted file mode 100644 index ffd3f349..00000000 --- a/tests/CommandLine.Tests/project.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "version": "1.0.0-*", - "description": "CommandLine.DotNet.Tests", - - "dependencies": { - "xunit": "2.1.0", - "CommandLine": { - "target": "project" - } - }, - - "testRunner": "xunit", - - "frameworks": { - "netcoreapp1.0": { - "buildOptions": { - "define": [ "PLATFORM_DOTNET", "SKIP_FSHARP" ], - "keyFile": "../../CommandLine.snk" - }, - "dependencies": { - "Microsoft.NETCore.App": { - "type": "platform", - "version": "1.0.0" - }, - "System.Reflection": "4.1.0", - "System.Reflection.TypeExtensions": "4.1.0", - "FluentAssertions": "4.2.2", - "dotnet-test-xunit": "2.2.0-*" - }, - "imports": [ "dotnet5.4", "dnxcore50", "portable-net451+win81" ] - }, - "net45": { - "frameworkAssemblies": {} - } - }, - "runtimes": { - "win": {} - } -} diff --git a/wrap/CommandLine/project.json b/wrap/CommandLine/project.json deleted file mode 100644 index a6cda444..00000000 --- a/wrap/CommandLine/project.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "version": "1.0.0-*", - "frameworks": { - "netstandard1.3": { - "bin": { - "assembly": "../../src/CommandLine.dotnet/bin/portable-dotnet/{configuration}/CommandLine.dll", - "pdb": "../../src/CommandLine.dotnet/bin/portable-dotnet/{configuration}/CommandLine.pdb" - }, - "dependencies": { - "FSharp.Core": "1.0.0-*" - } - } - } -} \ No newline at end of file diff --git a/wrap/FSharp.Core/project.json b/wrap/FSharp.Core/project.json deleted file mode 100644 index 41c7df67..00000000 --- a/wrap/FSharp.Core/project.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "version": "1.0.0-*", - "frameworks": { - "netstandard1.3": { - "bin": { - "assembly": "../../src/CommandLine.dotnet/bin/portable-dotnet/{configuration}/FSharp.Core.dll" - } - } - } -} \ No newline at end of file