diff --git a/src/CommandLine/Text/CopyrightInfo.cs b/src/CommandLine/Text/CopyrightInfo.cs index 3fd2b6a8..c8bc3593 100644 --- a/src/CommandLine/Text/CopyrightInfo.cs +++ b/src/CommandLine/Text/CopyrightInfo.cs @@ -27,11 +27,11 @@ public class CopyrightInfo /// /// An empty object used for initialization. /// - public static CopyrightInfo Empty + public static CopyrightInfo Empty { get { - return new CopyrightInfo("author", 1); + return new CopyrightInfo("author", DateTime.Now.Year); } } @@ -115,12 +115,13 @@ public static CopyrightInfo Default case MaybeType.Just: return new CopyrightInfo(copyrightAttr.FromJustOrFail()); default: - // if no copyright attribute exist but a company attribute does, use it as copyright holder - return new CopyrightInfo( - ReflectionHelper.GetAttribute().FromJustOrFail( - new InvalidOperationException("CopyrightInfo::Default requires that you define AssemblyCopyrightAttribute or AssemblyCompanyAttribute.") - ).Company, - DateTime.Now.Year); + var companyAttr = ReflectionHelper.GetAttribute(); + return companyAttr.IsNothing() + //if both copyrightAttr and companyAttr aren't available in Assembly,don't fire Exception + ? Empty + // if no copyright attribute exist but a company attribute does, use it as copyright holder + : new CopyrightInfo(companyAttr.FromJust().Company, DateTime.Now.Year); + } } } @@ -192,4 +193,4 @@ protected virtual string FormatYears(int[] years) return yearsPart.ToString(); } } -} \ No newline at end of file +} diff --git a/src/CommandLine/Text/HelpText.cs b/src/CommandLine/Text/HelpText.cs index f5fd8065..94b7e4f3 100644 --- a/src/CommandLine/Text/HelpText.cs +++ b/src/CommandLine/Text/HelpText.cs @@ -266,7 +266,7 @@ public bool AdditionalNewLineAfterOption /// public bool AddNewLineBetweenHelpSections { - get { return addNewLineBetweenHelpSections; } + get { return addNewLineBetweenHelpSections; } set { addNewLineBetweenHelpSections = value; } } @@ -389,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. @@ -400,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"); @@ -410,13 +427,25 @@ public static HelpText AutoBuild(ParserResult parserResult, int maxDisplay 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); } /// @@ -431,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, @@ -732,8 +760,8 @@ public override string ToString() var result = new StringBuilder(sbLength); result.Append(heading) - .AppendWhen(!string.IsNullOrEmpty(copyright), - Environment.NewLine, + .AppendWhen(!string.IsNullOrEmpty(copyright), + Environment.NewLine, copyright) .AppendWhen(preOptionsHelp.SafeLength() > 0, NewLineIfNeededBefore(preOptionsHelp), @@ -743,15 +771,15 @@ public override string ToString() Environment.NewLine, Environment.NewLine, optionsHelp.SafeToString()) - .AppendWhen(postOptionsHelp.SafeLength() > 0, + .AppendWhen(postOptionsHelp.SafeLength() > 0, NewLineIfNeededBefore(postOptionsHelp), Environment.NewLine, postOptionsHelp.ToString()); string NewLineIfNeededBefore(StringBuilder sb) { - if (AddNewLineBetweenHelpSections - && result.Length > 0 + if (AddNewLineBetweenHelpSections + && result.Length > 0 && !result.SafeEndsWith(Environment.NewLine) && !sb.SafeStartsWith(Environment.NewLine)) return Environment.NewLine; @@ -952,7 +980,7 @@ specification is OptionSpecification optionSpecification && if (optionGroupSpecification != null) { - optionHelpText = "({0}: {1}) ".FormatInvariant(optionGroupWord, optionGroupSpecification.Group) + optionHelpText; + 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 diff --git a/tests/CommandLine.Tests/Unit/Text/HelpTextTests.cs b/tests/CommandLine.Tests/Unit/Text/HelpTextTests.cs index 1364c982..29e64431 100644 --- a/tests/CommandLine.Tests/Unit/Text/HelpTextTests.cs +++ b/tests/CommandLine.Tests/Unit/Text/HelpTextTests.cs @@ -191,7 +191,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(headingInfo) { MaximumDisplayWidth = 100} ; + var sut = new HelpText(headingInfo) { MaximumDisplayWidth = 100 }; sut.AddOptions( new NotParsed( TypeInfo.Create(typeof(Simple_Options_With_HelpText_Set_To_Long_Description)), @@ -270,7 +270,7 @@ public void Long_pre_and_post_lines_without_spaces() // Teardown } - + [Fact] public void Invoking_RenderParsingErrorsText_returns_appropriate_formatted_text() { @@ -296,32 +296,32 @@ public void Invoking_RenderParsingErrorsText_returns_appropriate_formatted_text( 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; - case ErrorType.MissingGroupOptionError: + 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(); - } - }; + default: + throw new InvalidOperationException(); + } + }; Func, string> fakeMutExclRenderer = _ => string.Empty; @@ -420,12 +420,12 @@ public void Invoke_AutoBuild_for_Verbs_with_specific_verb_returns_appropriate_fo }); // Exercize system - var helpText = HelpText.AutoBuild(fakeResult, maxDisplayWidth: 100); + var helpText = HelpText.AutoBuild(fakeResult, maxDisplayWidth: 100); // Verify outcome var lines = helpText.ToString().ToNotEmptyLines().TrimStringArray(); lines[0].Should().Be(HeadingInfo.Default.ToString()); - lines[1].Should().Be(CopyrightInfo.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."); @@ -451,7 +451,7 @@ public void Invoke_AutoBuild_for_Verbs_with_unknown_verb_returns_appropriate_for var lines = helpText.ToString().ToNotEmptyLines().TrimStringArray(); lines[0].Should().Be(HeadingInfo.Default.ToString()); - lines[1].Should().Be(CopyrightInfo.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."); @@ -495,7 +495,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); @@ -584,7 +584,7 @@ public void Invoke_AutoBuild_for_Options_with_Usage_returns_appropriate_formatte var text = helpText.ToString(); var lines = text.ToLines().TrimStringArray(); - + lines.Should().StartWith(expected); } @@ -667,23 +667,17 @@ public void Default_set_to_sequence_should_be_properly_printed() [Fact] public void AutoBuild_when_no_assembly_attributes() { - string expectedCopyright = "Copyright (C) 1 author"; + string expectedCopyright = $"Copyright (C) {DateTime.Now.Year} 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(); + 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() { @@ -697,15 +691,8 @@ public void AutoBuild_with_assembly_title_and_version_attributes_only() }); 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(); + 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)); } @@ -720,7 +707,7 @@ public void AutoBuild_with_assembly_company_attribute_only() }); ParserResult fakeResult = new NotParsed( - TypeInfo.Create(typeof (Simple_Options)), new Error[0]); + TypeInfo.Create(typeof(Simple_Options)), new Error[0]); bool onErrorCalled = false; HelpText actualResult = HelpText.AutoBuild(fakeResult, ht => { @@ -748,7 +735,7 @@ public void HelpTextHonoursLineBreaks() { // Fixture setup // Exercize system - var sut = new HelpText {AddDashesToOption = true} + var sut = new HelpText { AddDashesToOption = true } .AddOptions(new NotParsed(TypeInfo.Create(typeof(HelpTextWithLineBreaks_Options)), Enumerable.Empty())); @@ -758,7 +745,7 @@ public void HelpTextHonoursLineBreaks() 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 } @@ -767,7 +754,7 @@ public void HelpTextHonoursIndentationAfterLineBreaks() { // Fixture setup // Exercize system - var sut = new HelpText {AddDashesToOption = true} + var sut = new HelpText { AddDashesToOption = true } .AddOptions(new NotParsed(TypeInfo.Create(typeof(HelpTextWithLineBreaks_Options)), Enumerable.Empty())); @@ -777,7 +764,7 @@ public void HelpTextHonoursIndentationAfterLineBreaks() 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 } @@ -786,7 +773,7 @@ public void HelpTextPreservesIndentationAcrossWordWrap() { // Fixture setup // Exercise system - var sut = new HelpText {AddDashesToOption = true,MaximumDisplayWidth = 60} + var sut = new HelpText { AddDashesToOption = true, MaximumDisplayWidth = 60 } .AddOptions(new NotParsed(TypeInfo.Create(typeof(HelpTextWithLineBreaksAndSubIndentation_Options)), Enumerable.Empty())); @@ -809,7 +796,7 @@ public void HelpTextIsConsitentRegardlessOfCompileTimeLineStyle() { // Fixture setup // Exercize system - var sut = new HelpText {AddDashesToOption = true} + var sut = new HelpText { AddDashesToOption = true } .AddOptions(new NotParsed(TypeInfo.Create(typeof(HelpTextWithMixedLineBreaks_Options)), Enumerable.Empty())); @@ -819,7 +806,7 @@ public void HelpTextIsConsitentRegardlessOfCompileTimeLineStyle() 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 } @@ -828,14 +815,14 @@ public void HelpTextPreservesIndentationAcrossWordWrapWithSmallMaximumDisplayWid { // Fixture setup // Exercise system - var sut = new HelpText {AddDashesToOption = true,MaximumDisplayWidth = 10} + 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); - + + Assert.True(sut.ToString().Length > 0); + // Teardown } @@ -926,5 +913,117 @@ public void Options_Should_Render_Multiple_OptionGroups_When_Available() 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 } }