// 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.Linq; using System.Reflection; using System.Text; 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; private int maximumDisplayWidth; private string heading; private string copyright; private bool additionalNewLineAfterOption; 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. /// public HelpText() : this(SentenceBuilder.Create(), string.Empty, string.Empty) { } /// /// Initializes a new instance of the class /// specifying the sentence builder. /// /// /// A instance. /// public HelpText(SentenceBuilder sentenceBuilder) : this(sentenceBuilder, string.Empty, string.Empty) { } /// /// Initializes a new instance of the class /// specifying heading string. /// /// An heading string or an instance of . /// Thrown when parameter is null or empty string. public HelpText(string heading) : this(SentenceBuilder.Create(), heading, string.Empty) { } /// /// Initializes a new instance of the class /// specifying the sentence builder and heading string. /// /// A instance. /// A string with heading or an instance of . public HelpText(SentenceBuilder sentenceBuilder, string heading) : this(sentenceBuilder, heading, string.Empty) { } /// /// Initializes a new instance of the class /// specifying heading and copyright strings. /// /// A string with heading or an instance of . /// A string with copyright or an instance of . /// Thrown when one or more parameters are null or empty strings. public HelpText(string heading, string copyright) : this(SentenceBuilder.Create(), heading, copyright) { } /// /// Initializes a new instance of the class /// specifying heading and copyright strings. /// /// A instance. /// A string with heading or an instance of . /// A string with copyright or an instance of . /// Thrown when one or more parameters are null or empty strings. public HelpText(SentenceBuilder sentenceBuilder, string heading, string copyright) { if (sentenceBuilder == null) throw new ArgumentNullException("sentenceBuilder"); if (heading == null) throw new ArgumentNullException("heading"); if (copyright == null) throw new ArgumentNullException("copyright"); preOptionsHelp = new StringBuilder(BuilderCapacity); postOptionsHelp = new StringBuilder(BuilderCapacity); try { maximumDisplayWidth = Console.WindowWidth; if (maximumDisplayWidth < 1) { maximumDisplayWidth = DefaultMaximumLength; } } catch (IOException) { maximumDisplayWidth = DefaultMaximumLength; } this.sentenceBuilder = sentenceBuilder; this.heading = heading; this.copyright = copyright; this.autoHelp = true; this.autoVersion = true; } /// /// Gets or sets the heading string. /// You can directly assign a instance. /// public string Heading { get { return heading; } set { if (value == null) throw new ArgumentNullException("value"); heading = value; } } /// /// Gets or sets the copyright string. /// You can directly assign a instance. /// public string Copyright { get { return copyright; } set { if (value == null) throw new ArgumentNullException("value"); copyright = value; } } /// /// Gets or sets the maximum width of the display. This determines word wrap when displaying the text. /// /// The maximum width of the display. public int MaximumDisplayWidth { get { return maximumDisplayWidth; } set { maximumDisplayWidth = value; } } /// /// Gets or sets a value indicating whether the format of options should contain dashes. /// It modifies behavior of method. /// public bool AddDashesToOption { get { return addDashesToOption; } set { addDashesToOption = value; } } /// /// Gets or sets a value indicating whether to add an additional line after the description of the specification. /// public bool AdditionalNewLineAfterOption { get { return 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. /// public bool AddEnumValuesToHelpText { get { return 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. /// public SentenceBuilder SentenceBuilder { get { return sentenceBuilder; } } /// /// Creates a new instance of the class using common defaults. /// /// /// An instance of class. /// /// 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. /// 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, int maxDisplayWidth = DefaultMaximumLength) { var auto = new HelpText { Heading = HeadingInfo.Empty, Copyright = CopyrightInfo.Empty, AdditionalNewLineAfterOption = true, 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) { errors = ((NotParsed)parserResult).Errors; if (errors.IsHelp() || errors.OnlyMeaningfulOnes().Any()) auto = onError(auto); } ReflectionHelper.GetAttribute() .Do(license => license.AddToHelpText(auto, true)); var usageAttr = ReflectionHelper.GetAttribute(); var usageLines = HelpText.RenderUsageTextAsLines(parserResult, onExample).ToMaybe(); if (usageAttr.IsJust() || usageLines.IsJust()) { var heading = auto.SentenceBuilder.UsageHeadingText(); if (heading.Length > 0) { if (auto.AddNewLineBetweenHelpSections) heading = Environment.NewLine + heading; auto.AddPreOptionsLine(heading); } } usageAttr.Do( usage => usage.AddToHelpText(auto, true)); usageLines.Do( lines => auto.AddPreOptionsLines(lines)); if ((verbsIndex && parserResult.TypeInfo.Choices.Any()) || errors.Any(e => e.Tag == ErrorType.NoVerbSelectedError)) { auto.AddDashesToOption = false; auto.AddVerbs(parserResult.TypeInfo.Choices.ToArray()); } else auto.AddOptions(parserResult); return auto; } /// /// 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. /// 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, 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"); var errors = ((NotParsed)parserResult).Errors; if (errors.Any(e => e.Tag == ErrorType.VersionRequestedError)) return new HelpText($"{HeadingInfo.Default}{Environment.NewLine}") { MaximumDisplayWidth = maxDisplayWidth }.AddPreOptionsLine(Environment.NewLine); if (!errors.Any(e => e.Tag == ErrorType.HelpVerbRequestedError)) 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), new Error[] { err }); return err.Matched ? 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); } /// /// Supplies a default parsing error handler implementation. /// /// The containing the instance that collected command line arguments parsed with class. /// The instance. public static HelpText DefaultParsingErrorsHandler(ParserResult parserResult, HelpText current) { if (parserResult == null) throw new ArgumentNullException("parserResult"); if (current == null) throw new ArgumentNullException("current"); if (((NotParsed)parserResult).Errors.OnlyMeaningfulOnes().Empty()) return current; var errors = RenderParsingErrorsTextAsLines(parserResult, current.SentenceBuilder.FormatError, current.SentenceBuilder.FormatMutuallyExclusiveSetErrors, 2); // indent with two spaces if (errors.Empty()) return current; return current .AddPreOptionsLine( string.Concat(Environment.NewLine, current.SentenceBuilder.ErrorsHeadingText())) .AddPreOptionsLines(errors); } /// /// Converts the help instance to a . /// /// This instance. /// The that contains the help screen. public static implicit operator string(HelpText info) { return info.ToString(); } /// /// Adds a text line after copyright and before options usage strings. /// /// A instance. /// Updated instance. /// Thrown when parameter is null or empty string. public HelpText AddPreOptionsLine(string value) { return AddPreOptionsLine(value, MaximumDisplayWidth); } /// /// Adds a text line at the bottom, after options usage string. /// /// A instance. /// Updated instance. /// Thrown when parameter is null or empty string. public HelpText AddPostOptionsLine(string value) { return AddLine(postOptionsHelp, value); } /// /// Adds text lines after copyright and before options usage strings. /// /// A sequence of line to add. /// Updated instance. public HelpText AddPreOptionsLines(IEnumerable lines) { lines.ForEach(line => AddPreOptionsLine(line)); return this; } /// /// Adds text lines at the bottom, after options usage string. /// /// A sequence of line to add. /// Updated instance. public HelpText AddPostOptionsLines(IEnumerable lines) { lines.ForEach(line => AddPostOptionsLine(line)); return this; } /// /// Adds a text block of lines after copyright and before options usage strings. /// /// A text block. /// Updated instance. public HelpText AddPreOptionsText(string text) { var lines = text.Split(new[] { Environment.NewLine }, StringSplitOptions.None); lines.ForEach(line => AddPreOptionsLine(line)); return this; } /// /// Adds a text block of lines at the bottom, after options usage string. /// /// A text block. /// Updated instance. public HelpText AddPostOptionsText(string text) { var lines = text.Split(new[] { Environment.NewLine }, StringSplitOptions.None); lines.ForEach(line => AddPostOptionsLine(line)); return this; } /// /// Adds a text block with options usage string. /// /// A parsing computation result. /// Thrown when parameter is null. public HelpText AddOptions(ParserResult result) { if (result == null) throw new ArgumentNullException("result"); return AddOptionsImpl( GetSpecificationsFromType(result.TypeInfo.Current), SentenceBuilder.RequiredWord(), SentenceBuilder.OptionGroupWord(), MaximumDisplayWidth); } /// /// Adds a text block with verbs usage string. /// /// The array of with verb commands. /// Thrown when parameter is null. /// Thrown if array is empty. public HelpText AddVerbs(params Type[] types) { if (types == null) throw new ArgumentNullException("types"); if (types.Length == 0) throw new ArgumentOutOfRangeException("types"); return AddOptionsImpl( AdaptVerbsToSpecifications(types), SentenceBuilder.RequiredWord(), SentenceBuilder.OptionGroupWord(), MaximumDisplayWidth); } /// /// Adds a text block with options usage string. /// /// The maximum length of the help screen. /// A parsing computation result. /// Thrown when parameter is null. public HelpText AddOptions(int maximumLength, ParserResult result) { if (result == null) throw new ArgumentNullException("result"); return AddOptionsImpl( GetSpecificationsFromType(result.TypeInfo.Current), SentenceBuilder.RequiredWord(), SentenceBuilder.OptionGroupWord(), maximumLength); } /// /// Adds a text block with verbs usage string. /// /// The maximum length of the help screen. /// The array of with verb commands. /// Thrown when parameter is null. /// Thrown if array is empty. public HelpText AddVerbs(int maximumLength, params Type[] types) { if (types == null) throw new ArgumentNullException("types"); if (types.Length == 0) throw new ArgumentOutOfRangeException("types"); return AddOptionsImpl( AdaptVerbsToSpecifications(types), SentenceBuilder.RequiredWord(), SentenceBuilder.OptionGroupWord(), maximumLength); } /// /// Builds a string that contains a parsing error message. /// /// The containing the instance that collected command line arguments parsed with class. /// The error formatting delegate. /// The specialized sequence formatting delegate. /// Number of spaces used to indent text. /// The that contains the parsing error message. public static string RenderParsingErrorsText( ParserResult parserResult, Func formatError, Func, string> formatMutuallyExclusiveSetErrors, int indent) { return string.Join( Environment.NewLine, RenderParsingErrorsTextAsLines(parserResult, formatError, formatMutuallyExclusiveSetErrors, indent)); } /// /// Builds a sequence of string that contains a parsing error message. /// /// The containing the instance that collected command line arguments parsed with class. /// The error formatting delegate. /// The specialized sequence formatting delegate. /// Number of spaces used to indent text. /// A sequence of that contains the parsing error message. public static IEnumerable RenderParsingErrorsTextAsLines( ParserResult parserResult, Func formatError, Func, string> formatMutuallyExclusiveSetErrors, int indent) { if (parserResult == null) throw new ArgumentNullException("parserResult"); var meaningfulErrors = ((NotParsed)parserResult).Errors.OnlyMeaningfulOnes(); if (meaningfulErrors.Empty()) yield break; foreach (var error in meaningfulErrors .Where(e => e.Tag != ErrorType.MutuallyExclusiveSetError)) { var line = new StringBuilder(indent.Spaces()) .Append(formatError(error)); yield return line.ToString(); } var mutuallyErrs = formatMutuallyExclusiveSetErrors( meaningfulErrors.OfType()); if (mutuallyErrs.Length > 0) { var lines = mutuallyErrs .Split(new[] { Environment.NewLine }, StringSplitOptions.None); foreach (var line in lines) yield return line; } } /// /// Builds a string with usage text block created using data and metadata. /// /// Type of parsing computation result. /// A parsing computation result. /// Resulting formatted text. public static string RenderUsageText(ParserResult parserResult) { return RenderUsageText(parserResult, example => example); } /// /// Builds a string with usage text block created using data and metadata. /// /// Type of parsing computation result. /// A parsing computation result. /// A mapping lambda normally used to translate text in other languages. /// Resulting formatted text. public static string RenderUsageText(ParserResult parserResult, Func mapperFunc) { return string.Join(Environment.NewLine, RenderUsageTextAsLines(parserResult, mapperFunc)); } /// /// Builds a string sequence with usage text block created using data and metadata. /// /// Type of parsing computation result. /// A parsing computation result. /// A mapping lambda normally used to translate text in other languages. /// Resulting formatted text. public static IEnumerable RenderUsageTextAsLines(ParserResult parserResult, Func mapperFunc) { if (parserResult == null) throw new ArgumentNullException("parserResult"); var usage = GetUsageFromType(parserResult.TypeInfo.Current); if (usage.MatchNothing()) yield break; var usageTuple = usage.FromJustOrFail(); var examples = usageTuple.Item2; var appAlias = usageTuple.Item1.ApplicationAlias ?? ReflectionHelper.GetAssemblyName(); foreach (var e in examples) { var example = mapperFunc(e); var exampleText = new StringBuilder(example.HelpText) .Append(':'); yield return exampleText.ToString(); var styles = example.GetFormatStylesOrDefault(); foreach (var s in styles) { var commandLine = new StringBuilder(OptionPrefixWidth.Spaces()) .Append(appAlias) .Append(' ') .Append(Parser.Default.FormatCommandLine(example.Sample, config => { config.PreferShortName = s.PreferShortName; config.GroupSwitches = s.GroupSwitches; config.UseEqualToken = s.UseEqualToken; config.SkipDefault = s.SkipDefault; })); yield return commandLine.ToString(); } } } /// /// Returns the help screen as a . /// /// The that contains the help screen. public override string ToString() { const int ExtraLength = 10; 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.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) { if (builder == null) { throw new ArgumentNullException(nameof(builder)); } if (value == null) { throw new ArgumentNullException(nameof(value)); } if (maximumLength < 1) { throw new ArgumentOutOfRangeException(nameof(value)); } value = value.TrimEnd(); builder.AppendWhen(builder.Length > 0, Environment.NewLine); builder.Append(TextWrapper.WrapAndIndentText(value, 0, maximumLength)); } private IEnumerable GetSpecificationsFromType(Type type) { var specs = type.GetSpecifications(Specification.FromProperty); var optionSpecs = specs .OfType(); if (autoHelp) optionSpecs = optionSpecs.Concat(new[] { MakeHelpEntry() }); if (autoVersion) optionSpecs = optionSpecs.Concat(new[] { MakeVersionEntry() }); var valueSpecs = specs .OfType() .OrderBy(v => v.Index); return Enumerable.Empty() .Concat(optionSpecs) .Concat(valueSpecs); } private static Maybe>> GetUsageFromType(Type type) { return type.GetUsageData().Map( tuple => { var prop = tuple.Item1; var attr = tuple.Item2; var examples = (IEnumerable)prop .GetValue(null, BindingFlags.Public | BindingFlags.Static | BindingFlags.GetProperty, null, null, null); return Tuple.Create(attr, examples); }); } private IEnumerable AdaptVerbsToSpecifications(IEnumerable types) { 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 + 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)); } return this; } private OptionSpecification MakeHelpEntry() { return OptionSpecification.NewSwitch( string.Empty, "help", false, sentenceBuilder.HelpCommandText(AddDashesToOption), string.Empty, false); } private OptionSpecification MakeVersionEntry() { return OptionSpecification.NewSwitch( string.Empty, "version", false, sentenceBuilder.VersionCommandText(AddDashesToOption), string.Empty, false); } private HelpText AddPreOptionsLine(string value, int maximumLength) { AddLine(preOptionsHelp, value, maximumLength); return this; } 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; optionsHelp.Append(" "); var name = new StringBuilder(maxLength) .BimapIf( specification.Tag == SpecificationType.Option, it => it.Append(AddOptionName(maxLength, (OptionSpecification)specification)), it => it.Append(AddValueName(maxLength, (ValueSpecification)specification))); optionsHelp .Append(name.Length < maxLength ? name.ToString().PadRight(maxLength) : name.ToString()) .Append(OptionToHelpTextSeparatorWidth.Spaces()); var optionHelpText = specification.HelpText; if (addEnumValuesToHelpText && specification.EnumValues.Any()) optionHelpText += " Valid values: " + string.Join(", ", specification.EnumValues); specification.DefaultValue.Do( defaultValue => optionHelpText = "(Default: {0}) ".FormatInvariant(FormatDefaultValue(defaultValue)) + optionHelpText); var optionGroupSpecification = GetOptionGroupSpecification(); if (specification.Required && optionGroupSpecification == null) optionHelpText = "{0} ".FormatInvariant(requiredWord) + optionHelpText; if (optionGroupSpecification != null) { 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(indented) .Append(Environment.NewLine) .AppendWhen(additionalNewLineAfterOption, Environment.NewLine); return this; } private string AddOptionName(int maxLength, OptionSpecification specification) { return new StringBuilder(maxLength) .MapIf( specification.ShortName.Length > 0, it => it .AppendWhen(addDashesToOption, '-') .AppendFormat("{0}", specification.ShortName) .AppendFormatWhen(specification.MetaValue.Length > 0, " {0}", specification.MetaValue) .AppendWhen(specification.LongName.Length > 0, ", ")) .MapIf( specification.LongName.Length > 0, it => it .AppendWhen(addDashesToOption, "--") .AppendFormat("{0}", specification.LongName) .AppendFormatWhen(specification.MetaValue.Length > 0, "={0}", specification.MetaValue)) .ToString(); } private string AddValueName(int maxLength, ValueSpecification specification) { return new StringBuilder(maxLength) .BimapIf( specification.MetaName.Length > 0, it => it.AppendFormat("{0} (pos. {1})", specification.MetaName, specification.Index), it => it.AppendFormat("value pos. {0}", specification.Index)) .AppendFormatWhen( specification.MetaValue.Length > 0, " {0}", specification.MetaValue) .ToString(); } private HelpText AddLine(StringBuilder builder, string value) { AddLine(builder, value, MaximumDisplayWidth); return this; } private int GetMaxLength(IEnumerable specifications) { return specifications.Aggregate(0, (length, spec) => { if (spec.Hidden) return length; var specLength = spec.Tag == SpecificationType.Option ? GetMaxOptionLength((OptionSpecification)spec) : GetMaxValueLength((ValueSpecification)spec); return Math.Max(length, specLength); }); } private int GetMaxOptionLength(OptionSpecification spec) { var specLength = 0; var hasShort = spec.ShortName.Length > 0; var hasLong = spec.LongName.Length > 0; var metaLength = 0; if (spec.MetaValue.Length > 0) metaLength = spec.MetaValue.Length + 1; if (hasShort) { ++specLength; if (AddDashesToOption) ++specLength; specLength += metaLength; } if (hasLong) { specLength += spec.LongName.Length; if (AddDashesToOption) specLength += OptionPrefixWidth; specLength += metaLength; } if (hasShort && hasLong) specLength += OptionPrefixWidth; return specLength; } private int GetMaxValueLength(ValueSpecification spec) { var specLength = 0; var hasMeta = spec.MetaName.Length > 0; var metaLength = 0; if (spec.MetaValue.Length > 0) metaLength = spec.MetaValue.Length + 1; if (hasMeta) specLength += spec.MetaName.Length + spec.Index.ToStringInvariant().Length + 8; //METANAME (pos. N) else specLength += spec.Index.ToStringInvariant().Length + 11; // "value pos. N" specLength += metaLength; return specLength; } private static string FormatDefaultValue(T value) { if (value is bool) return value.ToStringLocal().ToLowerInvariant(); if (value is string) return value.ToStringLocal(); var asEnumerable = value as IEnumerable; if (asEnumerable == null) return value.ToStringLocal(); var builder = new StringBuilder(); foreach (var item in asEnumerable) builder .Append(item.ToStringLocal()) .Append(" "); return builder.Length > 0 ? builder.ToString(0, builder.Length - 1) : string.Empty; } } }