diff --git a/.gitignore b/.gitignore index a7d34775..eb52d2c0 100644 --- a/.gitignore +++ b/.gitignore @@ -39,4 +39,7 @@ artifacts/* .vs/* *.xproj.user *.nuget.targets -*.lock.json \ No newline at end of file +*.lock.json +*.nuget.props +*.DotSettings.user + diff --git a/README.md b/README.md index 4a7a5f7c..f42bb4e9 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ Compatibility: --- - .NET Framework 4.0+ - Mono 2.1+ Profile + - .Net Core - Note: support for Core is currently limited to an [alternate branch](https://github.com/gsscoder/commandline/tree/dotnet-core) while we wait for release of 1.0. If you have suggestions or changes for support of .Net Core, please submit a pull request to that branch until it's merged into master. Current Release: --- @@ -90,17 +91,8 @@ class Options { Consume them: ```csharp static int Main(string[] args) { - var result = CommandLine.Parser.Default.ParseArguments(args); - var exitCode = result - .MapResult( - options = > { - if (options.Verbose) Console.WriteLine("Filenames: {0}", string.Join(",", options.InputFiles.ToArray())); - return 0; }, - errors => { - LogHelper.Log(errors); - return 1; }); - return exitCode; -} + var options = new Options(); + var isValid = CommandLine.Parser.Default.ParseArgumentsStrict(args, options); ``` **F#:** ```fsharp @@ -119,6 +111,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) + + ' Omitting long name, default --verbose + + Public Property Verbose As Boolean + + + Public Property Language As String + + + Public Property Offset As Long? +End Class +``` +Consume them: +```VB.NET +TODO +``` + For verbs: @@ -175,6 +193,25 @@ let main args = | :? CloneOptions as opts -> RunCloneAndReturnExitCode opts | :? CommandLine.NotParsed -> 1 ``` +**VB.NET:** +```VB.NET + +Public Class AddOptions + 'Normal options here +End Class + +Public Class AddOptions + 'Normal options here +End Class + +Public Class AddOptions + 'Normal options here +End Class + +Public Shared Sub Main() + 'TODO +End Sub +``` Acknowledgements: --- diff --git a/src/CommandLine/BaseAttribute.cs b/src/CommandLine/BaseAttribute.cs index a009111d..f91a17a2 100644 --- a/src/CommandLine/BaseAttribute.cs +++ b/src/CommandLine/BaseAttribute.cs @@ -123,5 +123,14 @@ public string MetaValue metaValue = value; } } + + /// + /// Gets or sets a value indicating whether a command line option is visible in the help text. + /// + public bool Hidden + { + get; + set; + } } } diff --git a/src/CommandLine/Core/OptionSpecification.cs b/src/CommandLine/Core/OptionSpecification.cs index e519ffb9..0bbbbb06 100644 --- a/src/CommandLine/Core/OptionSpecification.cs +++ b/src/CommandLine/Core/OptionSpecification.cs @@ -16,8 +16,8 @@ sealed class OptionSpecification : Specification 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) - : base(SpecificationType.Option, required, min, max, defaultValue, helpText, metaValue, enumValues, conversionType, targetType) + Type conversionType, TargetType targetType, bool hidden = false) + : base(SpecificationType.Option, required, min, max, defaultValue, helpText, metaValue, enumValues, conversionType, targetType, hidden) { this.shortName = shortName; this.longName = longName; @@ -40,13 +40,14 @@ public static OptionSpecification FromAttribute(OptionAttribute attribute, Type attribute.MetaValue, enumValues, conversionType, - conversionType.ToTargetType()); + conversionType.ToTargetType(), + attribute.Hidden); } - public static OptionSpecification NewSwitch(string shortName, string longName, bool required, string helpText, string metaValue) + 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); + '\0', Maybe.Nothing(), helpText, metaValue, Enumerable.Empty(), typeof(bool), TargetType.Switch, hidden); } public string ShortName diff --git a/src/CommandLine/Core/Specification.cs b/src/CommandLine/Core/Specification.cs index a0a46f7e..9b267741 100644 --- a/src/CommandLine/Core/Specification.cs +++ b/src/CommandLine/Core/Specification.cs @@ -25,6 +25,7 @@ abstract class Specification { private readonly SpecificationType tag; private readonly bool required; + private readonly bool hidden; private readonly Maybe min; private readonly Maybe max; private readonly Maybe defaultValue; @@ -37,7 +38,7 @@ abstract class Specification protected Specification(SpecificationType tag, bool required, Maybe min, Maybe max, Maybe defaultValue, string helpText, string metaValue, IEnumerable enumValues, - Type conversionType, TargetType targetType) + Type conversionType, TargetType targetType, bool hidden = false) { this.tag = tag; this.required = required; @@ -49,6 +50,7 @@ protected Specification(SpecificationType tag, bool required, Maybe min, Ma this.helpText = helpText; this.metaValue = metaValue; this.enumValues = enumValues; + this.hidden = hidden; } public SpecificationType Tag @@ -101,6 +103,11 @@ public TargetType TargetType get { return targetType; } } + public bool Hidden + { + get { return hidden; } + } + public static Specification FromProperty(PropertyInfo property) { var attrs = property.GetCustomAttributes(true); diff --git a/src/CommandLine/Core/SpecificationExtensions.cs b/src/CommandLine/Core/SpecificationExtensions.cs index d8a30bb4..5f77a5dd 100644 --- a/src/CommandLine/Core/SpecificationExtensions.cs +++ b/src/CommandLine/Core/SpecificationExtensions.cs @@ -33,7 +33,8 @@ public static OptionSpecification WithLongName(this OptionSpecification specific specification.MetaValue, specification.EnumValues, specification.ConversionType, - specification.TargetType); + specification.TargetType, + specification.Hidden); } public static string UniqueName(this OptionSpecification specification) diff --git a/src/CommandLine/Core/Tokenizer.cs b/src/CommandLine/Core/Tokenizer.cs index 902b65e4..e0e09fd3 100644 --- a/src/CommandLine/Core/Tokenizer.cs +++ b/src/CommandLine/Core/Tokenizer.cs @@ -6,6 +6,7 @@ using CommandLine.Infrastructure; using CSharpx; using RailwaySharp.ErrorHandling; +using System.Text.RegularExpressions; namespace CommandLine.Core { @@ -188,9 +189,19 @@ private static IEnumerable TokenizeLongName( onError(new BadFormatTokenError(value)); yield break; } - var parts = text.Split('='); - yield return Token.Name(parts[0]); - yield return Token.Value(parts[1], true); + + var tokenMatch = Regex.Match(text, "^([^=]+)=([^ ].*)$"); + + if (tokenMatch.Success) + { + yield return Token.Name(tokenMatch.Groups[1].Value); + yield return Token.Value(tokenMatch.Groups[2].Value, true); + } + else + { + onError(new BadFormatTokenError(value)); + yield break; + } } } } diff --git a/src/CommandLine/Core/TypeConverter.cs b/src/CommandLine/Core/TypeConverter.cs index 2cec8ada..ad2b7d81 100644 --- a/src/CommandLine/Core/TypeConverter.cs +++ b/src/CommandLine/Core/TypeConverter.cs @@ -84,7 +84,7 @@ private static Result ChangeTypeScalarImpl(string value, Type return (value == null) ? empty() : withValue(); }; - return value.IsBooleanString() + return value.IsBooleanString() && conversionType == typeof(bool) ? value.ToBoolean() : conversionType.GetTypeInfo().IsEnum ? value.ToEnum(conversionType, ignoreValueCase) : safeChangeType(); }; diff --git a/src/CommandLine/Core/ValueSpecification.cs b/src/CommandLine/Core/ValueSpecification.cs index d898b6db..bd90252e 100644 --- a/src/CommandLine/Core/ValueSpecification.cs +++ b/src/CommandLine/Core/ValueSpecification.cs @@ -13,8 +13,8 @@ sealed class ValueSpecification : Specification public ValueSpecification(int index, string metaName, bool required, Maybe min, Maybe max, Maybe defaultValue, string helpText, string metaValue, IEnumerable enumValues, - Type conversionType, TargetType targetType) - : base(SpecificationType.Value, required, min, max, defaultValue, helpText, metaValue, enumValues, conversionType, targetType) + Type conversionType, TargetType targetType, bool hidden = false) + : base(SpecificationType.Value, required, min, max, defaultValue, helpText, metaValue, enumValues, conversionType, targetType, hidden) { this.index = index; this.metaName = metaName; @@ -33,7 +33,8 @@ public static ValueSpecification FromAttribute(ValueAttribute attribute, Type co attribute.MetaValue, enumValues, conversionType, - conversionType.ToTargetType()); + conversionType.ToTargetType(), + attribute.Hidden); } public int Index diff --git a/src/CommandLine/Core/Verb.cs b/src/CommandLine/Core/Verb.cs index 9fa126d3..880d4d4e 100644 --- a/src/CommandLine/Core/Verb.cs +++ b/src/CommandLine/Core/Verb.cs @@ -13,14 +13,16 @@ sealed class Verb { private readonly string name; private readonly string helpText; + private readonly bool hidden; - public Verb(string name, string helpText) + public Verb(string name, string helpText, bool hidden = false) { if (name == null) throw new ArgumentNullException("name"); if (helpText == null) throw new ArgumentNullException("helpText"); this.name = name; this.helpText = helpText; + this.hidden = hidden; } public string Name @@ -33,11 +35,17 @@ public string HelpText get { return helpText; } } + public bool Hidden + { + get { return hidden; } + } + public static Verb FromAttribute(VerbAttribute attribute) { return new Verb( attribute.Name, - attribute.HelpText + attribute.HelpText, + attribute.Hidden ); } diff --git a/src/CommandLine/Error.cs b/src/CommandLine/Error.cs index 9739a18a..475ac8e3 100644 --- a/src/CommandLine/Error.cs +++ b/src/CommandLine/Error.cs @@ -157,6 +157,11 @@ public abstract class TokenError : Error, IEquatable { private readonly string token; + /// + /// Initializes a new instance of the class. + /// + /// Error type. + /// Problematic token. protected internal TokenError(ErrorType tag, string token) : base(tag) { @@ -232,6 +237,12 @@ public abstract class NamedError : Error, IEquatable { private readonly NameInfo nameInfo; + /// + /// Initializes a new instance of the class. + /// + /// Error type. + /// Problematic name. + protected internal NamedError(ErrorType tag, NameInfo nameInfo) : base(tag) { diff --git a/src/CommandLine/Infrastructure/ReflectionHelper.cs b/src/CommandLine/Infrastructure/ReflectionHelper.cs index 97aac26f..bec7d470 100644 --- a/src/CommandLine/Infrastructure/ReflectionHelper.cs +++ b/src/CommandLine/Infrastructure/ReflectionHelper.cs @@ -1,8 +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 System.Reflection; +using System.Runtime.Remoting.Messaging; using CommandLine.Core; using CSharpx; @@ -10,9 +12,46 @@ namespace CommandLine.Infrastructure { static class ReflectionHelper { + /// + /// Per thread assembly attribute overrides for testing. + /// + [ThreadStatic] private static IDictionary _overrides; + + /// + /// Assembly attribute overrides for testing. + /// + /// + /// The implementation will fail if two or more attributes of the same type + /// are included in . + /// + /// + /// Attributes that replace the existing assembly attributes or null, + /// to clear any testing attributes. + /// + public static void SetAttributeOverride(IEnumerable overrides) + { + if (overrides != null) + { + _overrides = overrides.ToDictionary(attr => attr.GetType(), attr => attr); + } + else + { + _overrides = null; + } + } + public static Maybe GetAttribute() where TAttribute : Attribute { + // Test support + if (_overrides != null) + { + return + _overrides.ContainsKey(typeof(TAttribute)) ? + Maybe.Just((TAttribute)_overrides[typeof(TAttribute)]) : + Maybe.Nothing< TAttribute>(); + } + var assembly = GetExecutingOrEntryAssembly(); #if NET40 diff --git a/src/CommandLine/Parser.cs b/src/CommandLine/Parser.cs index f2cae77b..69a7e862 100644 --- a/src/CommandLine/Parser.cs +++ b/src/CommandLine/Parser.cs @@ -187,15 +187,16 @@ private static ParserResult MakeParserResult(ParserResult parserResult, { return DisplayHelp( parserResult, - settings.HelpWriter); + settings.HelpWriter, + settings.MaximumDisplayWidth); } - private static ParserResult DisplayHelp(ParserResult parserResult, TextWriter helpWriter) + private static ParserResult DisplayHelp(ParserResult parserResult, TextWriter helpWriter, int maxDisplayWidth) { parserResult.WithNotParsed( errors => Maybe.Merge(errors.ToMaybe(), helpWriter.ToMaybe()) - .Do((_, writer) => writer.Write(HelpText.AutoBuild(parserResult))) + .Do((_, writer) => writer.Write(HelpText.AutoBuild(parserResult, maxDisplayWidth))) ); return parserResult; diff --git a/src/CommandLine/ParserSettings.cs b/src/CommandLine/ParserSettings.cs index 1777f05f..5e2eb895 100644 --- a/src/CommandLine/ParserSettings.cs +++ b/src/CommandLine/ParserSettings.cs @@ -13,6 +13,8 @@ namespace CommandLine /// public class ParserSettings : IDisposable { + private const int DefaultMaximumLength = 80; // default console width + private bool disposed; private bool caseSensitive; private bool caseInsensitiveEnumValues; @@ -20,6 +22,7 @@ public class ParserSettings : IDisposable private bool ignoreUnknownArguments; private CultureInfo parsingCulture; private bool enableDashDash; + private int maximumDisplayWidth; /// /// Initializes a new instance of the class. @@ -29,6 +32,14 @@ public ParserSettings() caseSensitive = true; caseInsensitiveEnumValues = false; parsingCulture = CultureInfo.InvariantCulture; + try + { + maximumDisplayWidth = Console.WindowWidth; + } + catch (IOException) + { + maximumDisplayWidth = DefaultMaximumLength; + } } /// @@ -114,6 +125,15 @@ public bool EnableDashDash set { PopsicleSetter.Set(Consumed, ref enableDashDash, value); } } + /// + /// Gets or sets the maximum width of the display. This determines word wrap when displaying the text. + /// + public int MaximumDisplayWidth + { + get { return maximumDisplayWidth; } + set { maximumDisplayWidth = value; } + } + internal StringComparer NameComparer { get diff --git a/src/CommandLine/Text/CopyrightInfo.cs b/src/CommandLine/Text/CopyrightInfo.cs index 8db2a532..3df9e630 100644 --- a/src/CommandLine/Text/CopyrightInfo.cs +++ b/src/CommandLine/Text/CopyrightInfo.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.Dynamic; using System.Globalization; using System.Reflection; using System.Text; @@ -24,6 +25,17 @@ public class CopyrightInfo private readonly string author; private readonly int builderSize; + /// + /// An empty object used for initialization. + /// + public static CopyrightInfo Empty + { + get + { + return new CopyrightInfo("author", 1); + } + } + /// /// Initializes a new instance of the class /// specifying author and year. diff --git a/src/CommandLine/Text/HeadingInfo.cs b/src/CommandLine/Text/HeadingInfo.cs index dfe95edd..286a1872 100644 --- a/src/CommandLine/Text/HeadingInfo.cs +++ b/src/CommandLine/Text/HeadingInfo.cs @@ -33,6 +33,17 @@ public HeadingInfo(string programName, string version = null) this.version = version; } + /// + /// An empty object used for initialization. + /// + public static HeadingInfo Empty + { + get + { + return new HeadingInfo(""); + } + } + /// /// Gets the default heading instance. /// The title is retrieved from , diff --git a/src/CommandLine/Text/HelpText.cs b/src/CommandLine/Text/HelpText.cs index 9a4803f8..68f7af3b 100644 --- a/src/CommandLine/Text/HelpText.cs +++ b/src/CommandLine/Text/HelpText.cs @@ -3,6 +3,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.IO; using System.Text; using System.Linq; using System.Reflection; @@ -23,7 +24,7 @@ public class HelpText private readonly StringBuilder preOptionsHelp; private readonly StringBuilder postOptionsHelp; private readonly SentenceBuilder sentenceBuilder; - private int? maximumDisplayWidth; + private int maximumDisplayWidth; private string heading; private string copyright; private bool additionalNewLineAfterOption; @@ -101,7 +102,14 @@ public HelpText(SentenceBuilder sentenceBuilder, string heading, string copyrigh preOptionsHelp = new StringBuilder(BuilderCapacity); postOptionsHelp = new StringBuilder(BuilderCapacity); - + try + { + maximumDisplayWidth = Console.WindowWidth; + } + catch (IOException) + { + maximumDisplayWidth = DefaultMaximumLength; + } this.sentenceBuilder = sentenceBuilder; this.heading = heading; this.copyright = copyright; @@ -143,7 +151,7 @@ public string Copyright /// The maximum width of the display. public int MaximumDisplayWidth { - get { return maximumDisplayWidth.HasValue ? maximumDisplayWidth.Value : DefaultMaximumLength; } + get { return maximumDisplayWidth; } set { maximumDisplayWidth = value; } } @@ -193,20 +201,34 @@ public SentenceBuilder SentenceBuilder /// A delegate used to customize the text block of reporting parsing errors text block. /// A delegate used to customize model used to render text block of usage examples. /// If true the output style is consistent with verb commands (no dashes), otherwise it outputs options. + /// The maximum width of the display. /// The parameter is not ontly a metter of formatting, it controls whether to handle verbs or options. public static HelpText AutoBuild( ParserResult parserResult, Func onError, Func onExample, - bool verbsIndex = false) + bool verbsIndex = false, + int maxDisplayWidth = DefaultMaximumLength) { - var auto = new HelpText { - Heading = HeadingInfo.Default, - Copyright = CopyrightInfo.Default, + var auto = new HelpText + { + Heading = HeadingInfo.Empty, + Copyright = CopyrightInfo.Empty, AdditionalNewLineAfterOption = true, - AddDashesToOption = !verbsIndex + AddDashesToOption = !verbsIndex, + MaximumDisplayWidth = maxDisplayWidth }; + try + { + auto.Heading = HeadingInfo.Default; + auto.Copyright = CopyrightInfo.Default; + } + catch (Exception) + { + auto = onError(auto); + } + var errors = Enumerable.Empty(); if (onError != null && parserResult.Tag == ParserResultType.NotParsed) @@ -253,12 +275,13 @@ public static HelpText AutoBuild( /// automatically handling verbs or options scenario. /// /// The containing the instance that collected command line arguments parsed with class. + /// 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) + public static HelpText AutoBuild(ParserResult parserResult, int maxDisplayWidth = DefaultMaximumLength) { if (parserResult.Tag != ParserResultType.NotParsed) throw new ArgumentException("Excepting NotParsed type.", "parserResult"); @@ -266,16 +289,16 @@ public static HelpText AutoBuild(ParserResult parserResult) var errors = ((NotParsed)parserResult).Errors; if (errors.Any(e => e.Tag == ErrorType.VersionRequestedError)) - return new HelpText(HeadingInfo.Default).AddPreOptionsLine(Environment.NewLine); + return new HelpText(HeadingInfo.Default){MaximumDisplayWidth = maxDisplayWidth }.AddPreOptionsLine(Environment.NewLine); if (!errors.Any(e => e.Tag == ErrorType.HelpVerbRequestedError)) - return AutoBuild(parserResult, current => DefaultParsingErrorsHandler(parserResult, current), e => e); + return AutoBuild(parserResult, current => DefaultParsingErrorsHandler(parserResult, current), e => e, maxDisplayWidth: maxDisplayWidth); var err = errors.OfType().Single(); var pr = new NotParsed(TypeInfo.Create(err.Type), Enumerable.Empty()); return err.Matched - ? AutoBuild(pr, current => DefaultParsingErrorsHandler(pr, current), e => e) - : AutoBuild(parserResult, current => DefaultParsingErrorsHandler(parserResult, current), e => e, true); + ? AutoBuild(pr, current => DefaultParsingErrorsHandler(pr, current), e => e, maxDisplayWidth: maxDisplayWidth) + : AutoBuild(parserResult, current => DefaultParsingErrorsHandler(parserResult, current), e => e, true, maxDisplayWidth); } /// @@ -674,7 +697,8 @@ private IEnumerable AdaptVerbsToSpecifications(IEnumerable verbTuple.Item1.Name, false, verbTuple.Item1.HelpText, - string.Empty)).Concat(new[] { MakeHelpEntry(), MakeVersionEntry() }); + string.Empty, + verbTuple.Item1.Hidden)).Concat(new[] { MakeHelpEntry(), MakeVersionEntry() }); } private HelpText AddOptionsImpl( @@ -702,7 +726,8 @@ private OptionSpecification MakeHelpEntry() "help", false, sentenceBuilder.HelpCommandText(AddDashesToOption), - string.Empty); + string.Empty, + false); } private OptionSpecification MakeVersionEntry() @@ -712,7 +737,8 @@ private OptionSpecification MakeVersionEntry() "version", false, sentenceBuilder.VersionCommandText(AddDashesToOption), - string.Empty); + string.Empty, + false); } private HelpText AddPreOptionsLine(string value, int maximumLength) @@ -724,6 +750,9 @@ private HelpText AddPreOptionsLine(string value, int maximumLength) private HelpText AddOption(string requiredWord, int maxLength, Specification specification, int widthOfHelpText) { + if (specification.Hidden) + return this; + optionsHelp.Append(" "); var name = new StringBuilder(maxLength) .BimapIf( @@ -832,13 +861,15 @@ private int GetMaxLength(IEnumerable specifications) { return specifications.Aggregate(0, (length, spec) => - { - var specLength = spec.Tag == SpecificationType.Option + { + if (spec.Hidden) + return length; + var specLength = spec.Tag == SpecificationType.Option ? GetMaxOptionLength((OptionSpecification)spec) : GetMaxValueLength((ValueSpecification)spec); - return Math.Max(length, specLength); - }); + return Math.Max(length, specLength); + }); } diff --git a/src/CommandLine/VerbAttribute.cs b/src/CommandLine/VerbAttribute.cs index 3172f210..af66ac12 100644 --- a/src/CommandLine/VerbAttribute.cs +++ b/src/CommandLine/VerbAttribute.cs @@ -34,6 +34,15 @@ public string Name get { return name; } } + /// + /// Gets or sets a value indicating whether a command line verb is visible in the help text. + /// + public bool Hidden + { + get; + set; + } + /// /// Gets or sets a short description of this command line option. Usually a sentence summary. /// diff --git a/src/CommandLine/project.json b/src/CommandLine/project.json index 11eafe2e..a8a53bcc 100644 --- a/src/CommandLine/project.json +++ b/src/CommandLine/project.json @@ -41,7 +41,8 @@ "System.Linq": "4.1.0", "System.Diagnostics.Debug": "4.0.11", "System.Console": "4.0.0", - "System.Reflection.Extensions": "4.0.1" + "System.Reflection.Extensions": "4.0.1", + "System.Text.RegularExpressions": "4.1.0" }, "imports": [ "netcore45" ] }, @@ -55,4 +56,4 @@ "frameworkAssemblies": { } } } -} \ No newline at end of file +} diff --git a/tests/CommandLine.Tests/CommandLine.Tests.csproj b/tests/CommandLine.Tests/CommandLine.Tests.csproj index f3c6925b..dfcc5c1e 100644 --- a/tests/CommandLine.Tests/CommandLine.Tests.csproj +++ b/tests/CommandLine.Tests/CommandLine.Tests.csproj @@ -112,6 +112,7 @@ Code + diff --git a/tests/CommandLine.Tests/Fakes/Help_Fakes.cs b/tests/CommandLine.Tests/Fakes/Help_Fakes.cs index 8b7af30a..cceb5331 100644 --- a/tests/CommandLine.Tests/Fakes/Help_Fakes.cs +++ b/tests/CommandLine.Tests/Fakes/Help_Fakes.cs @@ -12,6 +12,9 @@ class Simple_Options_Without_HelpText [Option("input-file")] public string FileName { get; set; } + + [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; } } class Simple_Options_With_HelpText_Set @@ -21,6 +24,9 @@ class Simple_Options_With_HelpText_Set [Option('i', "input-file", Required = true, HelpText = "Specify input file to be processed.")] public string FileName { get; set; } + + [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; } } class Simple_Options_With_HelpText_Set_To_Long_Description @@ -30,6 +36,9 @@ class Simple_Options_With_HelpText_Set_To_Long_Description [Option("input-file", HelpText = "This is a very long description of the Input File argument that gets passed in. It should be passed in as a string.")] public string FileName { get; set; } + + [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; } } class Simple_Options_With_HelpText_Set_To_Long_Description_Without_Spaces @@ -39,6 +48,9 @@ class Simple_Options_With_HelpText_Set_To_Long_Description_Without_Spaces [Option("input-file", HelpText = "Before 012345678901234567890123456789 After")] public string FileName { get; set; } + + [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; } } class Options_With_Usage_Attribute @@ -64,6 +76,9 @@ class Options_With_Usage_Attribute [Value(0, HelpText = "Value.")] public string Value { get; set; } + [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")] public static IEnumerable Examples { @@ -78,6 +93,16 @@ public static IEnumerable Examples } } + [Verb("secert", Hidden = true, HelpText = "This is a secert hidden verb that should never be visible to the user via help text.")] + public class Secert_Verb + { + [Option('f', "force", SetName = "mode-f", HelpText = "Allow adding otherwise ignored files.")] + public bool Force { get; set; } + + [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; } + } + [Verb("add", HelpText = "Add file contents to the index.")] public class Add_Verb_With_Usage_Attribute { @@ -92,6 +117,9 @@ public class Add_Verb_With_Usage_Attribute [Value(0)] public string FileName { get; set; } + [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 = "git")] public static IEnumerable Examples { @@ -112,6 +140,9 @@ public class Commit_Verb_With_Usage_Attribute [Option("amend", HelpText = "Used to amend the tip of the current branch.")] public bool Amend { get; set; } + [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 = "git")] public static IEnumerable Examples { @@ -133,6 +164,9 @@ public class Clone_Verb_With_Usage_Attribute HelpText = "Suppress summary message.")] public bool Quiet { get; set; } + [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; } + [Value(0, MetaName = "URLS", HelpText = "A list of url(s) to clone.")] public IEnumerable Urls { get; set; } diff --git a/tests/CommandLine.Tests/Unit/Core/TokenizerTests.cs b/tests/CommandLine.Tests/Unit/Core/TokenizerTests.cs index 64d8eaf1..08aae057 100644 --- a/tests/CommandLine.Tests/Unit/Core/TokenizerTests.cs +++ b/tests/CommandLine.Tests/Unit/Core/TokenizerTests.cs @@ -91,6 +91,41 @@ public void Normalize_should_remove_all_value_with_explicit_assignment_of_existi // 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 = Tokenizer.Tokenize(args, name => NameLookupResult.OtherOptionFound, token => token); + + 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= fail" }; + + var result = Tokenizer.Tokenize(args, name => NameLookupResult.OtherOptionFound, token => token); + + var tokens = result.SuccessfulMessages(); + + Assert.NotNull(tokens); + Assert.Equal(2, tokens.Count()); + Assert.Equal(ErrorType.BadFormatTokenError, tokens.First().Tag); + Assert.Equal(ErrorType.BadFormatTokenError, tokens.Last().Tag); + } } } diff --git a/tests/CommandLine.Tests/Unit/Core/TypeConverterTests.cs b/tests/CommandLine.Tests/Unit/Core/TypeConverterTests.cs new file mode 100644 index 00000000..7642a80a --- /dev/null +++ b/tests/CommandLine.Tests/Unit/Core/TypeConverterTests.cs @@ -0,0 +1,111 @@ +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; + +namespace CommandLine.Tests.Unit.Core +{ + public class TypeConverterTests + { + enum TestEnum + { + ValueA = 1, + ValueB = 2 + } + + [Theory] + [MemberData("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); + + if (expectFail) + { + result.MatchNothing().Should().BeTrue("should fail parsing"); + } + else + { + object matchedValue; + + result.MatchJust(out matchedValue).Should().BeTrue("should parse successfully"); + Assert.Equal(matchedValue, expectedResult); + } + } + + public static IEnumerable ChangeType_scalars_source + { + get + { + return new[] + { + new object[] {"1", typeof (int), false, 1}, + new object[] {"0", typeof (int), false, 0}, + new object[] {"-1", typeof (int), false, -1}, + new object[] {"abcd", typeof (int), true, null}, + new object[] {"1.0", typeof (int), true, null}, + new object[] {int.MaxValue.ToString(), typeof (int), false, int.MaxValue}, + new object[] {int.MinValue.ToString(), typeof (int), false, int.MinValue}, + new object[] {((long) int.MaxValue + 1).ToString(), typeof (int), true, null}, + 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[] {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}, + new object[] {((long) uint.MinValue - 1).ToString(), typeof (uint), true, null}, + + new object[] {"true", typeof (bool), false, true}, + new object[] {"True", typeof (bool), false, true}, + new object[] {"TRUE", typeof (bool), false, true}, + new object[] {"false", typeof (bool), false, false}, + new object[] {"False", typeof (bool), false, false}, + new object[] {"FALSE", typeof (bool), false, false}, + new object[] {"abcd", typeof (bool), true, null}, + new object[] {"0", typeof (bool), true, null}, + new object[] {"1", typeof (bool), true, null}, + + new object[] {"1.0", typeof (float), false, 1.0f}, + new object[] {"0.0", typeof (float), false, 0.0f}, + new object[] {"-1.0", typeof (float), false, -1.0f}, + new object[] {"abcd", typeof (float), true, null}, + + new object[] {"1.0", typeof (double), false, 1.0}, + new object[] {"0.0", typeof (double), false, 0.0}, + new object[] {"-1.0", typeof (double), false, -1.0}, + new object[] {"abcd", typeof (double), true, null}, + + new object[] {"1.0", typeof (decimal), false, 1.0m}, + new object[] {"0.0", typeof (decimal), false, 0.0m}, + new object[] {"-1.0", typeof (decimal), false, -1.0m}, + new object[] {"-1.123456", typeof (decimal), false, -1.123456m}, + new object[] {"abcd", typeof (decimal), true, null}, + + new object[] {"", typeof (string), false, ""}, + new object[] {"abcd", typeof (string), false, "abcd"}, + + new object[] {"ValueA", typeof (TestEnum), false, TestEnum.ValueA}, + new object[] {"VALUEA", typeof (TestEnum), false, TestEnum.ValueA}, + new object[] {"ValueB", typeof(TestEnum), false, TestEnum.ValueB}, + new object[] {((int) TestEnum.ValueA).ToString(), typeof (TestEnum), false, TestEnum.ValueA}, + new object[] {((int) TestEnum.ValueB).ToString(), typeof (TestEnum), false, TestEnum.ValueB}, + new object[] {((int) TestEnum.ValueB + 1).ToString(), typeof (TestEnum), true, null}, + new object[] {((int) TestEnum.ValueA - 1).ToString(), typeof (TestEnum), true, null}, + + // Failed before #339 + new object[] {"false", typeof (int), true, 0}, + new object[] {"true", typeof (int), true, 0} + }; + } + } + } +} diff --git a/tests/CommandLine.Tests/Unit/ParserTests.cs b/tests/CommandLine.Tests/Unit/ParserTests.cs index 1a0ce1b4..5e15d61b 100644 --- a/tests/CommandLine.Tests/Unit/ParserTests.cs +++ b/tests/CommandLine.Tests/Unit/ParserTests.cs @@ -526,6 +526,103 @@ public void Properly_formatted_help_screen_is_displayed_when_usage_is_defined_in // Teardown } + [Fact] + public void Properly_formatted_help_screen_is_displayed_when_there_is_a_hidden_verb() + { + // 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.ToNotEmptyLines().TrimStringArray(); +#if !PLATFORM_DOTNET + lines[0].Should().StartWithEquivalent("CommandLine"); + lines[1].ShouldBeEquivalentTo("Copyright (c) 2005 - 2015 Giacomo Stelluti Scala"); +#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."); + + // Teardown + } + + [Fact] + public void Properly_formatted_help_screen_is_displayed_when_there_is_a_hidden_verb_selected_usage_displays_with_hidden_option() + { + // Fixture setup + var help = new StringWriter(); + var sut = new Parser(config => config.HelpWriter = help); + + // Exercize system + sut.ParseArguments(new string[] { "secert", "--help" }); + var result = help.ToString(); + + // Verify outcome + var lines = result.ToNotEmptyLines().TrimStringArray(); +#if !PLATFORM_DOTNET + lines[0].Should().StartWithEquivalent("CommandLine"); + lines[1].ShouldBeEquivalentTo("Copyright (c) 2005 - 2015 Giacomo Stelluti Scala"); +#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."); + + // Teardown + } + + [Fact] + public void Parse_options_when_given_hidden_verb() + { + // Fixture setup + var expectedOptions = new Secert_Verb { Force = true, SecertOption = null}; + var help = new StringWriter(); + var sut = new Parser(config => config.HelpWriter = help); + + // Exercize system + var result = sut.ParseArguments(new string[] { "secert", "--force" }); + + + // Verify outcome + result.Tag.ShouldBeEquivalentTo(ParserResultType.Parsed); + result.GetType().Should().Be>(); + result.TypeInfo.Current.Should().Be(); + ((Parsed)result).Value.ShouldBeEquivalentTo(expectedOptions, o => o.RespectingRuntimeTypes()); + // Teardown + } + + [Fact] + public void Parse_options_when_given_hidden_verb_with_hidden_option() + { + // Fixture setup + var expectedOptions = new Secert_Verb { Force = true, SecertOption = "shhh" }; + var help = new StringWriter(); + var sut = new Parser(config => config.HelpWriter = help); + + // Exercize system + var result = sut.ParseArguments(new string[] { "secert", "--force", "--secert-option", "shhh" }); + + // Verify outcome + result.Tag.ShouldBeEquivalentTo(ParserResultType.Parsed); + result.GetType().Should().Be>(); + result.TypeInfo.Current.Should().Be(); + ((Parsed)result).Value.ShouldBeEquivalentTo(expectedOptions, o => o.RespectingRuntimeTypes()); + // Teardown + } + [Fact] public void Specific_verb_help_screen_should_be_displayed_regardless_other_argument() { diff --git a/tests/CommandLine.Tests/Unit/Text/HelpTextTests.cs b/tests/CommandLine.Tests/Unit/Text/HelpTextTests.cs index ca133501..6d7e8099 100644 --- a/tests/CommandLine.Tests/Unit/Text/HelpTextTests.cs +++ b/tests/CommandLine.Tests/Unit/Text/HelpTextTests.cs @@ -4,8 +4,11 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; +using System.Reflection; using CommandLine.Core; +using CommandLine.Infrastructure; using CommandLine.Tests.Fakes; +using CommandLine.Tests.Unit.Infrastructure; using CommandLine.Text; using FluentAssertions; using Xunit; @@ -131,7 +134,7 @@ public void When_defined_MetaValue_should_be_rendered() } [Fact] - public void When_help_text_is_longer_than_width_it_will_wrap_around_as_if_in_a_column() + public void When_help_text_is_longer_than_width_it_will_wrap_around_as_if_in_a_column_given_width_of_40() { // Fixture setup // Exercize system @@ -152,6 +155,46 @@ public void When_help_text_is_longer_than_width_it_will_wrap_around_as_if_in_a_c lines[6].ShouldBeEquivalentTo(" Help 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} ; + sut.AddOptions( + new NotParsed( + TypeInfo.Create(typeof(Simple_Options_With_HelpText_Set_To_Long_Description)), + Enumerable.Empty())); + + // 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."); + //string formattingMessage = "Beyond the second line should be formatted as though it's in a column."; + lines[3].ShouldBeEquivalentTo(" the Help Text."); + // Teardown + } + + [Fact] + public void When_help_text_has_hidden_option_it_should_not_be_added_to_help_text_output() + { + // Fixture setup + // Exercize system + var sut = new HelpText(new HeadingInfo("CommandLine.Tests.dll", "1.9.4.131")); + sut.AddOptions( + new NotParsed( + TypeInfo.Create(typeof(Simple_Options_With_HelpText_Set_To_Long_Description)), + Enumerable.Empty())); + + // 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."); + //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."); + // Teardown + } [Fact] public void Long_help_text_without_spaces() @@ -327,6 +370,38 @@ public void Invoke_AutoBuild_for_Verbs_with_specific_verb_returns_appropriate_fo // Teardown } + [Fact] + public void Invoke_AutoBuild_for_Verbs_with_specific_verb_returns_appropriate_formatted_text_given_display_width_100() + { + // 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, maxDisplayWidth: 100); + + // Verify outcome + var lines = helpText.ToString().ToNotEmptyLines().TrimStringArray(); + +#if !PLATFORM_DOTNET + lines[0].Should().StartWithEquivalent("CommandLine"); + lines[1].ShouldBeEquivalentTo("Copyright (c) 2005 - 2015 Giacomo Stelluti Scala"); +#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."); + // Teardown + } + [Fact] public void Invoke_AutoBuild_for_Verbs_with_unknown_verb_returns_appropriate_formatted_text() { @@ -428,7 +503,7 @@ public void Invoke_AutoBuild_for_Options_with_Usage_returns_appropriate_formatte var helpText = HelpText.AutoBuild(fakeResult); // Verify outcome - var text = helpText.ToString(); + var text = helpText.ToString(); var lines = text.ToNotEmptyLines().TrimStringArray(); #if !PLATFORM_DOTNET lines[0].Should().StartWithEquivalent("CommandLine"); @@ -492,5 +567,95 @@ 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"; + + 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); + } + } + + [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); + + onErrorCalled.Should().BeTrue(); + actualResult.Heading.Should().Be(string.Format("{0} {1}", expectedTitle, expectedVersion)); + } + finally + { + ReflectionHelper.SetAttributeOverride(null); + } + } + + + [Fact] + public void AutoBuild_with_assembly_company_attribute_only() + { + try + { + 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); + + onErrorCalled.Should().BeFalse(); // Other attributes have fallback logic + actualResult.Copyright.Should().Be(string.Format("Copyright (C) {0} {1}", DateTime.Now.Year, expectedCompany)); + } + finally + { + ReflectionHelper.SetAttributeOverride(null); + } + } } -} \ No newline at end of file +}