diff --git a/src/CommandLine/Core/GetoptTokenizer.cs b/src/CommandLine/Core/GetoptTokenizer.cs index b8c97fc2..1d97125f 100644 --- a/src/CommandLine/Core/GetoptTokenizer.cs +++ b/src/CommandLine/Core/GetoptTokenizer.cs @@ -88,36 +88,6 @@ public static Result, Error> Tokenize( return Result.Succeed, Error>(tokens.AsEnumerable(), errors.AsEnumerable()); } - public static Result, Error> ExplodeOptionList( - Result, Error> tokenizerResult, - Func> optionSequenceWithSeparatorLookup) - { - var tokens = tokenizerResult.SucceededWith().Memoize(); - - var exploded = new List(tokens is ICollection coll ? coll.Count : tokens.Count()); - var nothing = Maybe.Nothing(); // Re-use same Nothing instance for efficiency - var separator = nothing; - foreach (var token in tokens) { - if (token.IsName()) { - separator = optionSequenceWithSeparatorLookup(token.Text); - exploded.Add(token); - } else { - // Forced values are never considered option values, so they should not be split - if (separator.MatchJust(out char sep) && sep != '\0' && !token.IsValueForced()) { - if (token.Text.Contains(sep)) { - exploded.AddRange(token.Text.Split(sep).Select(Token.ValueFromSeparator)); - } else { - exploded.Add(token); - } - } else { - exploded.Add(token); - } - separator = nothing; // Only first value after a separator can possibly be split - } - } - return Result.Succeed(exploded as IEnumerable, tokenizerResult.SuccessMessages()); - } - public static Func< IEnumerable, IEnumerable, @@ -131,7 +101,7 @@ public static Func< return (arguments, optionSpecs) => { var tokens = GetoptTokenizer.Tokenize(arguments, name => NameLookup.Contains(name, optionSpecs, nameComparer), ignoreUnknownArguments, enableDashDash, posixlyCorrect); - var explodedTokens = GetoptTokenizer.ExplodeOptionList(tokens, name => NameLookup.HavingSeparator(name, optionSpecs, nameComparer)); + var explodedTokens = Tokenizer.ExplodeOptionList(tokens, name => NameLookup.HavingSeparator(name, optionSpecs, nameComparer)); return explodedTokens; }; } diff --git a/src/CommandLine/Parser.cs b/src/CommandLine/Parser.cs index 4301aa52..ad057e6e 100644 --- a/src/CommandLine/Parser.cs +++ b/src/CommandLine/Parser.cs @@ -185,16 +185,28 @@ private static Result, Error> Tokenize( IEnumerable optionSpecs, ParserSettings settings) { - return settings.GetoptMode - ? GetoptTokenizer.ConfigureTokenizer( + switch (settings.ParserMode) + { + case ParserMode.GetoptParserV1: + return Tokenizer.ConfigureTokenizer( + settings.NameComparer, + settings.IgnoreUnknownArguments, + settings.EnableDashDash)(arguments, optionSpecs); + + case ParserMode.GetoptParserV2: + return GetoptTokenizer.ConfigureTokenizer( settings.NameComparer, settings.IgnoreUnknownArguments, settings.EnableDashDash, - settings.PosixlyCorrect)(arguments, optionSpecs) - : Tokenizer.ConfigureTokenizer( + settings.PosixlyCorrect)(arguments, optionSpecs); + + // No need to test ParserMode.Default, as it should always be one of the above modes + default: + return Tokenizer.ConfigureTokenizer( settings.NameComparer, settings.IgnoreUnknownArguments, settings.EnableDashDash)(arguments, optionSpecs); + } } private static ParserResult MakeParserResult(ParserResult parserResult, ParserSettings settings) diff --git a/src/CommandLine/ParserSettings.cs b/src/CommandLine/ParserSettings.cs index 5ed73f30..582eb2be 100644 --- a/src/CommandLine/ParserSettings.cs +++ b/src/CommandLine/ParserSettings.cs @@ -9,6 +9,14 @@ namespace CommandLine { + public enum ParserMode + { + GetoptParserV1, + GetoptParserV2, + + Default = GetoptParserV1 + } + /// /// Provides settings for . Once consumed cannot be reused. /// @@ -27,7 +35,7 @@ public class ParserSettings : IDisposable private Maybe enableDashDash; private int maximumDisplayWidth; private Maybe allowMultiInstance; - private bool getoptMode; + private ParserMode parserMode; private Maybe posixlyCorrect; /// @@ -41,7 +49,7 @@ public ParserSettings() autoVersion = true; parsingCulture = CultureInfo.InvariantCulture; maximumDisplayWidth = GetWindowWidth(); - getoptMode = false; + parserMode = ParserMode.Default; enableDashDash = Maybe.Nothing(); allowMultiInstance = Maybe.Nothing(); posixlyCorrect = Maybe.Nothing(); @@ -166,11 +174,11 @@ public bool AutoVersion /// /// Gets or sets a value indicating whether enable double dash '--' syntax, /// that forces parsing of all subsequent tokens as values. - /// If GetoptMode is true, this defaults to true, but can be turned off by explicitly specifying EnableDashDash = false. + /// Normally defaults to false. If ParserMode = ParserMode.GetoptParserV2, this defaults to true, but can be turned off by explicitly specifying EnableDashDash = false. /// public bool EnableDashDash { - get => enableDashDash.MatchJust(out bool value) ? value : getoptMode; + get => enableDashDash.MatchJust(out bool value) ? value : (parserMode == ParserMode.GetoptParserV2); set => PopsicleSetter.Set(Consumed, ref enableDashDash, Maybe.Just(value)); } @@ -185,21 +193,38 @@ public int MaximumDisplayWidth /// /// Gets or sets a value indicating whether options are allowed to be specified multiple times. - /// If GetoptMode is true, this defaults to true, but can be turned off by explicitly specifying AllowMultiInstance = false. + /// If ParserMode = ParserMode.GetoptParserV2, this defaults to true, but can be turned off by explicitly specifying AllowMultiInstance = false. /// public bool AllowMultiInstance { - get => allowMultiInstance.MatchJust(out bool value) ? value : getoptMode; + get => allowMultiInstance.MatchJust(out bool value) ? value : (parserMode == ParserMode.GetoptParserV2); set => PopsicleSetter.Set(Consumed, ref allowMultiInstance, Maybe.Just(value)); } /// - /// Whether strict getopt-like processing is applied to option values; if true, AllowMultiInstance and EnableDashDash will default to true as well. + /// Set this to change how the parser processes command-line arguments. Currently valid values are: + /// + /// + /// Classic + /// Uses - for short options and -- for long options. + /// Values of long options can only start with a - character if the = syntax is used. + /// E.g., "--string-option -x" will consider "-x" to be an option, not the value of "--string-option", + /// but "--string-option=-x" will consider "-x" to be the value of "--string-option". + /// + /// + /// Getopt + /// Strict getopt-like processing is applied to option values. + /// Mostly like classic mode, except that option values with = and with space are more consistent. + /// After an option that takes a value, and whose value was not specified with "=", the next argument will be considered the value even if it starts with "-". + /// E.g., both "--string-option=-x" and "--string-option -x" will consider "-x" to be the value of "--string-option". + /// If this mode is chosen, AllowMultiInstance and EnableDashDash will default to true as well, though they can be explicitly turned off if desired. + /// + /// /// - public bool GetoptMode + public ParserMode ParserMode { - get => getoptMode; - set => PopsicleSetter.Set(Consumed, ref getoptMode, value); + get => parserMode; + set => PopsicleSetter.Set(Consumed, ref parserMode, value); } /// diff --git a/tests/CommandLine.Tests/Unit/Core/GetoptTokenizerTests.cs b/tests/CommandLine.Tests/Unit/Core/GetoptTokenizerTests.cs index 337a9a3f..d4669a66 100644 --- a/tests/CommandLine.Tests/Unit/Core/GetoptTokenizerTests.cs +++ b/tests/CommandLine.Tests/Unit/Core/GetoptTokenizerTests.cs @@ -24,7 +24,7 @@ public void Explode_scalar_with_separator_in_odd_args_input_returns_sequence() // Exercize system var result = - GetoptTokenizer.ExplodeOptionList( + Tokenizer.ExplodeOptionList( Result.Succeed( Enumerable.Empty().Concat(new[] { Token.Name("i"), Token.Value("10"), Token.Name("string-seq"), Token.Value("aaa,bb,cccc"), Token.Name("switch") }), @@ -47,7 +47,7 @@ public void Explode_scalar_with_separator_in_even_args_input_returns_sequence() // Exercize system var result = - GetoptTokenizer.ExplodeOptionList( + Tokenizer.ExplodeOptionList( Result.Succeed( Enumerable.Empty().Concat(new[] { Token.Name("x"), Token.Name("string-seq"), Token.Value("aaa,bb,cccc"), Token.Name("switch") }), @@ -90,7 +90,7 @@ public void Should_return_error_if_option_format_with_equals_is_not_correct() var errors = result.SuccessMessages(); Assert.NotNull(errors); - Assert.Equal(1, errors.Count()); + Assert.NotEmpty(errors); Assert.Equal(ErrorType.BadFormatTokenError, errors.First().Tag); var tokens = result.SucceededWith(); diff --git a/tests/CommandLine.Tests/Unit/GetoptParserTests.cs b/tests/CommandLine.Tests/Unit/GetoptParserTests.cs index cd2a1577..8c5aee6d 100644 --- a/tests/CommandLine.Tests/Unit/GetoptParserTests.cs +++ b/tests/CommandLine.Tests/Unit/GetoptParserTests.cs @@ -155,7 +155,7 @@ public void Getopt_parser_without_posixly_correct_allows_mixed_options_and_nonop { // Arrange var sut = new Parser(config => { - config.GetoptMode = true; + config.ParserMode = ParserMode.GetoptParserV2; config.PosixlyCorrect = false; }); @@ -215,7 +215,7 @@ public void Getopt_parser_with_posixly_correct_stops_parsing_at_first_nonoption( { // Arrange var sut = new Parser(config => { - config.GetoptMode = true; + config.ParserMode = ParserMode.GetoptParserV2; config.PosixlyCorrect = true; config.EnableDashDash = true; }); @@ -233,7 +233,7 @@ public void Getopt_mode_defaults_to_EnableDashDash_being_true() { // Arrange var sut = new Parser(config => { - config.GetoptMode = true; + config.ParserMode = ParserMode.GetoptParserV2; config.PosixlyCorrect = false; }); var args = new string [] {"--stringvalue", "foo", "256", "--", "-x", "-sbar" }; @@ -259,7 +259,7 @@ public void Getopt_mode_can_have_EnableDashDash_expicitly_disabled() { // Arrange var sut = new Parser(config => { - config.GetoptMode = true; + config.ParserMode = ParserMode.GetoptParserV2; config.PosixlyCorrect = false; config.EnableDashDash = false; }); diff --git a/tests/CommandLine.Tests/Unit/SequenceParsingTests.cs b/tests/CommandLine.Tests/Unit/SequenceParsingTests.cs index cb65e42f..14da14d9 100644 --- a/tests/CommandLine.Tests/Unit/SequenceParsingTests.cs +++ b/tests/CommandLine.Tests/Unit/SequenceParsingTests.cs @@ -17,9 +17,9 @@ public class SequenceParsingTests { // Issue #91 [Theory] - [InlineData(false)] - [InlineData(true)] - public static void Enumerable_with_separator_before_values_does_not_try_to_parse_too_much(bool useGetoptMode) + [InlineData(ParserMode.GetoptParserV1)] + [InlineData(ParserMode.GetoptParserV2)] + public static void Enumerable_with_separator_before_values_does_not_try_to_parse_too_much(ParserMode parserMode) { var args = "--exclude=a,b InputFile.txt".Split(); var expected = new Options_For_Issue_91 { @@ -27,7 +27,7 @@ public static void Enumerable_with_separator_before_values_does_not_try_to_parse Included = Enumerable.Empty(), InputFileName = "InputFile.txt", }; - var sut = new Parser(parserSettings => { parserSettings.GetoptMode = useGetoptMode; }); + var sut = new Parser(parserSettings => { parserSettings.ParserMode = parserMode; }); var result = sut.ParseArguments(args); result.Should().BeOfType>(); result.As>().Value.Should().BeEquivalentTo(expected); @@ -35,13 +35,13 @@ public static void Enumerable_with_separator_before_values_does_not_try_to_parse // Issue #396 [Theory] - [InlineData(false)] - [InlineData(true)] - public static void Options_with_similar_names_are_not_ambiguous(bool useGetoptMode) + [InlineData(ParserMode.GetoptParserV1)] + [InlineData(ParserMode.GetoptParserV2)] + public static void Options_with_similar_names_are_not_ambiguous(ParserMode parserMode) { var args = new[] { "--configure-profile", "deploy", "--profile", "local" }; var expected = new Options_With_Similar_Names { ConfigureProfile = "deploy", Profile = "local", Deploys = Enumerable.Empty() }; - var sut = new Parser(parserSettings => { parserSettings.GetoptMode = useGetoptMode; }); + var sut = new Parser(parserSettings => { parserSettings.ParserMode = parserMode; }); var result = sut.ParseArguments(args); result.Should().BeOfType>(); result.As>().Value.Should().BeEquivalentTo(expected); @@ -68,17 +68,17 @@ public static void Values_with_same_name_as_sequence_option_do_not_cause_later_v // Issue #454 [Theory] - [InlineData(false)] - [InlineData(true)] + [InlineData(ParserMode.GetoptParserV1)] + [InlineData(ParserMode.GetoptParserV2)] - public static void Enumerable_with_colon_separator_before_values_does_not_try_to_parse_too_much(bool useGetoptMode) + public static void Enumerable_with_colon_separator_before_values_does_not_try_to_parse_too_much(ParserMode parserMode) { var args = "-c chanA:chanB file.hdf5".Split(); var expected = new Options_For_Issue_454 { Channels = new[] { "chanA", "chanB" }, ArchivePath = "file.hdf5", }; - var sut = new Parser(parserSettings => { parserSettings.GetoptMode = useGetoptMode; }); + var sut = new Parser(parserSettings => { parserSettings.ParserMode = parserMode; }); var result = sut.ParseArguments(args); result.Should().BeOfType>(); result.As>().Value.Should().BeEquivalentTo(expected); @@ -86,14 +86,14 @@ public static void Enumerable_with_colon_separator_before_values_does_not_try_to // Issue #510 [Theory] - [InlineData(false)] - [InlineData(true)] + [InlineData(ParserMode.GetoptParserV1)] + [InlineData(ParserMode.GetoptParserV2)] - public static void Enumerable_before_values_does_not_try_to_parse_too_much(bool useGetoptMode) + public static void Enumerable_before_values_does_not_try_to_parse_too_much(ParserMode parserMode) { var args = new[] { "-a", "1,2", "c" }; var expected = new Options_For_Issue_510 { A = new[] { "1", "2" }, C = "c" }; - var sut = new Parser(parserSettings => { parserSettings.GetoptMode = useGetoptMode; }); + var sut = new Parser(parserSettings => { parserSettings.ParserMode = parserMode; }); var result = sut.ParseArguments(args); result.Should().BeOfType>(); result.As>().Value.Should().BeEquivalentTo(expected); @@ -101,17 +101,17 @@ public static void Enumerable_before_values_does_not_try_to_parse_too_much(bool // Issue #617 [Theory] - [InlineData(false)] - [InlineData(true)] + [InlineData(ParserMode.GetoptParserV1)] + [InlineData(ParserMode.GetoptParserV2)] - public static void Enumerable_with_enum_before_values_does_not_try_to_parse_too_much(bool useGetoptMode) + public static void Enumerable_with_enum_before_values_does_not_try_to_parse_too_much(ParserMode parserMode) { var args = "--fm D,C a.txt".Split(); var expected = new Options_For_Issue_617 { Mode = new[] { FMode.D, FMode.C }, Files = new[] { "a.txt" }, }; - var sut = new Parser(parserSettings => { parserSettings.GetoptMode = useGetoptMode; }); + var sut = new Parser(parserSettings => { parserSettings.ParserMode = parserMode; }); var result = sut.ParseArguments(args); result.Should().BeOfType>(); result.As>().Value.Should().BeEquivalentTo(expected); @@ -119,10 +119,10 @@ public static void Enumerable_with_enum_before_values_does_not_try_to_parse_too_ // Issue #619 [Theory] - [InlineData(false)] - [InlineData(true)] + [InlineData(ParserMode.GetoptParserV1)] + [InlineData(ParserMode.GetoptParserV2)] - public static void Separator_just_before_values_does_not_try_to_parse_values(bool useGetoptMode) + public static void Separator_just_before_values_does_not_try_to_parse_values(ParserMode parserMode) { var args = "--outdir ./x64/Debug --modules ../utilities/x64/Debug,../auxtool/x64/Debug m_xfunit.f03 m_xfunit_assertion.f03".Split(); var expected = new Options_For_Issue_619 { @@ -131,7 +131,7 @@ public static void Separator_just_before_values_does_not_try_to_parse_values(boo Ignores = Enumerable.Empty(), Srcs = new[] { "m_xfunit.f03", "m_xfunit_assertion.f03" }, }; - var sut = new Parser(parserSettings => { parserSettings.GetoptMode = useGetoptMode; }); + var sut = new Parser(parserSettings => { parserSettings.ParserMode = parserMode; }); var result = sut.ParseArguments(args); result.Should().BeOfType>(); result.As>().Value.Should().BeEquivalentTo(expected);